Создание блочного токенайзера

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

Блочный токенайзер работает пошагово, анализируя текст строку за строкой. Каждая строка проверяется на соответствие определённому регулярному выражению, представляющему конкретный Markdown-элемент.


Структура токенов

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

  • type — тип токена (например, heading, paragraph, list, blockquote, code).
  • raw — исходный текст блока, включая все Markdown-символы.
  • text — очищенный текст без управляющих символов.
  • depth — уровень вложенности, актуально для заголовков и списков.
  • tokens — массив вложенных токенов (используется для элементов с внутренней структурой, например, списки или блоки цитат).

Пример токена заголовка:

{
  "type": "heading",
  "raw": "## Пример заголовка",
  "text": "Пример заголовка",
  "depth": 2
}

Основные методы блочного токенайзера

Блочный токенайзер в Marked реализован как класс Lexer с набором методов для обработки текста:

lex(src)

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

Пример использования:

import { Lexer } from 'marked';

const markdownText = `
# Заголовок 1

Параграф текста.

- Пункт списка 1
- Пункт списка 2
`;

const tokens = Lexer.lex(markdownText);
console.log(tokens);

token(src, top)

Метод token — это ядро парсинга, вызываемое рекурсивно для каждого блока. Параметр top указывает, что текущий уровень — верхний, что позволяет корректно обрабатывать вложенные списки и блоки цитат. Метод анализирует текст и возвращает массив токенов, готовых к дальнейшей обработке рендерером.


Обработка списков

Списки в Markdown могут быть упорядоченными и неупорядоченными, а также содержать вложенные пункты. Токенайзер обрабатывает их с учётом нескольких условий:

  1. Определение типа списка: проверка на символ -, * или + для неупорядоченных списков, числа с точкой для упорядоченных.
  2. Разделение элементов: каждая строка списка становится отдельным токеном list_item.
  3. Рекурсия: вложенные списки обрабатываются вызовом метода token для подблоков.

Пример токенизации списка:

{
  "type": "list",
  "ordered": false,
  "start": null,
  "loose": false,
  "items": [
    {
      "type": "list_item",
      "text": "Пункт списка 1",
      "tokens": []
    },
    {
      "type": "list_item",
      "text": "Пункт списка 2",
      "tokens": []
    }
  ]
}

Обработка заголовков и параграфов

Заголовки распознаются по количеству символов # в начале строки. Глубина заголовка соответствует количеству символов #. Параграфы объединяют последовательные строки текста, разделённые пустой строкой, в один токен paragraph.

Регулярное выражение для заголовка верхнего уровня:

/^(#{1,6})\s+(.*)$/gm

Принцип работы токенайзера:

  1. Проверка каждой строки на соответствие заголовку.
  2. Если найдено совпадение, создаётся токен heading с полями depth и text.
  3. Если не найдено совпадение, строка добавляется в текущий параграф.

Блоки кода и цитаты

Кодовые блоки могут быть двух типов:

  • Фенстричные (indented) — начинаются с 4 пробелов или таба.
  • Фенстричные с флагом (fenced) — начинаются с трёх или более символов ``` или ~~~, с возможным указанием языка.

Цитаты (blockquote) начинаются с символа > и могут содержать вложенные элементы. Токенайзер обрабатывает цитаты рекурсивно, вызывая token для внутренних блоков.

Пример токена блока цитаты:

{
  "type": "blockquote",
  "raw": "> Цитата\n> Продолжение цитаты",
  "tokens": [
    {
      "type": "paragraph",
      "text": "Цитата\nПродолжение цитаты"
    }
  ]
}

Настраиваемые правила

Marked позволяет переопределять стандартные правила блочного токенайзера через объект Lexer.rules. Это полезно для поддержки нестандартного синтаксиса Markdown или расширения функционала.

Пример добавления нового правила:

import { Lexer, LexerRules } from 'marked';

Lexer.rules.customHeading = /^!!!\s+(.*)$/m;

const customTokens = Lexer.lex("!!! Важный заголовок");

В результате будет создан токен с типом customHeading, который можно обработать отдельным рендерером.


Вложенность и рекурсивная обработка

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


Итоговая структура токенов

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