- BrainTools - https://www.braintools.ru -
Я собирался написать статью про Markdown. В процессе внутри документа оказался Doom.
До игры ещё доберёмся. Но сначала — почему два пробела меняют поведение [1] абзаца, обратные кавычки не всегда спасают таблицу, а одинаковый .md на Хабре и GitHub может оказаться двумя разными документами.
Для тридцатой статьи я выбрал Markdown. После расширения, которое сохраняет публикации Хабра в .md, хочется разобраться и с самим форматом: что он умеет, где удивляет и почему один файл в разных редакторах выглядит по-разному.
Ниже — подробный разбор, примеры для собственных документов, пять заданий с проверкой и Doom, упакованный в Markdown. Встроенная лаборатория позволяет править Markdown прямо в статье и смотреть получившийся HTML. Примеры из текста можно копировать в её поле ввода. Пять заданий связаны небольшим сюжетом: нужно восстановить архив, найти ключ и собрать последнюю страницу.
Текст можно читать подряд, а можно использовать как справочник. Если нужен только эксперимент с игрой, переходите к Doom [2]. Если хочется руками проверить странное поведение [3] разметки, начните с лаборатории ниже.
Игровые блоки разбирают Markdown с расширениями GFM. Сам редактор, проверка решений и сохранение прогресса написаны на JavaScript. По дороге будем отделять возможности разметки от возможностей программы, которая её показывает.
Если основы уже знакомы, можно сразу перейти к изображениям и GIF [4], обратным кавычкам [5], таблицам [6], интерактивности [7], Doom [2] или финальной проверке [8].
Файл с расширением .md не сообщает, какие расширения нужны для его отображения. В одном редакторе таблица превратится в таблицу, в другом останется текстом с вертикальными чертами. Оба результата возможны.
Для этой статьи достаточно четырёх ориентиров:
|
Обозначение |
Что имеется в виду |
|---|---|
|
CommonMark |
Формализованное базовое поведение Markdown |
|
GFM |
GitHub Flavored Markdown: база и расширения вроде таблиц и списков задач |
|
HFM |
Разметка Хабра с собственными элементами и ограничениями |
|
Возможность редактора |
То, что добавляет конкретная программа: диаграммы, подсказки, выполнение кода |
CommonMark подробно описывает разбор текста, а GFM задаёт дополнительные конструкции. Это удобно использовать как точку отсчёта, когда два просмотрщика спорят о результате. Спецификация CommonMark [9], спецификация GFM [10].
Словами «все возможности Markdown» легко пообещать лишнего: у расширений нет единого конечного списка. Здесь собрана основа для статей, README и заметок, а также несколько полезных расширений. У примеров, которые требуют особого окружения, оно будет указано.
Здесь находится встроенный редактор. Откройте вкладку Result во вставке. Сверху у самой лаборатории можно переключиться на одно из пяти заданий. У него три представления одного документа: исходник, результат и HTML предпросмотра после разбора и ограничения допустимых элементов. Последнее особенно полезно: крупный текст может быть заголовком, а может быть просто абзацем, которому достался такой стиль. По внешнему виду это не всегда очевидно.
Если вставка не загрузилась, лаборатория доступна отдельно [11].
Начните с заготовки «Таблица». С включённым GFM она становится таблицей. Отключите переключатель: тот же текст останется абзацем. Вы не испортили файл — вы поменяли правила, по которым его читают.
У лаборатории есть ещё режим принудительных переносов. Выберите «Переносы» и сравните результат с ним и без него. В нашей лаборатории этот переключатель работает при включённом GFM. Это настройка просмотрщика, а не новый синтаксис Markdown. Параметры Marked [12]. Именно такие незаметные настройки часто объясняют спор «у меня всё нормально отображается».
В лаборатории используется Marked. Переключатель GFM сравнивает режимы этой библиотеки, а не запускает настоящий обработчик GitHub или Хабра. Мы не выдаём похожую картинку за проверку совместимости с площадкой. Исходный HTML отключён, внешние картинки не загружаются, ссылки в предпросмотре не уводят со страницы. Это редактор учебных примеров, а не универсальный браузер.
Если читаете обычный Markdown-файл, исходники примеров доступны и без лаборатории. Их можно скопировать в свой редактор. Для итоговой проверки статьи всё равно понадобится предпросмотр Хабра.
Удобно разделить путь документа на три шага.
Обработчик находит блоки: заголовки, абзацы, списки, код, цитаты.
Внутри подходящих блоков разбирает выделение, ссылки и строчный код.
Программа показывает результат, применяет стили и добавляет свои возможности.
Возьмём короткий пример:
## Настройка
Укажите **порт** в файле `server.conf`.
Здесь получатся заголовок, абзац, выделенное слово и строчный код. А вот выбор шрифта, ширина колонки, подсветка, кнопка копирования и автоматическое оглавление относятся уже к программе вокруг разметки.
Из этого следует полезный порядок отладки. Если вместо заголовка получился абзац, проверяем исходник и правила разбора. Если заголовок появился, но слишком мелкий, смотрим оформление. Если не работает интерактивная кнопка, сначала выясняем, кто вообще должен был её добавить.
Начнём с этого:
Первая строка.
Вторая строка.
Следующий абзац.
Пустая строка отделяет абзацы. Одиночный перевод строки внутри первого абзаца обычно не означает обязательного визуального переноса: отображение мягкого переноса зависит от обработчика.
Если нужен именно разрыв строки, классический вариант — два пробела в конце предыдущей строки. Ниже точки обозначают пробелы; в настоящем файле их нужно заменить пробелами:
Первая строка.··
Вторая строка.
Технически работает, но невидимый синтаксис неудобно обсуждать в ревью. Редактор с автоматическим удалением пробелов в конце строки может заодно удалить и перенос. В CommonMark есть более заметная запись — обратный слеш перед переводом строки:
Первая строка.
Вторая строка.
Для обычного текста я бы использовал абзацы. Принудительный перенос пригодится для адреса, подписи или стихотворения. Делать им отступы между разделами не нужно. Исходный синтаксис абзацев и переносов описан у Джона Грубера [13].
Что изменится, если между абзацами поставить пять пустых строк вместо одной?
В обычном разборе CommonMark оба варианта отделяют абзацы. Пять пустых строк не означают пятикратный вертикальный интервал. Внешний вид абзацев задаётся при отображении, например стилями страницы.
Это удобно для исходника: можно визуально разнести большие фрагменты, не пытаясь вручную верстать готовую страницу пустыми строками.
В длинном абзаце необязательно держать всё предложение на одной физической строке. Исходник можно разбить по смысловым частям: в истории изменений тогда проще увидеть, какая мысль поменялась. Но прежде чем использовать такой стиль, проверьте, как выбранная площадка обрабатывает мягкие переносы.
Сравните три записи в лаборатории:
Один абзац,
две строки исходника.
Один абзац,
принудительный перенос.
Два отдельных абзаца.
Второй начинается здесь.
Во втором примере обратный слеш должен быть последним символом строки. Если после него случайно оставить пробел, проверяйте результат заново. В исходниках с невидимыми символами режим показа пробелов полезнее, чем долгое всматривание в экран.
Не стоит переносить этот приём на блоки кода: там переводы строк и отступы являются частью самого примера. Команда из двух строк не обязана исполняться так же, как команда из одной.
Привычная форма:
# Название документа
## Раздел
### Подраздел
Пробел после решёток нужен: #Название в CommonMark не заголовок. Уровень задаёт структуру документа, поэтому выбирать его только ради размера шрифта неудобно для оглавления и навигации.
У CommonMark есть и другая форма заголовков:
Название документа
==================
Название раздела
---------------
Это заголовки первого и второго уровня. Отсюда первая задача.
Что получится из следующего фрагмента в CommonMark?
Сервис недоступен
---
Получится заголовок второго уровня: Сервис недоступен.
Чтобы получить абзац и горизонтальную линию, отделите их пустой строкой:
Сервис недоступен
---
Здесь пустая строка меняет структуру документа. Правила этой формы заголовков можно проверить в CommonMark [14].
В CommonMark уровней заголовков шесть. Для статьи на Хабре ориентируйтесь на уровни 1–3: именно их поддержка заявлена в документации HFM [15].
Сломанная вывеска не пускает в архив. Исправьте заголовок и оставьте под ним обычный абзац. Можно экспериментировать: результат меняется при вводе, а проверка запускается кнопкой.
Перейти к лаборатории [16] и выбрать вкладку «Заголовок». Условия, исходник и проверка находятся внутри задания.
Базовый набор выглядит так:
*Небольшой акцент*
**Важная мысль**
***Очень важная мысль***
Результат: небольшой акцент, важная мысль, очень важная мысль.
Зачёркивание ~~устарело~~ поддерживается, в частности, в GFM и HFM. В базовый CommonMark оно не входит.
Если звёздочки нужны буквально, их можно экранировать:
*Это не курсив*
# Это не заголовок
1. Это не начало списка
Результат последнего примера — обычный текст 1. Это не начало списка. Слеш снимает специальное значение с точки.
А для названий параметров, путей и идентификаторов удобнее строчный код:
Параметр `cache_enabled` находится в файле `settings.yaml`.
Это избавляет от необходимости вручную защищать каждый символ. Правила выделения и экранирования есть в исходном описании Markdown [17].
У маркированного списка есть три распространённых маркера: -, * и +. Внутри одного списка удобнее придерживаться одного.
- Подготовить конфигурацию
- Проверить подключение
- Запустить сервис
Для вложенности нужен отступ:
1. Подготовить окружение
- Создать конфигурацию
- Проверить доступ к базе
2. Запустить приложение
В CommonMark содержимое вложенных блоков выравнивают с учётом ширины маркера и пробела после него. Поэтому «всегда два пробела» — плохое универсальное правило. Особенно если список дошёл до двузначных номеров.
Полезный приём для редактирования — нумеровать все пункты единицами:
1. Собрать
1. Проверить
1. Опубликовать
В CommonMark получится последовательность 1, 2, 3. Пункты удобно переставлять, не исправляя номера в исходнике. При этом номер первого пункта задаёт начало последовательности.
В обычном README пункт может содержать несколько абзацев и блок кода. У HFM содержимое списков ограничено; сложную инструкцию для Хабра проще разбить на подразделы. Это один из случаев, когда перенос текста требует проверки, даже если все символы выглядят знакомо.
Частая задача для инструкции: в пункте есть действие, пояснение и команда. Попробуйте такой исходник в лаборатории:
1. Подготовьте каталог.
Он понадобится для временных файлов.
```sh
mkdir build
```
2. Проверьте результат.
Уберите отступ перед пояснением и блоком кода. Затем посмотрите HTML: остались ли они внутри первого li или вышли из списка? Так проще понять проблему, чем подбирать число пробелов до визуально подходящего результата.
Пустая строка тоже влияет на оформление списка. Сравните плотную запись и запись с пустыми строками между пунктами:
- Первый пункт
- Второй пункт
Разделяем два примера обычным абзацем.
- Первый пункт
- Второй пункт
У второго списка в HTML могут появиться отдельные абзацы внутри пунктов. Дополнительные интервалы возникают из структуры и стилей, а не из того, что Markdown «запомнил высоту пустой строки».
Вложенность особенно часто ломается при копировании из чата: внешне отступы сохранились, но перед командой стало на один пробел меньше. Поэтому готовую инструкцию полезно проверить копированием в обе стороны: из исходника в предпросмотр и из опубликованного блока в редактор.
Список задач в GFM:
- [x] Написать инструкцию
- [ ] Проверить ссылки
- [ ] Прочитать результат на телефоне
Пробел внутри [ ] обозначает незавершённую задачу, x — завершённую. Можно ли кликнуть по галочке и сохранится ли изменение, решает приложение. В HTML-превью она может быть вообще неактивной.
Для интерактивного учебника это существенная разница: нарисованный флажок ещё не является системой учёта ответов. О самом расширении — в спецификации GFM [18].
> Сначала сохраните конфигурацию.
>
> Затем меняйте параметры подключения.
Это уже обычный абзац.
На пустой строке между абзацами цитаты оставлен >. Так исходник явно показывает, что оба абзаца относятся к одному блоку.
В CommonMark цитаты можно вкладывать:
> Внешняя цитата.
>
> > Цитата внутри неё.
Для Хабра этот пример переносить как готовую конструкцию не стоит: документация HFM отдельно исключает вложенные цитаты. Здесь он показан именно как пример CommonMark.
Строчный код обычно окружают одной обратной кавычкой с каждой стороны. Но как показать внутри саму обратную кавычку?
Используйте `` `cache_enabled` `` для имени параметра.
Результат: используйте `cache_enabled` для имени параметра.
Внешний ограничитель состоит из двух обратных кавычек, внутренняя одиночная его не закрывает. Пробелы возле внутреннего содержимого в этом примере убираются при разборе code span. Если нужно точно сохранить форматирование с пробелами и переносами, лучше использовать отдельный блок. Правила строчного кода [19].
С блоками действует похожий приём. Чтобы показать ограду из трёх обратных кавычек, внешнюю делаем из четырёх:
````markdown
```python
print("Markdown не выполнит эту программу")
```
````
Внутренняя ограда остаётся частью примера. Если показываем уже четыре кавычки, снаружи понадобится пять. Так можно объяснять сам синтаксис, не превращая половину статьи в код.
После открывающей ограды можно указать язык:
```json
{"debug": false, "port": 8080}
```
Метка json помогает просмотрщику выбрать подсветку. Она не запускает JSON, не проверяет конфигурацию и не гарантирует, что выбранная программа вообще умеет такую подсветку. Примеры оград и подсветки в документации GitHub [20].
Ещё один вариант ограды в CommonMark — тильды:
~~~python
print("Внутри такого блока можно показать ```")
~~~
Открывающий и закрывающий маркеры должны быть одного типа. Тильды и обратные кавычки не закрывают друг друга.
В примере оборвался блок кода. Нужно сохранить внутреннюю строку из трёх кавычек и правильно закрыть внешнюю ограду. Проверяется получившаяся структура, поэтому допустимо несколько решений.
Перейти к лаборатории [16] и выбрать вкладку «Код». Условия, исходник и проверка находятся внутри задания.
- timeout: 5
+ timeout: 15
retries: 3
Здесь можно показать изменение конфигурации прямо в статье. Цвета зависят от подсветки, а знаки - и + останутся понятными и без неё. В примере изменён только timeout; retries оставлен для контекста. Настоящий коммит для такой иллюстрации не нужен.
Обычная ссылка:
[Документация проекта](https://example.org/docs)
Для длинного текста удобна ссылочная форма:
Перед установкой прочитайте [инструкцию][setup].
Если меняете сервер, снова откройте [инструкцию][setup].
[setup]: https://example.org/docs/setup "Подготовка окружения"
Адрес хранится в одном месте, а абзацы не растягиваются из-за URL. Определение ссылки само по себе не становится отдельным абзацем в результате. Это часть классического синтаксиса, описанная в документации Markdown [21].
Относительные ссылки удобны для документов в репозитории:
[Установка](docs/install.md)
[Назад](../README.md)
При публикации на другой площадке пути нужно пересмотреть: там может не быть ни docs, ни соседнего README. На GitHub относительные ссылки обрабатываются в контексте репозитория и ветки. Документация GitHub [22].
У ссылочной формы есть приятное свойство: адрес не мешает читать предложение. Есть и обратная сторона — определение легко потерять при копировании одного раздела.
Откройте [инструкцию][install].
[install]: https://example.org/install
Скопируйте пример в лабораторию и удалите последнюю строку. Вместо готовой ссылки останется запись с квадратными скобками. Обработчик не знает, куда она ведёт, и не должен придумывать адрес.
Ещё одна причина проверять копирование: #installation — переход к фрагменту страницы, а installation.md — путь к файлу. В README оба варианта могут работать, но ведут в разные места. В публикации второго файла, скорее всего, вообще нет.
Для оглавления лучше выбирать устойчивые подписи. Заголовок можно переформулировать, а явно заданный идентификатор раздела оставить прежним. Тогда внешняя ссылка на этот раздел не сломается из-за редакторской правки. В этой статье именно поэтому используются якоря вроде md-doom, а не URL, собранные из длинного русского заголовка.
У изображения перед квадратными скобками добавляется восклицательный знак:

Текст в квадратных скобках — альтернативное описание. «Схема подключения клиента к серверу» полезнее, чем «картинка 3».
Чтобы по нажатию открывался оригинал, изображение можно поместить внутрь ссылки:
[](images/network-full.png)
Пути здесь учебные: для своей публикации нужно подставить реальные файлы или адреса. Размер изображения базовый Markdown отдельно не задаёт; синтаксис вроде ![[image.png|300]] уже относится к конкретным приложениям.
До этого у нас были в основном буквы. Но изображение в Markdown — такой же обычный элемент, как ссылка. Один синтаксис подходит и для фотографии, и для схемы, и для анимации:



Это учебные адреса. Ниже — уже настоящие файлы, подготовленные для статьи. На одной схеме показаны исходник Markdown, HTML после разбора и готовый результат:
Запись, которой вставлена эта схема:

У Markdown нет отдельной команды «показать JPEG» или «запустить GIF». Разметка задаёт адрес изображения и описание. Формат файла распознаёт уже программа, которая его загружает.
|
Формат |
Что удобно показывать |
Особенность |
|---|---|---|
|
JPEG / JPG |
Фотографии |
Обычно сжатие с потерями; мелкий текст может выглядеть хуже |
|
PNG |
Скриншоты, схемы, интерфейсы |
Сжатие без потерь, поддержка прозрачности |
|
GIF |
Короткую последовательность действий |
Поддерживает анимацию, но имеет ограниченную палитру |
|
WebP |
Изображения и анимацию |
Поддерживает прозрачность, сжатие с потерями и без них |
|
SVG |
Векторные схемы и значки |
Масштабируется; допустимость загрузки и встраивания зависит от площадки |
Это свойства форматов, а не обещание, что любой редактор примет любой из них. Обзор — в документации форматов изображений MDN [23]. Для публикации важна вся цепочка: загрузка, хранение, возможное преобразование площадкой и отображение у читателя.
Ниже та же схема с последовательным выделением трёх этапов. У неё нет кнопок, проверки ответа или состояния квеста: это обычная анимация из трёх кадров.

GIF воспроизводит записанную последовательность. Нажать на конкретный элемент схемы и изменить её поведение этим синтаксисом нельзя. Для такого примера нужен виджет вроде нашей лаборатории.
В этой демонстрации GIF весит около 34 КБ. Запись экрана на несколько минут — совсем другая история: много кадров легко превращают её в тяжёлую загрузку. Для длинной демонстрации я бы готовил видео, а не заставлял читателя скачивать гигантскую картинку.
Я сохранил одну и ту же схему в трёх вариантах: PNG [24], JPEG [25] и WebP [26]. Можно открыть их отдельно и сравнить мелкий текст и границы цветных блоков.
|
Файл этой демонстрации |
Размер |
|---|---|
|
PNG |
21 133 байта |
|
JPEG, качество 80 |
22 977 байт |
|
WebP, качество 80 |
10 528 байт |
Это результат для одной конкретной схемы и выбранных настроек сохранения. Из него нельзя вывести правило «JPEG всегда тяжелее PNG» или «одинаковое значение качества означает одинаковую картинку». На фотографии с плавными переходами результат будет другим. Здесь интересно именно то, что текст Markdown во всех случаях практически одинаковый, а свойства загружаемого ресурса различаются.
Картинка внутри ссылки позволяет не заставлять читателя искать отдельное «скачать». Например:
[](https://doom.synapsea.agency/media/markdown-flow.png)
В этом примере оба адреса ведут на один файл. Для тяжёлой иллюстрации можно подготовить отдельную уменьшенную копию: внутренний адрес будет указывать на неё, внешний — на оригинал. Markdown сам такую копию не создаёт.
Не путайте три разных текста: alt описывает изображение, необязательный title может использоваться для подсказки, подпись под картинкой — отдельный элемент оформления. Важное пояснение я оставлю обычным абзацем, чтобы оно было доступно и без наведения мыши.
А размеры изображения, кадрирование и галерея с переключением — уже возможности просмотрщика. Добавить width=300 в произвольное место Markdown и получить одинаковый результат на всех площадках не выйдет.
Таблицы из вертикальных черт — расширение, знакомое по GFM и поддерживаемое на Хабре:
| Параметр | Значение |
| --- | --- |
| Порт | 8080 |
| Повторные попытки | 3 |
В GFM двоеточия задают выравнивание:
| Параметр | Состояние | Количество |
| :--- | :---: | ---: |
| Запросы | Готово | 12 |
| Ошибки | Нет | 0 |
В первом столбце оно левое, во втором — по центру, в третьем — правое. Удобно для чисел. Внешние вертикальные черты можно оставлять: они делают исходник понятнее.
Теперь задача. Почему эта строка опасна для таблицы?
| Выражение | `a | b` |
В GFM вертикальную черту внутри ячейки нужно экранировать, в том числе внутри строчного кода:
| Выражение | `a | b` |
Табличный разбор использует | как разделитель. Одних обратных кавычек недостаточно. Этот случай отдельно показан в документации таблиц GitHub [27].
Обычная Markdown-таблица плохо подходит для длинных инструкций и многоабзацных ячеек. Если в столбце оказался экран текста, я бы перенёс его в подраздел. На телефоне такая таблица всё равно не станет удобной от идеально выровненных черт в исходнике.
В таблице пропала часть выражения. Исправьте разделитель так, чтобы вся запись осталась во второй ячейке и отображалась как код.
Перейти к лаборатории [16] и выбрать вкладку «Таблица». Условия, исходник и проверка находятся внутри задания.
Сноска позволяет оставить пояснение отдельно от основного предложения:
Время зависит от состояния кеша[^cache].
[^cache]: Сравнивать холодный и прогретый запуск нужно отдельно.
Такую запись поддерживает GitHub, но это не базовый CommonMark. Более того, набор возможностей GitHub шире текста спецификации GFM. Поэтому фраза «поддерживает GFM» ещё не обещает все удобства интерфейса GitHub. Документация сносок [28].
Ещё один пример — предупреждения:
> [!WARNING]
> Проверьте путь перед удалением временных файлов.
На GitHub это специальный блок. В другом просмотрщике может остаться цитатой с буквальным [!WARNING]. Это не обязательно ошибка [29] файла: обработчик просто не знает такого расширения.
HTML-комментарий позволяет оставить пометку в исходнике:
<!-- Проверить подпись перед публикацией. -->
Скрытие при отображении не делает содержимое секретным. Читатель, у которого есть исходный файл, увидит комментарий. Поэтому черновые заметки стоит просматривать перед отправкой вместе с основным текстом.
Markdown часто читают как набор замен: встретил _ — включил курсив. Но на практике важен контекст. Проверьте рядом:
snake_case_name
_отдельный акцент_
`snake_case_name`
Первую запись обработчик может оставить обычным текстом, вторую оформить как выделение, третью — как код. Для имени переменной я всё равно выберу третью: так читатель сразу видит, что это точное написание идентификатора, которое можно скопировать.
С парными маркерами полезно экспериментировать через HTML-представление. Если предполагаемое выделение осталось буквальными звёздочками, сначала посмотрите на пробелы возле открывающего и закрывающего маркеров. Не начинайте с добавления ещё пяти звёздочек.
Некоторые редакторы показывают оглавление автоматически. Другие распознают служебный маркер вроде [TOC]. Обычный файл .md не обещает ни того, ни другого.
Переносимый по смыслу вариант — список ссылок на разделы. Но даже в нём нужно знать правила формирования якорей. У разных площадок могут различаться обработка знаков препинания, повторяющихся заголовков и нелатинских символов. В этой статье есть явные якоря HFM; в репозитории я проверил бы ссылки в самом GitHub.
Проверка простая: открыть документ заново и пройти по каждой ссылке оглавления. Если переход работает только после какого-то действия редактора, это уже свойство текущей сессии, а не гарантия опубликованного файла.
В поддерживающем математику [30] редакторе можно написать:
Вероятность ошибки: $p = 1 - q$.
$$
T = T_{network} + T_{server}
$$
Получившаяся формула удобна для текста, но TeX внутри долларовых знаков не становится частью универсального Markdown. GitHub поддерживает математические выражения отдельным механизмом. Документация формул [31].
Со схемами похожая история. Например, GitHub умеет обрабатывать Mermaid в блоках кода:
```mermaid
flowchart LR
A[Исходный текст] --> B[Разбор разметки]
B --> C[Отображение]
```
Без поддержки Mermaid вы увидите текст схемы. Метка после тройных кавычек сама по себе ничего не рисует. На Хабр такой блок нельзя переносить с расчётом, что он обязательно превратится в диаграмму. Для переносимой публикации схему можно заранее экспортировать в изображение. Документация диаграмм GitHub [32].
В Obsidian можно встретить такие конструкции:
[[Сетевая диагностика]]
![[Сетевая диагностика]]
==Проверить позже==
Это внутренняя ссылка, встраивание заметки и выделение текста. Они удобны внутри соответствующей среды, но требуют её правил обработки. При переносе в другую систему их приходится преобразовывать. Синтаксис Obsidian [33].
Свой набор расширений есть и у Pandoc: сноски, списки определений, метаданные и другие конструкции. Например, YAML-блок в начале документа:
---
title: "Проверка конфигурации"
lang: ru
---
Значение такого блока определяется инструментом. В обычном обработчике без поддержки метаданных он не обязан исчезнуть из текста или превратиться в свойства документа. Руководство Pandoc [34].
Это важно и для архивов: сохранить текст достаточно для чтения исходника, но для точного восстановления результата иногда нужны сведения о диалекте и подключённых расширениях.
Все ответы под спойлерами в этой статье — уже интерактивные элементы. Читатель выбирает, когда их открыть. Такой же приём подходит для упражнений, подсказок и пошагового разбора.
На GitHub раскрывающийся блок обычно оформляют через HTML:
<details>
<summary>Показать пример конфигурации</summary>
```yaml
port: 8080
debug: false
```
</details>
Пустые строки помогают отделить Markdown-содержимое от HTML-блока. На GitHub внутри можно размещать текст, изображения и код. Поддержку той же конструкции в другом редакторе нужно проверять. Документация раскрывающихся блоков [35].
В HFM используется своя запись:
<spoiler title="Показать ответ">
Ответ с **выделением** и `примером`.
</spoiler>
Можно добавить и навигацию по документу. На Хабре явно заданный якорь выглядит так:
<anchor>example-answer</anchor>
А ссылка на него — так:
[Перейти к ответу](#example-answer)
Этим же способом устроены переходы в начале статьи. Проверьте: вернуться к навигации [36], а затем открыть раздел снова.
Из переходов можно собрать учебный маршрут с выбором следующей темы. Но ссылка не хранит инвентарь, а спойлер не подсчитывает баллы. Для переменных, таймеров и проверки введённого ответа нужен исполняемый код или дополнительные возможности приложения.
В этой статье уже есть спойлеры и якоря. Из них можно сделать небольшой выбор маршрута. Допустим, страница сломалась после добавления примера. Что проверяем первым?
Если весь остаток статьи стал кодом — проверить ограду [5].
Если исчезла часть выражения — проверить разделители таблицы [6].
Если не показалась картинка — проверить адрес и формат [4].
Если причина непонятна — сократить пример и разобрать его [37].
У этого маршрута нет скрытого счётчика или инвентаря. Состояние держит в голове читатель, а документ помогает выбрать следующий раздел. Для инструкции такой интерактивности иногда достаточно.
Не каждому читателю нужен готовый ответ сразу. Можно сначала открыть направление поиска, а затем — решение. HFM допускает вложенные спойлеры; это пригодится для задач на предсказание результата.
Задача: между строкой текста и --- нет пустой строки. Где окажется разделитель?
Дефисы могут относиться к строке над ними. Вспомните вторую форму заголовков.
В примере CommonMark строка над дефисами станет заголовком второго уровня. Чтобы отделить горизонтальную линию, добавьте перед ней пустую строку.
Структура такого блока описана в справке HFM [15]. А проверка ответа, баллы и запоминание [38] пройденного — уже работа приложения. Поэтому в нашей лаборатории есть и обычные примеры, и отдельные задания с кнопкой проверки.
У Хабра есть медиаэлементы, в том числе для CodePen. Такая вставка позволяет разместить интерактивный пример в публикации; её поведение нужно проверить в предпросмотре. Это возможность площадки, а не выполнение JavaScript из обычного Markdown-блока. Справка редактора Хабра [39].
На собственном сайте можно использовать MDX и добавлять компоненты прямо между абзацами. Например, компонент редактора и предпросмотра. Такой исходник требует соответствующей сборки, обычный просмотрщик .md его не исполнит. Документация MDX [40].
Во вкладке «Квест» ссылки становятся выходами из комнат. Найдите ключ и откройте дверь. Рядом виден Markdown текущей комнаты; его можно читать, но редактирование в этом уровне отключено.
Перейти к лаборатории [16] и выбрать вкладку «Квест». Условия, исходник и проверка находятся внутри задания.
Здесь как раз видна граница: Markdown описывает заголовки, текст и ссылки. Нажатие перехватывает JavaScript, меняет текущую комнату и запоминает ключ. Сама ссылка #exit ничего не знает о замке. Если перенести один исходник комнаты в обычный README, игровой логики у него не появится.
В какой-то момент обычных упражнений мне стало мало. Если уж разбираться, что можно засунуть в Markdown, проверять надо на Doom.
Откройте вкладку Result во встроенном блоке: игра загрузится автоматически. Затем выберите New Game в меню. Если встроенный блок не открывается, игру можно запустить в отдельной вкладке [41].
Управление в меню: ↑/↓ и Enter. В игре: WASD — ходьба, стрелки влево и вправо — поворот, пробел — стрельба, E — открыть дверь. Esc возвращает меню. Под экраном есть кнопки для управления без клавиатуры. В этой сборке звук отключён, а сохранения между запусками не переносятся.
Я взял готовый WebAssembly-порт Chocolate Doom и свободные игровые ресурсы Freedoom. Внутрь одного .md упаковал четыре файла: загрузчик JavaScript, модуль .wasm, файл уровней и графики .wad и настройки управления. Это примерно 16,4 МиБ текста.
Для каждого ресурса схема одинаковая: сжать gzip, закодировать в Base64 и положить в ограждённый блок кода. В строке после открывающих кавычек — имя файла, размер и SHA-256. Ниже — сокращённый пример структуры; вместо многоточий в настоящем документе находятся метаданные и данные:
```doom-asset {"name":"freedoom1.wad","bytes":28795076,"sha256":"…","encoding":"gzip+base64"}
H4sI…
```
Обычный Markdown-просмотрщик покажет такой блок как код. Метка doom-asset не добавляет языку никаких способностей. Её понимает написанный для этого опыта [42] загрузчик.
При загрузке встроенного блока он скачивает документ, находит блоки ресурсов, декодирует Base64 и распаковывает gzip. Затем сверяет размер и SHA-256 каждого файла. Эта проверка помогает обнаружить повреждённые данные; подлинность автора файла она сама по себе не доказывает.
Дальше загрузчик передаёт ресурсы во вложенный игровой фрейм. Emscripten получает WebAssembly-модуль, а WAD и настройки попадают в виртуальную файловую систему движка. Картинку рисует canvas, нажатия обрабатывает игра. Markdown к этому моменту уже выполнил свою работу: донёс все четыре файла до загрузчика.
Файл с игрой действительно имеет расширение .md, и ресурсы действительно лежат внутри него. Но исполняет игру браузер с WebAssembly. Если открыть этот же документ в обычном редакторе заметок, монстры оттуда не полезут.
Это эксперимент с Markdown как контейнером. Я не переписал движок на синтаксис заголовков и списков. Порт Chocolate Doom существовал до этого опыта; моя часть — упаковка ресурсов в документ и просмотрщик, который умеет их извлечь и запустить.
Чтобы проверить это самостоятельно, можно скачать документ с игрой [43] и офлайн-просмотрщик [44]. Откройте просмотрщик в современном браузере и выберите скачанный .md. Это отдельный способ запуска: для него не нужна статья на Хабре.
Свободный исходный код движка не делает свободными оригинальные уровни, графику и звуки коммерческой игры. Поэтому в эксперименте используются ресурсы Freedoom: знакомая механика Doom, но другие карты, противники и оформление. Лицензии и происхождение файлов [45], исходники движка [46] доступны рядом с демо.
Встроенный проигрыватель тоже требует отдельного окружения. На своём сайте это HTML-страница с JavaScript. В статье — внешний виджет, который поддерживает площадка. От вставки большого блока Base64 в текст публикации игра не запустится. Зато на этом примере хорошо видно, где заканчивается разметка и начинается приложение вокруг неё.
Base64 нужен, чтобы двоичные файлы можно было перенести как текст. Он не сжимает данные: три входных байта превращаются в четыре символа, не считая дополнения в конце. Устройство Base64 [47]. Упаковывать большой WAD непосредственно в Base64 было бы накладно.
В нашем случае порядок такой:
исходный ресурс → gzip → Base64 → блок в .md
блок в .md → Base64 → распаковка gzip → исходный ресурс
После обратного преобразования должен получиться тот же набор байтов. Поэтому рядом с ресурсом лежат размер распакованного файла и его хеш. Если при переносе документ обрезали, загрузчик должен сообщить о повреждении, а не передать движку случайный остаток файла.
Цена эксперимента хорошо видна ещё до запуска игры. Markdown с ресурсами весит примерно 16,4 МиБ; при первом открытии его нужно получить целиком. Затем браузеру понадобится память [48] и на текст Base64, и на распакованные файлы, и на сам движок. Размер скачивания не равен расходу оперативной памяти.
Для обычного сайта я бы раздавал .wasm и WAD отдельными файлами, с нормальным кешированием каждого ресурса. Здесь упаковка в документ — предмет эксперимента. Она даёт один переносимый файл с данными, но делает обновление маленькой настройки несоразмерно дорогим, если ради него пересобирать весь пакет.
Поэтому настройки управления веб-версии загрузчик дополняет отдельно: чтобы поменять подсказку или отключить вертикальное движение мышью, нам не понадобилось повторно выкладывать весь многомегабайтный документ.
Самая сложная часть оказалась между страницей и игрой. Просто вставить произвольный iframe в текст Хабра не получилось: редактор проверяет допустимые элементы. А ссылка на редактор CodePen и ссылка на встраиваемый результат — разные вещи.
Рабочая цепочка в нашем тестовом черновике получилась такой:
статья Хабра
→ разрешённая вставка JSFiddle
→ страница игры на нашем сервере
→ игровой фрейм с WebAssembly
У каждого слоя свои размеры, обработка фокуса и ограничения. Первый вариант показывал часть игрового экрана с полосой прокрутки. Игра уже работала, но играть было неудобно. Пришлось отдельно подогнать высоту вставки и канваса, а затем проверить результат именно в предпросмотре статьи.
С мышью обнаружилась ещё одна особенность. Нативное меню Doom не сопоставляет положение черепка с абсолютной координатой указателя, как меню обычного сайта. А вертикальное движение мышью в классическом управлении может двигать персонажа вперёд и назад. Для человека, который ожидает поведение современного браузерного шутера, это выглядит как сломанное управление.
Захват курсора во вставке тоже не заработал: в проверенном нами фрейме Хабра отсутствует разрешение allow-pointer-lock. Добавление этого разрешения во внутренний фрейм не отменяет ограничение внешнего. Документация Pointer Lock [49]. Поэтому для статьи оставили клавиатуру и экранные кнопки; перемещения свободного курсора больше не передаются движку.
Такой результат я и считаю честным описанием опыта: Doom удалось встроить в черновик, но вместе с ним пришлось учитывать ограничения площадки. В обычном Markdown всего этого нет. Там есть текст, который просмотрщик решил обработать специальным образом.
Помимо спойлеров и якорей, в HFM есть поддерживаемые HTML-подобные конструкции:
H<sub>2</sub>O
10<sup>3</sup>
<abbr title="Язык разметки">Markdown</abbr>
<u>Подчёркнутый фрагмент</u>
Формулы, упоминания и карточки персон тоже имеют свои правила. Наличие таких элементов не означает, что в публикацию можно вставить произвольный HTML, стили или скрипты.
Отдельная ловушка: возможности статьи и комментария различаются. В документации HFM, например, таблицы и заголовки отмечены как доступные для публикаций, но не для комментариев. Если переносите пример из статьи в обсуждение, проверьте результат заново. Полная справка HFM [15].
Допустим, после вставки команды половина документа превратилась в серый блок. Переписывать статью не нужно. Сначала оставляем минимальный фрагмент: несколько строк перед местом поломки, сам пример и строку после него.
Затем проверяем четыре вещи.
Ограда кода. Совпадает ли тип маркеров, хватает ли закрывающих символов, не попала ли внешняя ограда внутрь примера? При объяснении Markdown это моя первая проверка.
Пустые строки. Отделён ли новый блок от предыдущего? Не превратилась ли строка с дефисами в подчёркивание заголовка? Не продолжилась ли цитата там, где автор уже ожидал обычный текст?
Отступы. К какому пункту списка принадлежит команда? Осталось ли пояснение внутри li? Визуальное выравнивание исходника и структура результата связаны, но не тождественны.
Окружение. Есть ли у обработчика поддержка таблиц, сносок, формул, сырых HTML-элементов? Если её нет, дополнительные пробелы не установят недостающее расширение.
Уберите лишнее, добейтесь ожидаемого результата на пяти строках и только потом возвращайте большой документ. В лаборатории в начале статьи можно вставить свой небольшой фрагмент. Начните с заготовки «Сломанный README» и попробуйте исправить его без готового ответа.
Вернуться к лаборатории [16] → вкладка «Лаборатория» → заготовка «Сломанный README».
Для собственного формата ресурсов Doom регулярное выражение ищет строго определённые блоки, которые создаёт наш упаковщик. Это узкая задача с заранее известным входом.
Произвольная статья устроена сложнее: в ней могут быть вложенные списки, ограды разной длины, обратные кавычки внутри кавычек и ссылки внутри цитат. После нескольких «простых замен» быстро выясняется, что одна замена ломает результат другой.
Поэтому учебные задания проверяет готовый парсер. Мы сравниваем не написание ответа с эталонной строкой, а получившиеся элементы. Для задания с оградой достаточно, чтобы нужный текст попал в один блок кода, а слово «Готово» осталось отдельным абзацем. Решение через тильды имеет такое же право на успех, как решение через четыре обратные кавычки.
Это не универсальная система оценки произвольных документов. У каждого упражнения есть небольшой, явно заданный набор условий. Зато читателю не приходится угадывать, какой из нескольких корректных вариантов я внёс в список разрешённых ответов.
При обмене документом я бы сохранил исходник, нужные изображения и короткую заметку об окружении: какой диалект использован, какие расширения обязательны и что произойдёт без них.
Например, «GFM, диаграммы Mermaid, картинки в каталоге images» сообщает о документе больше, чем просто «README». Для нашего учебника условия ещё конкретнее: основной текст читается как Markdown, лаборатория выполняется во внешнем виджете внутри статьи, Doom дополнительно скачивает ресурсный документ с сервера.
Если интерактивный блок не загрузился, вокруг него должны остаться понятные условия упражнения и исходник. Пустой прямоугольник между двумя фразами не объясняет, чему мы собирались научить читателя.
Теперь можно проверить, насколько предсказуемым стал исходник. Все вопросы, где не указана площадка, относятся к CommonMark.
4. Подготовка
1. Проверка
99. Запуск
4, 5, 6. Начало последовательности задаёт первый пункт. Последующие номера в исходнике не заставляют нумерацию прыгать.
Практический смысл: после вставки нового пункта необязательно вручную пересчитывать остальные. Но если номера являются частью данных — например, это идентификаторы шагов из другой системы, — лучше не поручать их обычному нумерованному списку.
**Параметр** `**debug**`
Жирным станет только слово Параметр. Во втором фрагменте звёздочки останутся частью строчного кода: **debug**.
Поэтому примеры синтаксиса удобно оформлять как код: иначе текст может начать демонстрировать собственное действие вместо исходной записи.
````text
первая строка
```
последняя строка
````
На последней строке из четырёх обратных кавычек. Три кавычки внутри короче открывающего ограничителя и остаются содержимым блока.
Если при копировании примера внешняя ограда потеряла один символ, результат может измениться целиком. Именно поэтому исходник статьи про Markdown полезно проверять отдельно от её визуального вида.
```mermaid
flowchart LR
A --> B
```
Нет. Нужен обработчик Mermaid. Обычный блок кода допускает метку языка, но не обещает отдельный движок для неё.
На поддерживающей площадке появится диаграмма; в другом просмотрщике останется текст. При публикации важно знать, какой из этих результатов получит читатель.
<spoiler title="Ответ">Проверка закончена</spoiler>
Сам по себе этот тег не создаст на GitHub хабровский спойлер. Для README нужно использовать поддерживаемую там конструкцию details / summary.
При переносе документа меняется не только адрес картинки. Иногда приходится менять структуру интерактивного блока.
Осталось собрать заголовок, выделение, список и цитату. Смотрите на структуру результата: проверка не требует посимвольного совпадения с образцом. Подсказка и готовое решение доступны, если застряли.
Перейти к лаборатории [16] и выбрать вкладку «Финал». Условия, исходник и проверка находятся внутри задания.
У каждого задания есть исходный текст, описание цели и проверка результата. При вводе текст разбирает библиотека Marked в режиме GFM, а приложение показывает полученный HTML. Для первого задания проверяется наличие заголовка второго уровня с нужным текстом и отдельного абзаца. Для таблицы — число ячеек и содержимое строчного кода.
Поэтому замена тройной ограды на четыре кавычки — не единственное решение второго задания. Ограда из тильд тоже подойдёт, если внутри окажется нужный текст. Проверка смотрит на результат разбора.
В предпросмотре заданий отключён сырой HTML. После разбора остаётся ограниченный набор элементов, а ссылки допускаются только на локальные фрагменты. Это позволяет экспериментировать с разметкой, не превращая поле ввода в редактор исполняемых HTML-страниц.
Пройденные уровни и черновики сохраняются в localStorage этого браузера, если хранилище доступно. На другое устройство прогресс не переносится. Кнопка «Начать заново» сбрасывает выбранный уровень. Галочки прогресса, переключение комнат и проверка ответов — функции небольшого приложения вокруг Markdown.
Ниже небольшой шаблон для README или заметки с поддержкой GFM. Адрес документации — пример; замените его своим. При переносе на Хабр отдельно проверьте список задач и ссылочные определения.
# Настройка приложения
Кратко: что настраиваем и какой результат ожидаем.
## Перед запуском
- [ ] Подготовить конфигурацию
- [ ] Проверить доступ к базе
## Параметры
| Параметр | Значение | Назначение |
| --- | --- | --- |
| `port` | `8080` | Порт приложения |
| `debug` | `false` | Отладочный режим |
## Пример конфигурации
```yaml
port: 8080
debug: false
```
## Проверка
Опишите действие и наблюдаемый результат.
Подробности — в [документации][docs].
[docs]: https://example.org/docs
Перед публикацией откройте результат в той системе, где его будут читать: проверьте переходы, вложенные блоки, таблицы и копирование примеров. Для статьи на Хабре последняя проверка — его собственный предпросмотр, в том числе на узком экране.
Если встречали конструкцию, которая заметно по-разному работает в двух редакторах, принесите в комментарии короткий исходник и названия обоих. Такие примеры полезнее очередного спора о том, сколько пробелов «правильно» ставить вообще.
Автор: ShyDamn
Источник [50]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/36751
URLs in this post:
[1] поведение: http://www.braintools.ru/article/9372
[2] переходите к Doom: #md-doom
[3] поведение: http://www.braintools.ru/article/5593
[4] изображениям и GIF: #md-images
[5] обратным кавычкам: #md-code
[6] таблицам: #md-tables
[7] интерактивности: #md-interactive
[8] финальной проверке: #md-test
[9] Спецификация CommonMark: https://spec.commonmark.org/0.31.2/
[10] спецификация GFM: https://github.github.com/gfm/
[11] лаборатория доступна отдельно: https://doom.synapsea.agency/lab/
[12] Параметры Marked: https://marked.js.org/using_advanced#options
[13] Джона Грубера: https://daringfireball.net/projects/markdown/syntax#p
[14] CommonMark: https://spec.commonmark.org/0.31.2/#setext-headings
[15] документации HFM: https://habr.com/ru/docs/help/markdown/
[16] Перейти к лаборатории: #md-lab
[17] исходном описании Markdown: https://daringfireball.net/projects/markdown/syntax#backslash
[18] в спецификации GFM: https://github.github.com/gfm/#task-list-items-extension-
[19] Правила строчного кода: https://spec.commonmark.org/0.31.2/#code-spans
[20] Примеры оград и подсветки в документации GitHub: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks
[21] документации Markdown: https://daringfireball.net/projects/markdown/syntax#link
[22] Документация GitHub: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#relative-links
[23] документации форматов изображений MDN: https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types
[24] PNG: https://doom.synapsea.agency/media/markdown-flow.png
[25] JPEG: https://doom.synapsea.agency/media/markdown-flow.jpg
[26] WebP: https://doom.synapsea.agency/media/markdown-flow.webp
[27] документации таблиц GitHub: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables
[28] Документация сносок: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#footnotes
[29] ошибка: http://www.braintools.ru/article/4192
[30] математику: http://www.braintools.ru/article/7620
[31] Документация формул: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/writing-mathematical-expressions
[32] Документация диаграмм GitHub: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams
[33] Синтаксис Obsidian: https://help.obsidian.md/syntax
[34] Руководство Pandoc: https://pandoc.org/MANUAL.html#extension-yaml_metadata_block
[35] Документация раскрывающихся блоков: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
[36] вернуться к навигации: #md-route
[37] сократить пример и разобрать его: #md-debug
[38] запоминание: http://www.braintools.ru/article/722
[39] Справка редактора Хабра: https://habr.com/ru/docs/help/wysiwyg/
[40] Документация MDX: https://mdxjs.com/docs/
[41] игру можно запустить в отдельной вкладке: https://doom.synapsea.agency/
[42] опыта: http://www.braintools.ru/article/6952
[43] документ с игрой: https://doom.synapsea.agency/doom-in-markdown.md
[44] офлайн-просмотрщик: https://doom.synapsea.agency/doom-reader.html
[45] Лицензии и происхождение файлов: https://doom.synapsea.agency/NOTICE.txt
[46] исходники движка: https://doom.synapsea.agency/engine-corresponding-source.tar.gz
[47] Устройство Base64: https://developer.mozilla.org/en-US/docs/Glossary/Base64
[48] память: http://www.braintools.ru/article/4140
[49] Документация Pointer Lock: https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API
[50] Источник: https://habr.com/ru/articles/1091602/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1091602
Нажмите здесь для печати.