Принцип работы highlight.js
Код языка программирования — неоднородная штука. Например ключевые слова типа for, if и пр. не имеют смысла внутри строк. Зато внутри строк могут встречатсья escape-символы типа \", которые экранируют закрывающие кавычки. А внутри комментариев обычно вообще ничего не встречается, кроме конца этого самого комментария. Эти участки кода называются в highlight.js режимами (modes).
Каждый режим характеризуется:
- условием, по которому он начинается
- условием, по которому он заканчивается
- подрежимами, которые он может в себе содержать
- ключевыми словами, которые в нем имеют смысл
- а еще бывают режимы, которые представляют собой другой язык программирования (например javascript внутри HTML)
В итоге, вся работа 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), то есть описывать собственные ключевые слова и свои подрежимы. Но также у них есть отдельные атрибуты:
className (обязательный): название режима. Именно это название перечисляется в списке "contains", и оно же используется для имен классов при расцветке фрагмента кода. Что интересно, несколько режимов могут иметь одно и то же название. Это полезно, когда у языка существует несколько вариантов синтаксиса задания того, что этот режим означает (например строки могут заключаться как в двойные кавычки, так и в одинарные).
begin (необязательный): строчка регулярного выражения включения режима. Например одинарная кавычка для строк или '//' для комментария в Си-образных языках.
Этот параметр изредка можно не указывать, когда режим начинается по факту окончания предыдущего режима (см. "starts" дальше).
end (необязательный): строчка регулярного выражения окончания режима. Например та же кавычка для строк или '$' (конец строки) для однострочных комментариев.
Часто бывает, что начальное регулярное выражение не только означает начало какого-то режима, но и захватывает его полностью. Например режим "число" может начинаться реглярным выражением '\b\d+', которое охватывает все цифры подряд. Тогда, чтобы показать, что оно заканчивается тут же, как началось, в качестве конечного выражения ставится '^'.
Этот параметр изредка можно не указывать, когда режим заканчивается вместе с содержащим его режимом (см. "endsWIthParent" дальше). Хотя на практике такого, кажется, до сих пор не существует.
endsWithParent (необязательный, true или false): признак того, что режим может кончаться не только тем, что написано в свойстве "end", но и вместе с режимом, внутри которого он находится.
Проще всего продемонстрировать это на примере. В языке CSS все правила находятся между символами { и }, которые являются началом и концом режима "rules". Внутри него находятся пары свойств и значений, разделенные точкой с запятой:
p { width: 100%; color: red }Но у последнего правила точки с запятой может не быть, и тогда оно кончается вместе со всем блоком правил на символе "}". Вот для этого и служит
endsWithParent: true.excludeBegin, excludeEnd (необязательные, true или false): признаки, позволяющие исключить начало и конец режима из блока расцветки. Взяв тот же пример с языком CSS, значения в правилах там начинаются с ":", а заканчиваются ";", но чисто эстетически сами эти символы помечать тем же цветом, что и содержимое между ними, не принято. Для этого и нужны эти свойства. (А вот для строк расцвечивать начальную и конечную кавычки как-раз в порядке вещей отчего-то.)
Не указывание этих атрибутов эквивалентно "false".
returnBegin, returnEnd (необязательные, true или false): признаки возвращающие обратно в парсер лексему начала или конца режима. "returnBegin" ставится в тех случаях, когда начальная лексема подрежима -- это сложная конструкция, и ее надо заново распарсить по правилам этого начатого подрежима. Аналогично, "returnEnd" означает, что лексему конца текущего режима надо вернуть в парсер, чтобы ее заново разобрать по правилам родительского режима.
"returnEnd" используется например для Javascript, вложенного внутрь HTML. Javascript знает, что он заканчивается по лексеме "
</script>", но не знает про нее никаких подробностей. Поэтому он возвращает ее в свой родительский режим -- HTML'ный defaultMode -- который признает в ней тег и распарсит ее соответственно.starts (необязательный): название режима, который начинается просто по факту окончания текущего режима. Обратите внимание, новый режим будет не вложеным в текущий, а просто пойдет после него.
Этот параметр используется например для режимов вложенных в HTML внешних языков, например Javascript. HTML-тег
<script>является в HTML полноценным собственным режимом с ключевыми словами и подрежимами атрибутов. Как только он заканчивается на символе ">" тут же нужно начать режим вложенного Javascript'а, потому что никакой другой синтаксической конструкции для его начала уже не будет.subLanguage (необязательный): название языка, который должен использоваться для расцветки содержимого этого режима. При использовании "subLanguage" соответственно нет смысла определять lexems и keywords: этим будет заниматься вложенный язык.
Обратите внимание, что язык будет использован, только если пользователь библиотеки включил его в расцветку. Что логично, потому что иначе его он просто не подгружается.
Внимание! При задании регулярных выражений в "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 файла, наверху которого в комментарии указывать свое авторство в виде:
/*
Superlanguage definition (c) John Smith <email@domain.com>
*/
Вместе с языком нужно сделать небольшой фрагмент кода, демонстрирующий возможности языка. Фрагмент лучше всего брать не из живого кода, а написать специальный синтетический, потому что в живом коде редко в одном месте встречается набор всех конструкций, которые описывает ваш язык. Синтетический фрагмент лучше всего делать маленьким, и совершенно необязательно работающим :-). В качестве примера можно посмотреть на фрагменты C++ и XML в файле test.html.
Дальше описание языка с тестовым фрагментом можно прислать мне на Maniac@SoftwareManiacs.Org или выложить где-нибудь в интернете и написать про него в форуме.