Вступление

Я обожаю Markdown. Это мощный, но вместе с тем лаконичный язык разметки. В его основе лежит концепция разделения данных и представления, что делает его очень удобным в ряде применений, например в системах контроля версий. Поэтому, например, Markdown является стандартом для документации на GitHub.

Markdown широко распространен в вебе как язык разметки для текстовых редакторов: на сайтах для ведения блогов, в вики проектах и т. д. Я сам ежедневно использую Markdown, и не только в разработке ПО, но и для ведения заметок. Я использую программу Obsidian: ide-подобный текстовый редактор Markdown для управления базой знаний.

Вообще говоря, Obsidian - одна из лучших программ для ведения заметок. Если вы еще не слышали о ней или о принципе zettelkasten, то, возможно, вам стоит заглянуть сюда и сюда.

Недавно я решил создать свой сайт, и мне понадобилось выбрать язык для разметки статей. Разумеется, я выбрал Markdown. Оставалось только определиться со всем остальным стеком.

Поискав готовые решения, я наткнулся на jekyll - генератор статических сайтов на основе Markdown. Он выглядел неплохим решением для минималистов, но, на мой взгляд, имел слишком много ограничений. В итоге я решил остаться на своем любимом фреймворке vue.js, а для конвертации Markdown в HTML использовать библиотеку. И вот тут началось самое интересное...

Выбор инструмента

Благодаря открытости, сравнительной простоте и популярности Markdown среди разработчиков, существует несколько десятков его реализаций на различных языках программирования. Далеко не полный список реализаций можно посмотреть здесь.

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

Конечно, для рендеринга статических страниц можно было бы использовать реализацию на любом языке, но я решил остановиться на pure-JavaScript решениях для большей гибкости.
Так у меня осталось 9 кандидатов:

  1. commonmark.js

  2. markdown-js

  3. markdown-it

  4. MarkdownDeep - GitHub и сайт

  5. Marked

  6. remark

  7. remarkable

  8. Showdown

  9. texts.js

Для сравнения парсеров я составил такой список параметров:

  1. лицензия

  2. инфраструктура

    1. документация

    2. наличие демо

    3. живое коммьюнити

  3. поддержка определенного подмножества синтаксиса Markdown

  4. возможность модифицировать логику работы парсера

  5. производительность

Лицензии

Итак, приступим! Начнем с лицензии.
Здесь все просто:

  1. Лицензия commonmark.js - 2-clause BSD, две зависимости, обе под MIT

  2. Лицензия markdown-js - MIT

  3. Лицензия markdown-it - MIT

  4. Лицензия MarkdownDeep - Apache 2.0

  5. Лицензия Marked - MIT, ссылается на Джона Грубера, создателя языка Markdown, распространяющего его под лицензией 3-clause BSD, что довольно мило

  6. Лицензия remark - MIT

  7. Лицензия remarkable - MIT

  8. Лицензия Showdown - MIT

  9. Лицензия texts.js - Apache 2.0

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

Инфраструктура

На документации останавливаться не будем: у всех проектов она имеется.

С демо дела чуть хуже:

  1. Демо commonmark.js

  2. Демо markdown-js - отсутствует

  3. Демо markdown-it

  4. Демо MarkdownDeep

  5. Демо Marked

  6. Демо remark - отсутствует

  7. Демо remarkable

  8. Демо Showdown

  9. Демо texts.js - отсутствует

Поддержку коммьюнити оценить сложно, не погрузившись в проект и не столкнувшись с трудностями. Косвенно проект можно оценить по числу звездочек на GitHub, но по этическим соображениям я не буду этого делать.

Что касается активности, то:

  1. проект markdown-js в данный момент не поддерживается, последний коммит в 2019 году

  2. texts.js - последний коммит в 2013 году

  3. remarkable - последний коммит в сентябре 2021 (в целом не так уж давно)

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

Синтаксис

Пожалуй, это самая важная часть. Для начала я составил список необходимой мне разметки:

Требования к разметке
  1. заголовки (h1 - h6)

  2. текстовые блоки

    • перевод строки

  3. цитаты (>)

    1. вложенные цитаты

  4. блоки кода (a = b)

    • эскейпинг спецсимволов

    • подсветка синтаксиса

  5. списки

    1. нумерованный (1.)

    2. маркированный (-)

    3. смешанный

  6. выделение текста

    1. курсив (*text*)

    2. жирный (**text**)

    3. жирный курсив (***text***)

    4. подчеркнутый (text)

    5. зачеркнутый (~~)

    6. выделение цветом (==)

    7. однострочный код (code)

    8. подстрочный регистр (a)

    9. надстрочный регистр (a)

  7. ссылки

    1. внешние (в интернет)

    2. внутренние (к заголовкам)

  8. медиа

    1. изображения

    2. эмодзи

  9. таблицы

  10. другое

    1. эскейпинг спецсимволов

    2. разделительная полоса (---)

  11. html

    • отрисовка html

    • сохранение html "как есть"

Для тестирования парсеров я составил текст с примерами всей необходимой разметки:

Тестовый текст в формате Markdown:
# 1. Headers

# h1
## h2
### h3
#### h4
##### h5
###### h6

# 2. Text blocks

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Text before line break
Text after line break

# 3. Quotes
> quote

>quote
> > nested quote
> - list in quote

# 4. Code blocks

```
untyped code block
```

```
escaped chars in code:
\```
```

```js
// js code
let a = 0
```

```python
# python code
print({"a":0})
```


# 5. lists

1. item-1
1. item-1
	1. item-1
	1. item-1
	- item
		- item

- item
	1. item-1
	2. item-2

# 6. Text decoration

*italic*

**bold**

***bold italic***

<u>underscored</u>

~~strikethrough~~

==highlighted==

`one line code`

A~subscript~

A^superscript^

# 7. Links

External link: [example.com](http://example.com)

Internal link: [link to h1](#h1)

# 8. Media

image: 

![Luke](https://habrastorage.org/webt/m_/it/vm/m_itvm5jqcvwj68gsk150c_caj0.jpeg)

emoji: ⛺  ????‚

# 9. Tables

| title | title2 |
| --- | ---- |
| data | data2 |
| more data | more data2 |
| even more data | even more data2 |

# 10. other
## 10.1 Escaped special symbols

\\
\`
\*
\_
\{ \}
\[ \]
\< \>
\( \)
\#
\+
\-
\.
\!
\|

## 10.2 Hline

---

---

---

# 11. html

<h2> H2 header </h2>


<p> # This markdown inside "p" tag should stay intact </p>

html image inside text block <img src="https://habrastorage.org/webt/m_/it/vm/m_itvm5jqcvwj68gsk150c_caj0.jpeg" style="width:200px; max-width:100%"> like that

**The first YouTube video "Me at the zoo". Embedded as an iframe**
<iframe style="width:560px; max-width:100%; height:315px" src="https://www.youtube.com/embed/jNQXAC9IVRw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

Под спойлером ниже показано, как примерно должен рендериться в HTML описанный выше Markdown. Редактор статей на Хабре - это WYSIWIG, а не на Markdown, так что мне не удалось вставить в превью вложенную цитату и выделение текста цветом, однако остальная верстка должна быть в порядке.

Тестовый текст после конвертации в HTML

1. Headers

h1

h2

h3

h4

h5

h6

2. Text blocks

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Text before line break
Text after line break

3. Quotes

quote

quote

nested quote

- list in quote

4. Code blocks

untyped code block
escaped chars in code:
```
// js code
let a = 0
# python code
print({"a":0})

5. lists

  1. item-1

  2. item-1

    1. item-1

    2. item-1

    • item

      • item

  • item

    1. item-1

    2. item-2

6. Text decoration

italic

bold

bold italic

underscored

strikethrough

highlighted

one line code

Asubscript

Asuperscript

7. Links

External link: example.com

Internal link: link to h1

8. Media

image:

emoji: ⛺ ????

9. Tables

title

title2

data

data2

more data

more data2

even more data

even more data2

10. other

10.1 Escaped special symbols

\
`
*
_
{ }
[ ]
< >
( )
#
+
-
.
!
|

10.2 Hline




11. html

H2 header

# This Markdown inside "p" tag should stay intact

html image inside text block like that

The first YouTube video "Me at the zoo". Embedded as an iframe

Разбираться с установкой всех парсеров мне не хотелось, поэтому я тестировал только те, у которых было демо. При желании вы можете протестировать остальные самостоятельно, используя тестовый текст выше (или любой другой).

Итак, перейдем к результатам.

commonmark.js

Что не работает:

  1. перевод строки в текстовых блоках

  2. подсветка синтаксиса

  3. выделение текста

    1. зачеркнутый текст (приходится использовать <del>)

    2. выделение цветом (приходится использовать <mark>)

    3. подстрочный регистр

    4. надстрочный регистр

  4. таблицы

Немного неудобно, что в демо не работают переходы по ссылкам и не отрисовывается видео с YouTube, но сырой код HTML вроде верный

markdown-it

Все работает!

Можно включить по желанию:

  1. перевод строки в текстовых блоках

  2. парсинг HTML

MarkdownDeep

Пожалуй, это самый косячный парсер Markdown из проверенных.

Что не работает:

  1. перевод строки в текстовых блоках

  2. вложенная цитата интерферирует со списком

  3. блоки кода

    1. перевод строки в коде

    2. подсветка синтаксиса

    3. экранирование спец. символов

    4. код почему-то дублируется: один раз как код и еще раз как текст

  4. выделение текста

    1. зачеркнутый текст

    2. выделение цветом

    3. подстрочный регистр

    4. надстрочный регистр

  5. iframe не работает

Баги с цитатами и блоками кода наглядно:

Что не так:

  1. Текст - list in quote должен быть на следующей строке.

  2. Весь текст из кодового блока идет в одну линию

  3. значки ``` вылезли из кодового блока

  4. текст из последнего блока повторяется - но уже как Markdown

Marked

Что не работает:

  1. выделение текста

    1. зачеркнутый текст

    2. выделение цветом

    3. подстрочный регистр

    4. надстрочный регистр

  2. таблицы

Можно включить по желанию:

  1. перевод строки в текстовых блоках

  2. В демо синтаксис не подсвечивается. Однако, в конфиге есть поля "highlight": null и "langPrefix": "language-", указывающие на то, что как-то можно подключить подсветку синтаксиса. Правда, как это сделать, я не разбирался.

Не отрисовывается iframe с видео с YouTube, но сырой код HTML вроде верный.

remarkable

Все работает!

Можно включить по желанию:

  1. перевод строки в текстовых блоках

  2. парсинг HTML

Проект очень сильно напоминает markdown-it, и неспроста (см. далее).

Showdown

Что не работает

  1. Заголовки h5 и h6

  2. перевод строки в текстовых блоках

  3. подсветка синтаксиса

  4. выделение текста

    1. выделение цветом

    2. подстрочный регистр

    3. надстрочный регистр

  5. iframe не работает

С заголовками творится что-то странное: # транслируется в <h3>## в <h4> и т. д., а на заголовки 5 и 6 уровней тегов в HTML не остается, и они вставляются как простой текст. Это мешает их нормальной стилизации через CSS, а также приводит к багу с переносом на следующую строчку:

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

Судя по названию одной из галочек (simpleLineBreaks) перевод строки должен работать, но у меня заставить его работать не получилось.

Bonus: Obsidian

В конце концов мне захотелось проверить и свой заметочник Obsidian, так как именно в нем я буду набирать статьи, которые затем пойдут на сайт. (Сами понимаете, где я набирал эту статью). К моей радости, он без труда справился со всем за исключением subscript-а и superscript-а. Но это простительно.

Bonus 2: PyCharm

Так как код сайта я пишу в PyCharm Community Edition, а у него есть встроенный просмотрщик Markdown, то... ну вы поняли!

Что не работает:

  1. перевод строки в текстовых блоках

  2. выделение текста

    1. выделение цветом

    2. подстрочный регистр

    3. надстрочный регистр

  3. внутренние ссылки не работают внутри ide

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

  5. iframe не работает

Работает выборочно:

  1. подсветка синтаксиса доступна только для Python. Возможно, все дело в Community Edition, а в Enterprise Edition поддерживаются и другие языки, но я не проверял.

Ремарка

На самом деле в ряде случаев отсутствие поддержки части синтаксиса (к примеру, выделения текста и таблиц) - это не баг, а фича, так как часть парсеров Markdown придерживается спецификации CommonMark. Другие парсеры, такие как remarkable, позволяют включать опцию "CommonMark" по желанию.

Спецификация CommonMark нацелена на унификацию языка Markdown. Это может быть полезно, например, при необходимости переноса текста в Markdown между различными системами. Однако, мне для сайта требовался расширенный функционал, так что эти парсеры мне не подошли.

Также в ряде парсеров теги HTML, признанные небезопасными (как <iframe>), не рендерятся намеренно. Это называется "санитайзинг HTML". Он полезен, например, если парсер Markdown используется для рендеринга пользовательского контента. Но, так как на моем сайте все статьи буду писать я, эта функция мне будет только мешать.

Возможность модифицировать логику работы парсера

В основном парсеры работают по следущему алгоритму:

Markdown -> парсинг -> внутреннее представление -> рендеринг -> HTML

Часть парсеров позволяет изменять логику своей работы. Парсер может давать доступ к функциям парсинга и рендеринга, либо позволять модифицировать внутреннее представление. Это дает возможность добавлять дополнительный функционал, либо модифицировать существующий. Такая расширяемость парсера открывает путь для создания плагинов сообществом.

Я не смог найти упоминаний о расширяемости в документации следующих парсеров:

  1. commonmark.js

  2. MarkdownDeep

Остальные рассмотрены ниже:

markdown-js

markdown-js позволяет получить доступ к внутренним представлениям. Логика работы парсера такая:

Markdown -> парсинг -> дерево Markdown -> конвертация -> дерево HTML -> рендеринг -> HTML

Промежуточные представления хранятся в виде деревьев в формате JsonML и к ним можно получить доступ, вызывая функции парсинга, конвертации и рендеринга по очереди.

markdown-it

Пайплайн markdown-it состоит из парсера и рендерера.

Логика работы парсера описывается правилами, разбитыми на 3 группы: coreblock и inline, что бы это ни значило. К существующим правилам можно дописывать свои.

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

Список токенов также можно модифицировать самостоятельно.

Список токенов передается в рендерер, который также можно расширять, добавляя свои правила.

Список доступных плагинов можно посмотреть здесь.

Marked

Логика работы Marked выглядит во многом похоже на остальные парсеры:

Markdown -> парсер -> синтаксическое дерево -> рендерер -> HTML

Правда, документация весьма вольно использует термины.

Парсер, который называется lexer, управляет набором правил, которые называются tokenizers. Можно как добавлять свои токенайзеры, так и модифицировать встроенные при помощи способа, напоминающего наследование, от встроенного объекта, содержащего функции-токенайзеры. При этом если функция в классе-наследнике вернет false, то будет выполнена функция из класса-родителя.

Можно определить функцию walkTokens, которая получает на вход синтаксическое дерево и его же должна отдать на выходе. Внутри можно провести любые модификации дерева.

Дерево отдается рендереру, который здесь parser, и он вызывает набор правил renderers. Как и в случае с парсером, можно и добавить свои функции, и отнаследоваться от существующих.

remark

Проект remark разработан с горячей любовью к декомпозиции. Remark использует парсер mdast-util-from-markdown, основанный на micromark, синтаксическое дерево mdast, являющееся реализацией unist для Markdown, рендерер mdast-util-to-markdown, а также обертку unified, чтобы склеить все это воедино. Фуууф!

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

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

remarkable

Так как remarkable имеет общие корни с markdown-it (см. далее), то и логика работы у них схожая. Я не вдавался в подробности, так что за тонкостями реализации обращайтесь сюда.

Список плагинов можно посмотреть здесь.

Showdown

Похоже на то, что плагины Showdown представляют собой набор регулярных выражений и функций, последовательно модифицирующих весь текст.

Логику работы можно описать так:

Markdown -> regex/function 1 -> modified text -> regex/function 2 -> ... -> regex/function n -> HTML

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

texts.js

Насколько я понял из документации, существует возможность получить доступ к внутреннему представлению texts.js, которое является кастомной реализацией JsonML под названием TextJSON.

Вывод

Интересно, что, хотя все рассмотренные парсеры имеют общие принципы работы, они сильно различаются в деталях реализаций.

Мне нравится логика реализации markdown-js - она не слишком замороченная и удобная с точки зрения написания плагинов. К сожалению, markdown-js на данный момент не поддерживается, и поэтому я не буду его использовать.

Логика реализаций markdown-it, remarkable и Marked неплоха, но документация смущает своей терминологией.

Remark выглядит как самый хорошо документированный проект, но вместе с тем степень его декомпозиции кажется излишней.

Плагины в Showdown устроены очень просто, но это достигается путем значительного снижения производительности.

Про texts.js вообще трудно что-либо сказать по причине неполной документации.

Итого, с точки зрения плагинов можно смело брать:

  1. markdown-it

  2. Marked

  3. remark

  4. remarkable

Производительность

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

Поиск бенчмарков

Я нашел 4 бенчмарка:

  1. сommonmark.js benchmark - 2015

    • commonmark.js

    • markdown-it

    • Marked

    • Showdown

  2. markdown-it benchmark - 2015

    • markdown-it

    • Marked

    • commonmark

  3. remarkable benchmark - 2014

    • remarkable

    • Marked

    • commonmark

  4. markdown-benchmark - 2015

    • markdown-js

    • Marked

    • showdown

Я не нашел бенчмарков для:

  1. MarkdownDeep

  2. texts.js

  3. remark

Все бенчмарки сделаны примерно в одно время, так что будем считать их примерно сопоставимыми.

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

Итого, имея эти бенчмарки, мы можем построить такой граф зависимостей:

Цвета стрелок здесь совпадают с цветом автора бенчмарка.

Сравнение

Производительность измеряется в операциях в секунду, то есть чем больше, тем лучше.

Я посчитал относительную производительность по каждому из бенчмарков в отдельности:

  1. commonmark

    1. showdown = 1

    2. commonmark.js ~ Marked ~ markdown-it = 3

  2. markdown-it

    1. commonmark.js = 1

    2. markdown-it = 0.6 (1.28 в режиме CommonMark)

    3. Marked = 1.3 (версия 0.3.5)

  3. remarkable

    1. commonmark.js = 1

    2. remarkable = 1.88 (2.34 в режиме CommonMark)

    3. Marked = 0.573 (тут старая и медленная версия - 0.3.2)

  4. markdown

    1. Showdown = 1

    2. markdown-js = 0.61

    3. Marked = 2.99

Анализ бенчмарков:

  1. Видно, что в среднем commonmark.js, Marked и markdown-it быстрее, чем Showdown, в 3 раза.

  2. Данные бенчмарка 2 примерно подтверждают данные бенчмарка 1

  3. По бенчмарку 3 remarkable быстрее commonmark.js в 2 раза, то есть быстрее Showdown в 6 раз. Это впечатляющий показатель, но так как он произведен разработчиком remarkable, ему нельзя слишком сильно доверять. Учитывая, что у remarkable и markdown-it одинаковые корни, можно предположить, что и производительность у них примерно одинаковая.

  4. По бенчмарку 4 markdown-js медленнее Showdown на 40%

Теперь надо привести все это к общему знаменателю. Самым надежным выглядит бенчмарк 1, так что в качестве единицы возьму производительность Showdown. Итого получаем такую сравнительную таблицу:

Парсер

Производительность

Источники оценки

commonmark.js

~3

1

markdown-js

~0.6

4

markdown-it

~3

1

Marked

~3

1, 4

remarkable

~3 / ~6

моя догадка / 3

Showdown

1

-

Вывод

Результаты сравнения показывают, что по производительности markdown-js и Showdown катастрофически проигрывают остальным парсерам, в то время как остальные держатся примерно на одном уровне.

Если верить бенчмарку от разработчика remarkable, то он быстрее всех с большим отрывом. Правда, я сомневаюсь в его достоверности.

Было бы интересно посмотреть на производительность парсера remark. Возможно, в другой раз...

Подводя итог, если вам важна производительность, вы можете смело выбирать:

  1. commonmark.js

  2. markdown-it

  3. Marked

  4. remarkable

Окончательный выбор

По результатам сравнения победили два парсера: markdown-it и remarkable. У этих проектов много общего, в том числе общие разработчки.

Если посмотреть в историю версий проектов, то можно узнать много интересного. Так, первым появился проект remarkable. Через несколько месяцев возник markdown-it - скорее всего, как форк remarkable. С тех пор проекты развиваются параллельно.

Оба проекта:

  1. имеют лицензию MIT

  2. предоставляют рабочее демо

  3. безупречно прошли тест на синтаксис

  4. дают широкие возможности к модификации своей логики работы

  5. имеют много готовых плагинов

  6. находятся в лидерах по производительности

Я для себя выбрал remarkable, потому что у него в демо был пример кода и я смог быстро интегрировать его в свой проект.

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

Как я настроил парсер

Итак, я выбрал remarkable, и мне предстояло его настроить.

Что есть из коробки

Я был приятно удивлен, что из коробки он поддерживает много полезных вещей. В том числе и тех, о которых я не знал:

  1. Примечания: текст[1]

  2. Аббревиатуры: SQL

Но что оказалось действительно полезным, так это поддержка скрываемых блоков (спойлеров):

Нажмите, чтобы увидеть спойлер

Это спойлер!

Плагины

В списке плагинов есть много интересных.

Себе я установил remarkable-katex, основанный на библиотеке KaTeX для отрисовки формул LaTeX в вебе.

С ним можно делать такие вещи: \frac{1}{2}

И такие:

\frac{1}{2}

Если вы знаете японский, вам может пригодиться плагин remarkable-furigana, позволяющий отрисовывать над иероглифами их произношение.

Остальные плагины оставлю вам для самостоятельного изучения.

include

На сайте я храню исходники статей как файлы с текстом Markdown. Для удобства мне понадобилась возможность подключать содержимое одних файлов в другие.

Решать эту задачу средствами remarkable было бы неправильно, поэтому я написал функцию препроцессора, которая получает на вход путь к корневому файлу и рекурсивно вставляет в него необходимые подфайлы.

Например, для такой структуры файлов

  • posts/

    • main.md

    • parts/

      • part.md

      • part2.md

Это будет выглядеть примерно так:

// main.md
// absolute path
@include '/posts/parts/part.md'
// or relative path
@include './parts/part2.md'
!!!
// part.md
Hello
// part2.md
world
// output
Hello
world
!!!
Для тех, кто заинтересовался, вот код препроцессора:
async load_content_by_url(url) {
  let response = await fetch(url)
  let text = await response.text()
  return text
},

// str.replace() can't handle asynchronous requests, so we need a wrapper
// source: https://stackoverflow.com/questions/33631041/javascript-async-await-in-replace
async replaceAsync(str, regex, asyncFn) {
  const promises = [];
  str.replace(regex, (match, ...args) => {
    const promise = asyncFn(match, ...args);
    promises.push(promise);
  });
  const data = await Promise.all(promises);
  return str.replace(regex, () => data.shift());
},

async load_content_with_includes(url) {
  let file_dir = url.substring(0, url.lastIndexOf("/"))

  let text = await load_content_by_url(url)

  let out_text = await replaceAsync(
    text,
    /^@include\s*"(.+)"\s*$/mg, // regex for file includes
    async (...match) => {
      let url = match[1]
      url = url.replace(/^\./, file_dir) // if relative path -> make absolute

      let included_text = await load_content_with_includes(url) // get data by url
      return included_text
    }
  )
  return out_text
},

Стили

Для того, чтобы кастомизировать внешний вид статей, я создал свои стили для всех HTML компонентов, получаемых при генерации из Markdown.

Вот моя таблица стилей для тех, кому интересно:
<style lang="scss">
$site-defaults-color: #c8c3bc;

// 3.
$quote-border-color: #666;
// 4.
$code-border-color: #666;
$code-bg-color: rgba(255, 255, 255, 0.05);
// 6.
$highlight-color: $site-defaults-color;

$inline-code-color: rgb(3, 218, 197); // = #03dac5
$inline-code-bg-color: rgba(3, 218, 197, 0.1);
// 9.
$table-border-color: #666;
$table-stripe-color: rgba(255, 255, 255, 0.07);
// 10
$hline-color: $site-defaults-color;
//
$details-border-color: #666;

.md-wrapper {
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1. headers~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  @for $i from 1 through 6 {
    $sel: "h" + $i;
    #{$sel} {
      // nothing here
    }
  }
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~2. text blocks~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~3. quotes~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  blockquote {
    margin: 15px 0;
    padding: 0 20px;

    border: 1px solid $quote-border-color;
    border-left: 5px solid $quote-border-color;
  }
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~4. code blocks~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  code {
    font-family: Raleway;
  }
  pre {
    padding: 10px;
    margin-bottom: 10px;
    display: block;

    border: 1px solid $code-border-color;
    border-radius: 4px;
    background-color: $code-bg-color;

    overflow-x: auto;

    code {
      white-space: pre;
      word-break: normal;
      word-spacing: normal;
      word-wrap: normal;
    }
  }
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~5. lists~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  ul {
    list-style-type: circle;
  }
  ol,
  ul {
    padding-inline-start: 25px;
  }
  li {
    padding: 3px 0;
  }
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~6. text-decoration~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  mark {
    padding: 2px;
    background-color: $highlight-color;
  }
  code:not([class]) {
    padding: 2px 4px;
    font-size: 90%;
    color: $inline-code-color;
    background-color: $inline-code-bg-color;
  }

  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~7. links~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  @mixin link {
    color: #fff;
    font-weight: bold;
    text-decoration: none;
    cursor: pointer;
  }
  a {
    @include link;
  }
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~8. images~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  img {
    max-width: 100%;
  }
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~9. tables~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  table {
    width: 100%;
    max-width: 100%;
    margin: 15px 0;

    border-collapse: collapse;
    border-spacing: 0;

    text-align: left;

    display: block;
    overflow-x: auto;

    th,
    td {
      padding: 10px;
      border: 1px solid $table-border-color;
    }
    thead tr th {
      border-bottom: 2px solid $table-border-color;
    }

    tbody tr:nth-child(odd) {
      td,
      th {
        background-color: $table-stripe-color;
      }
    }
  }
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~10.2 hline~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  hr {
    border: 0;
    height: 1px;
    width: 100%;

    background-image: linear-gradient(
      to right,
      rgba(0, 0, 0, 0),
      $hline-color,
      rgba(0, 0, 0, 0)
    );
  }

  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~spoilers~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  @mixin user_select_none {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }

  details {
    background-color: rgba(255, 255, 255, 0.02);
    padding: 10px;
    border: dotted 1px $details-border-color;

    summary {
      @include link;
      @include user_select_none;
    }
  }
}
</style>

Для подсветки синтаксиса я использовал highlight.js. Пример подключения можно посмотреть на странице демо remarkable.

Заключение

Настала пора подводить итоги. Парсер для моего сайта выбран и настроен, чем я очень доволен.

Работая над статьей, я узнал много нового про Markdown и открыл его для себя с новой стороны.

Буду рад, если вам понравилось читать это небольшое исследование. До новых встреч, всем добра!

Комментарии (25)


  1. rubero
    19.06.2022 18:09
    +1

    Судя по npm, remarkable пару лет не обновлялся и в 5 раз менее популярен, чем markdown-it


  1. InstaHeat
    19.06.2022 18:33
    +2

    а почему не Pandoc?


    1. k12th
      19.06.2022 21:26
      +1

      решил остановиться на pure-JavaScript решениях для большей гибкости


    1. nikitalogos Автор
      20.06.2022 02:20
      +1

      Не знал о таком (точнее слышал, но никогда не пользовался). Сейчас скачал себе на десктоп pandoc и прогнал через него тестовый файл вот такой командой

      pandoc -f markdown -t html text_for_testing.md -o t.html

      Я не знаю, насколько можно кастомизировать поведение pandoc-а, но из коробки он выдал такие ошибки:

      1. перевод строки в текстовых блоках

      2. вложенная цитата

      3. лист в цитате

      4. выделение цветом (==)

      В целом неплохо (даже подсветку синтаксиса осилил - выделил переменные и т. п. css классами), но все же не идеальный результат


  1. petrov_engineer
    19.06.2022 19:51
    +1

    Есть ещё нативные варианты, например, https://github.com/rsms/markdown-wasm

    Если речь идёт о скорости, то почему их не рассматривать...


    1. nikitalogos Автор
      20.06.2022 02:46
      +1

      Спасибо, не знал о таком! Попробовал.

      Вот его live demo - https://rsms.me/markdown-wasm/

      Тестировал на том же тестовом файле, что и все остальные парсеры

      Что не работает:

      1. перевод строки в текстовых блоках

      2. выделение текста

        1. нижнее подчеркивание (тег <u>) - кажется, он просто весь html игнорирует

        2. выделение цветом (==)

        3. подстрочный шрифт (распознается как зачеркивание)

        4. надстрочный шрифт

      3. html игнорируется, так что ни картинка, вставленная тегом <img>, ни <iframe> не рендерятся

      Cудя по readme проекта, html можно включить в настройках. Но почему настройки нельзя покрутить в live demo - это хороший вопрос...


  1. makar_crypt
    19.06.2022 22:15

    для меня самое главное

    1) через cntrl +v подхватывалась картинка

    2) скопированный стиль из сайта при вставки сохранял стилистику

    ни один из вашего топ 3 это не умеет делать - печаль


    1. nikitalogos Автор
      20.06.2022 02:31
      +7

      Мне кажется, то, о чем вы говорите - задача редактора Markdown, а не парсера.
      Редактор нужен для того, чтобы статью написать в Markdown, а парсер - для того чтобы отобразить Markdown в HTML страничку.


  1. olku
    19.06.2022 23:36

    Asciidoc не пробовали? Из коробки и в html, и в pdf.


    1. nikitalogos Автор
      20.06.2022 02:36
      +1

      Спасибо за предложение! Попробовал)

      Нашел вот такое демо https://asciidoclive.com/edit/scratch/1

      Вставил тестовый текст (есть в публикации выше)

      Результат не очень впечатляющий. Помимо стандартных проблем с переводом строки в текстовых блоках и вложенных цитат, есть ряд совсем "детских" проблем. На мой взгляд, этоn проект справляется даже хуже, чем антигерой данной статьи - MarkdownDeep...


      1. olku
        20.06.2022 11:46

        https://github.com/jichu4n/asciidoclive не обновлялся с 2016. Референсный процессор живет на https://github.com/asciidoctor/asciidoctor Есть плагин для IDE от авторов https://plugins.jetbrains.com/plugin/7391-asciidoc
        Не, я не спорю - больше редакторов хороших и разных. Давний поклонник markdown, но влез в asciidoc и прилип. Для паблишинга показался гибче MD и значительно проще TEX. Проблем пока не поймал, пользуюсь плагином. Бросьте в личку кейс, пожалуйста, мейнтейнеры отвечали в течение нескольких часов.


        1. nikitalogos Автор
          20.06.2022 13:01
          +1

          Обновил PyCharm, установил последнюю версию AsciiDoc. Запустил превьюшку. На первый взгляд ошибки те же, что и в https://asciidoclive.com/edit/scratch/1 :(


    1. nin-jin
      20.06.2022 18:02

    1. blinky-z
      21.06.2022 02:04

      Asciidoc, к сожалению, не так лаконичен, прост и читабелен как Markdown. Например, об этом сказано даже в спеке CommonMark


  1. tdemin
    20.06.2022 05:43
    -3

    del, не дочитал пост до конца


  1. Pinkerton42
    20.06.2022 06:06
    +2

    Есть нативное готовое решение: https://content.nuxtjs.org/

    Если коротко - это смесь из статически генерируемых страниц из {md, json, yml, etc} файлов в html и все это приправлено vue и nuxtjs. В последней версии даже появились markdown-компоненты


    1. nikitalogos Автор
      20.06.2022 12:36

      Сам недавно заинтересовался про nuxtjs, планирую как-нибудь мигрироваться на него, когда руки дойдут... Классно, что там есть такая библиотека!

      Я нашел ссылку на демку content-а на их странице на github https://stackblitz.com/github/nuxt/content/tree/main/examples/essentials/hello-world?file=app.vue

      Попробовал прогнать через нее тестовый Markdown

      Что не работает:

      1. перевод строки в текстовых блоках

      2. выделение текста

        1. выделение цветом

        2. подстрочный регистр

        3. надстрочный регистр

      В целом проблемы не критичные и с ними можно жить :)


      1. Pinkerton42
        20.06.2022 12:48
        +1

        В md работает (в других парсерах не проверял)

        1. Двойной пробел и перевод строки в конце дает перенос.

        2.2 и 2.3 <sub></sub> и <sup></sup> даже с индексами индексов (вложенность)


        1. nikitalogos Автор
          20.06.2022 13:56
          +1

          Да, понятно, что выделение текста можно победить через <sup>, <sub> и <mark>, но это уже raw html, и это накладывает небольшие неудобства.


          Двойной пробел и правда работает! Причем во всех парсерах... Наверное, надо немного поправить статью


  1. karambaso
    20.06.2022 09:54
    -5

    но я решил остановиться на pure-JavaScript решениях для большей гибкости

    Не надо вуалировать истину, которая состоит в том, что выбор был сделан с целью исключитть затраты на изучение других языков, ну а JS видимо уже был изучен, значит в его случае затраты минимальны.

    Но в тексте я не увидел подробностей, которые можно было бы указать, если бы автор реально изучал исходный код парсеров. Только ссылки на документацию и её неполноту. При таком подходет выбор парсеров на JS ничем не оправдан кроме личного пристрастия автора.

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

    Ну и немного о цели автора. Он заявляет, что хочет сделать сайт. Ну ладно, но почему на маркдауне? Просто потому, что ничего другого не знаем? HTML, вообще-то, очень простая штука, сильно проще JS. Видимо опять рулят привычки и нежелание изучать новое. В итоге - субъектив на субъективе. Плюс личные впечатления и самореклама.


    1. Klems
      21.06.2022 13:43

      Ну а почему нет? Автор выбирал парсер для себя. Почему он не должен руководствоваться привычками? Что плохого в минимизации затрат и усилий? И статья называется не "какой парсер лучше выбрать", чтобы претендовать на объективность.

      Если я знаю Java и захочу сделать сайт, то в первую очередь буду искать движки на Java (кстати, я так делал и нашел SparkJava)

      Такой подход позволяет первоначально сильно сузить широту выбора. Иначе чем больше выбор - тем дольше выбирать.


  1. EXL
    20.06.2022 13:20

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


    1. nikitalogos Автор
      20.06.2022 13:58
      +3

      Тестовый текст приведен в статье под спойлером "Тестовый текст в формате Markdown"


  1. nin-jin
    20.06.2022 19:18
    +2

    Не хотите добавить в сравнение $mol_text?

    Лицензия MIT, демо, дока, бенчмарк, отзывчивый всё ещё живой мейнтейнер.

    Что не работает:

    • Перевод строки разрывает абзацы.

    • Нумерованные списки. Не сложно добавить в принципе.

    • Экранирование в блоках кода - просто используется отступ.

    • Подчёркивание, раскрашивание, суперскрипт, субскрипт. Не сложно добавить в принципе.

    • Экранирование - используется инлайн код.

    • Линия разреза. Не сложно добавить в принципе.

    • Сырой HTML. Видео и приложения вставляются так же как и картинки.

    Приятные бонусы:

    • Фавиконки у ссылок.

    • Проверка ссылок на безопасность.

    • Виртуальный или ленивый рендеринг.

    • Быстрое копирование кода.

    • Номера строк в коде.

    • Подсветка найденного.

    Логика работы простая: одним парсером разбивается на блоки, потом попадающие в видимую область блоки парсятся уже вторым парсером и рендерятся. Можно настроить любой аспект, конечно, но лучше просто прислать пул реквест.


  1. SamoedR
    22.06.2022 09:51

    А вы на hugo не смотрели? Тоже можно из md делать сайты