Принцип работы highlight.js

Код языка программирования — неоднородная штука. Например ключевые слова типа for, if и пр. не имеют смысла внутри строк. Зато внутри строк могут встречатсья escape-символы типа \", которые экранируют закрывающие кавычки. А внутри комментариев обычно вообще ничего не встречается, кроме конца этого самого комментария. Эти участки кода называются в highlight.js режимами (modes).

Каждый режим характеризуется:

В итоге, вся работа highlight.js заключается в поиске этих режимов и ключевых слов внутри них. По мере нахождения они заключаются в конструкции <span class="...">...</span>, где в качестве имени класса проставляется либо название группы ключевых слов ("keyword", "literal", "built-in"), либо название режима ("string", "comment", "number"). Эти названия классов используются в CSS страницы для назначения конкретного оформления элементам языка.

Небольшое отступление для программистов, знакомых с написанием парсеров. Хотя highlight.js внутри себя использует регулярные выражения, его грамматики не ограничиваются регулярными. Фактически регулярки используются только как механизма лексера входного потока, а лексемы уже обрабатываются честно в своих контекстах. Так сделано потому что в javascript'е разбор строки регулярками гораздо быстрее перебора символов.

Описание языка

Описание языка -- это javascript'овый класс, который содержит описание режимов и ключевых слов. Структура у него такая:

some_lang = {
  defaultMode: {
    // описание режима по умолчанию
  },
  modes: [
    // список дополнительных режимов
  ],
  case_insensitive: true
}

Свойство "case_insensitive", если равно "true", означает, что язык принимает любой регистр символов (например HTML, CSS и Delphi). Его можно не указывать для регистрозависимых языков.

Режим по умолчанию

В большинстве языков режим по умолчанию -- это самое "мясо" кода, в котором и встречаются большинство ключевых слов, поэтому в нем самое главное свойство -- это один или несколько наборов ключевых слов -- "keywords".

В простом случае он задается в виде javascript-объекта, свойствами которого служат сами ключевые слова, а значениями каждого свойства -- цифра 1 (это на самом деле вес релевантности, но это сейчас неважно.) Выглядит это примерно так:

keywords: {'else': 1, 'for': 1, 'if': 1, 'while': 1}

В языках бывает несколько разных видов ключевых слов (например, в Javascript'е слова "true", "false" и "null" считаются не ключевыми словами, а литералами. Для задания нескольких групп свойство "keywords" превращается в объект, где свойствами служат имена классов улючевых слов, а значениями -- сами ключевые слова, как в первом случае:

keywords: {
  'keyword': {'else': 1, 'for': 1, 'if': 1, 'while': 1},
  'literal': {'false': 1, 'true': 1, 'null': 1}
}

Однако highlight.js сможет находить ключевые слова, только если будет знать, как разбивать код языка на составляющие его идентификаторы. Для этого служит набор строк для регулярных выражений "lexems". В итоге, типичное описание режима по умолчанию выглядит так:

defaultMode: {
  lexems: [IDENT_RE],
  keywords: {'else': 1, 'for': 1, 'if': 1, 'while': 1}
}

IDENT_RE -- это регулярное выражение, описывающее идентификаторы для большинства языков. Поэтому, для большинства языков список "lexems" будет выглядеть именно так, как в этом примере. В качестве яркого исключения можно привести HTML, где режим по умолчанию -- это пользовательский текст, который не содержит вообще никаких ключевых слов, и в нем нет смысл искать идентификаторы.

Следующий нужный атрибут режима по умолчанию -- это список названий дополнительных режимов, которые в нем встречаются: "contains".

defaultMode: {
  lexems: [...],
  keywords: {...},
  contains: ['string', 'comment', 'number']
}

Сами дополнительные режимы лежат в списке modes прямо в объекте языка.

Дополнительные режимы

Режимы, перечисленные в массиве "modes", могут содержать все те же свойства, что и режим по умолчанию (lexems, keywords, contains), то есть описывать собственные ключевые слова и свои подрежимы. Но также у них есть отдельные атрибуты:

Внимание! При задании регулярных выражений в "lexems", "begin" и "end" надо помнить, что это строки, а значит для того, чтобы написать туда обратный слеш ('\') его по правилам javascript'а надо удваивать.

Релевантность

highlight.js пытается автоматически определять язык расцвечиваемого фрагмента кода. Чтобы эта эвристика работала лучше, ей надо помогать в определении языка. Для этого для режимов языка можно указывать свойство "relevance" - число, которое означает специфичность описываемой конструкции для языка.

Пример. В Питоне есть специфичные способы задания строк не просто в кавычках, а с модификаторами: r"...", u"...". Если в каком-то отвлеченном коде встречаются строки заданные подобным образом, то это с очень высокой вероятностью означает, что этот код - на Питоне. Чтобы это подчеркнуть, в режим таких строк записывается релевантность побольше:

{
  className: 'string',
  begin: 'r"', end: '"',
  relevance: 10
},

Обратный пример. В большинстве языков строки задаются в кавычках (двойных или одинарных). Поэтому, если в фрагменте кода встречаются такие строки, то они, в общем-то, ничего не говорят о том, что это за язык. Поэтому такие конструкции стоит указывать с нулевой релевантностью, чтобы не портили статистику:

{
  className: 'string',
  begin: '"', end: '"',
  relevance: 0
},

Если релевантность не задана, она считается равной 1. Для явных значений рекомендуется использовать только 10 и 0, их пока хватает.

Помимо режимов релевантностью также обладают ключевые слова. Она указывается в виде значений атрибутов при описании объектов ключевых слов и практически всегда равна 1, потому что отдельное слово может встретиться в другом языке в качестве идентификатора:

{'for': 1, 'if': 1, 'while': 1}

Однако есть такие интересные слова, которые трудно придумать во второй раз. Например reinterpret_cast практически наверняка означает, что мы смотрим на C++.

Обратите внимание, что использовать нулевую релевантность для ключевых слов не нужно, потому что она попросту отменит распознавание этого слова вообще.

Недопустимые символы

Помимо релевантности для улучшения автоопределения языков существует еще одно средство — указание недопустимых символов. Например в языке Питон в первой строчке определения класса (class MyClass(object):) не может встретиться символ "{", а также перевод строки. Поэтому для этого режима пишется регулярное выражение, включающее эти символы:

{
  className: 'class',
  illegal: '[${]'
}

Если такие символы встречаются при попытке расцветить фрагмент кода по-питоновски, то расцветчик мгновенно отказывается от этой попытки. Символы эти, как нетрудно догадаться, подобраны не просто так, а в расчете на то, что например в Java такое сочетание встречается очень часто, а значит расцветчик не будет тратить время на расцветку Java-фрагментов Питоном и не будет их путать.

Библиотечные режими и регулярки

У многих языков правила местами совпадают. Чтобы экономить время и место, стоит посмотреть в код highlight.js на разделы "Common regexps" и "Common modes" и использовать их по возможности.

Оформление

Язык оформляется в виде отдельного .js файла, наверху которого делается специальный комментарий, описывающий язык. Описание должно быть в специально формате, чтобы язык находился build-системой.

Формат довольно простой -- пары Заголовок: значение, каждая на своей строке.

/*
Language: Superlanguage
Requires: java.js, sql.js
Author: John Smith <email@domain.com>
Contributors: Mike Johnson <...@...>, Matt Wilson <...@...>
Description: Some cool language definition
*/

Language -- единственный обязательный заголовок с человекочитаемым названием языка.

Requires -- перечисление через запятую файлов других языков, которые нужны для работы этому файлу. Бывает нужно для языков, которые очень сильно похожи на какой-то другой.

Остальные заголовки вполне очевидны.

Вместе с языком нужно сделать небольшой фрагмент кода, демонстрирующий возможности языка. Фрагмент лучше всего брать не из живого кода, а написать специальный синтетический, потому что в живом коде редко в одном месте встречается набор всех конструкций, которые описывает ваш язык. Синтетический фрагмент лучше всего делать маленьким, и совершенно необязательно работающим :-). В качестве примера можно посмотреть на фрагменты C++ и XML в файле test.html.

Дальше описание языка с тестовым фрагментом можно прислать мне на Maniac@SoftwareManiacs.Org или выложить где-нибудь в интернете и написать про него в форуме.