NB This post is in Russian, English version is here
Вечер воспоминаний
Никогда в жизни мне не нравились визуальные средства работы с текстом (ну, кроме дельфей, которые меня кормили несколько лет в девяностые). Для диплома и прочих своих стихов я использовал LaTeX, но у меня всегда немного свербило от осознания того, что я заколачиваю микроскопом шурупы. Формул в моем дипломе, как вы наверное уже догадались, было немного, а в стихах — и того меньше.
Потом я узнал про SGML, а с приходом в наши дома веба — и про HTML; я выучил наизусть пару тегов и стал оформлять тексты для моих трех с половиной верных читателей с его помощью. Уже тогда, в 1996 году, меня неимоверно злило, что для некоторых типовых образцов оформления (три звездочки для пустого заглавия стиха, например) мне приходится буквально копипастить из записной книжки несколько строчек нечитаемой лапши. Так появился мой первый самописный движок для блога. Это был один XML файл со всеми текстами и трансформация XSLT, которая создавала из него пучок HTML страничек. К сожалению, код этого произведения искусства утрачен.
Такое решение меня устраивало буквально всем, потому что тег <verse caption="none">
или типа того буквально без моего участия единообразно преобразовывался в ту самую лапшу, которая меня раздражала в чистом HTML. Потом появился ЖЖ и я на свой блог на несколько лет подзабил, потом уже не помню, в общем когда мне снова захотелось что-то публиковать в личном пространстве — фактически везде пышным цветом цвел маркдаун. Что, в принципе, на первый взгляд прекрасно, потому что списки из звездочек и наклон из подчеркиваний — это ровно то, что нужно для оформления опрятного текста не под ГОСТ. И все же, проблема с тем самым заголовком стиха снова распрямила плечи. Мне категорически не хватает возможности дать ссылку на учетку в твиттере в виде @mudasobwa
и на запись на реддите в виде [rd /f/foo/1234]
, или типа того. В общем, я решил писать свой парсер.
Свой парсер
Свой парсер хорош тем, что можно сразу задать грамматику под себя (я часто использую верхний регистр, например, или как он там называется, и каждый раз печатать <sup>foo</sup>
вместо напрашивающегося и абсолютно естественного для маркдауна ^foo^
— как минимум странно). Проблема в том, что я ненавижу все, прибитое гвоздями. Я — апологет шурупов. Взял отверточку, отвинтил люстру, привинтил вместо нее крюк для виселицы. Я весь код так пишу, что часто приводит к закрытию задач из бэклога как бы задним числом. В общем, я решил, что грамматика должна быть настраиваемой.
А еще я сразу решил, что не собираюсь ставить перед собой цель полностью пройти существующие тесты. Люди, которым нужны таблицы внутри списка внутри таблицы — не моя клиентура (в результате, как ни странно, почти вся такая экзотика поддержалась сама собой).
Первым делом я попытался систематизировать подвиды стандартной разметки маркдауна. Вот что приходит на ум сразу же:
- параграфы (разделенные двумя и более переносами строк)
- код (я программист, мне важно)
- «скобки» (жирный, наклонный, все то, что оформляется как скобки)
- экранированные символы
- бесхитростная замена (знак «
<
» должен стать «<
» и типа того) - заголовки и т. п. (отсюда до конца строки)
- разделители (
<hr/>
сотоварищи) - списки
На первое время достаточно. Потом я добавил комментарии, теги, сноски и что-то там еще, полный список на сегодняшний день можно увидеть в документации.
Как парсить?
Мне всегда нравился протокол XMPP, потому что он позволяет работать с бесконечным потоком байтиков вместо файла. По той же причине я предпочитаю SAX-парсеры. Сожрали доступную часть потока, обработали, позвали сторонние зарегистрированные обработчики, сидим, ждем новых данных.
Надо сказать, я всегда, во всех без исключения проектах, предусматриваю возможность подключения обработчиков. Что-то случилось, о чем потенциально может захотеть узнать окружающий мир? — зарегистрируй колбек, я его вызову. Поэтому к моему парсеру можно подключить listener и оперативно узнавать о перипетиях парсинга.
Итак: свой настраиваемый синтаксис (в рамках соответствия оригинальному маркдауну), возможность подключения обработчиков, один проход. Последнее требование родилось просто потому, что в бесконечном потоке невозможно предусмотреть поддержку lookbehind, рано или поздно я буду вынужден отрезать этой змее хвост.
Ну, круто. Пора заняться собственно парсингом. Сразу оговорюсь, что эта задача не обязательно выполнима в языках без уверенной поддержки сопоставления с образцом. На чистом си ее можно решить имитируя его заглядыванием вперед на длину максимального «тега» (в текущей версии маркдауна это число — 3, для ---
и подобных, для настраиваемого синтаксиса его можно посчитать). Идрис позволит решить эту задачу красиво без явного сопоставления с образцом на зависимых типах (хаскель прососет, естественно). Но у меня есть эликсир, в котором сопоставление с образцом занимает самое почетное место. Поэтому я просто отсортирую открывающие теги из описания грамматики по длине и буду сопоставлять ввод с этими образцами, рекурсивно вызывая одну и ту же функцию на оставшемся потоке.
И все бы ничего, но еще есть иерархия тегов (экранирование сильнее скобок), зависимость от контекста (внутри куска кода — подчеркивание — это просто подчеркивание, а не начало наклонного текста) и терминирующие последовательности символов (начало нового параграфа должно закрыть все открытые теги). Таким образом, нам потребуется протаскивать некие знания об уже обработанном тексте дальше.
Настраиваемые грамматики
В принципе, можно было бы просто прочитать грамматику из конфига и нагородить всяких условных операторов, но это было бы чертовски медленно, да и сопоставление с образцом приглось бы имитировать. А хочется, чтобы парсер работал с выбранной грамматикой так, как будто бы он был написан именно для нее, без лишних проверок и тиков процессора. Поэтому на основе конфигурации выбранной грамматики я генерирую код обработки. Весь. Если кто-то понимает код на эликсире и не боится сурового метапрограммирования, вот процесс генерации. Для простейшего варианта (грамматика слака) сгенерированный код будет быстрее кода с поддержкой полного маркдауна примерно в 8–10 раз за счет гораздо меньшего количества функции сопоставления с образцом.
Custom handler
Помимо обязательных колбеков отовсюду, где это имеет смысл, я всегда предоставляю возможность написать свой обработчик в дополнение к существующим. Когда я закладывал такую возможность, я даже не представлял, зачем, но — пригодится. И она пригодилась. Что сделает стандартный маркдаун, встретив обособленную ссылку https://github.com/am-kantox/md — в лучшем случае подсветит ее. Существование кастомных обработчиков и возможность их подключить к любому «тегу» позволили мне за полчаса реализовать вытаскивание TwitterCard/OG по ссылке и показ предпросмотра, как это делает твиттер. Когда карточки нет, я все еще способен показать правильный заголовок и запихнуть description
в alt
. И такого плана расширения возможны практически бесплатно.
Вот, наверное, и все. Я получил некоторое количество отзывов на эту библиотеку, все негативные сводились к тому, что я нарушаю стандарт. Ну да, наверное, мне не нужно было вообще упоминать маркдаун; если вам нужен идеальный сферический парсер в вакууме, проходящий все тесты — возьмите что-нибудь другое, наверное. Но если вы хотите иметь полный контроль над парсингом, колбеки и собственные грамматики — возможно, md
подойдет и вам.