Модификация токенов на лету

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

  • type – тип элемента (например, paragraph_open, inline, heading_open).
  • tag – соответствующий HTML-тег (p, h1, ul и др.).
  • attrs – массив атрибутов [ключ, значение].
  • map – массив с индексами строк [начальная, конечная].
  • nesting – уровень вложенности: 1 – открывающий тег, -1 – закрывающий, 0 – одиночный.
  • content – текстовое содержимое для токенов типа inline.
  • children – массив вложенных токенов для inline и некоторых других типов.

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


Обход и модификация токенов

Чтобы изменить поведение Markdown-it, используют метод use для подключения плагинов. Основная идея модификации заключается в обходе массива токенов и редактировании их свойств. Пример базовой функции-плагина:

function customPlugin(md) {
  md.core.ruler.push('modify_tokens', function(state) {
    state.tokens.forEach(token => {
      if (token.type === 'paragraph_open') {
        token.attrs = token.attrs || [];
        token.attrs.push(['class', 'custom-paragraph']);
      }
    });
  });
}

const md = require('markdown-it')();
md.use(customPlugin);

В этом примере каждому параграфу добавляется CSS-класс custom-paragraph. Важно, что модификация происходит до рендеринга, что обеспечивает единообразие HTML-кода.


Работа с вложенными токенами

Некоторые токены, например inline, содержат массив children, где хранятся текстовые элементы и встроенные объекты, такие как ссылки, эмодзи или код. Для изменения текста или атрибутов внутри таких токенов требуется рекурсивный обход:

function modifyInlineTokens(tokens) {
  tokens.forEach(token => {
    if (token.type === 'inline' && token.children) {
      token.children.forEach(child => {
        if (child.type === 'text') {
          child.content = child.content.replace(/важно/g, 'критично');
        }
      });
    }
  });
}

После такой обработки все вхождения слова “важно” в тексте будут заменены на “критично”. Рекурсивный подход необходим, если структура документа сложная, с вложенными списками, блоками цитат и таблицами.


Создание новых токенов на лету

Markdown-it позволяет не только изменять существующие токены, но и создавать новые. Для этого используется конструктор Token, доступный через require('markdown-it/lib/token'):

const Token = require('markdown-it/lib/token');

function insertCustomToken(md) {
  md.core.ruler.push('insert_token', function(state) {
    const token = new Token('paragraph_open', 'p', 1);
    token.attrs = [['class', 'inserted']];
    state.tokens.push(token);

    const inline = new Token('inline', '', 0);
    inline.content = 'Вставленный абзац';
    inline.children = [];
    state.tokens.push(inline);

    state.tokens.push(new Token('paragraph_close', 'p', -1));
  });
}

md.use(insertCustomToken);

Этот метод позволяет добавлять произвольные элементы в любой точке документа, создавая динамические блоки Markdown, которые затем корректно рендерятся в HTML.


Изменение рендеринга через токены

Еще один мощный способ управления выводом — переопределение рендер-функций для конкретных типов токенов:

md.renderer.rules.paragraph_open = (tokens, idx) => {
  const token = tokens[idx];
  return `<p style="color:red;">`;
};

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


Работа с атрибутами и дополнительными свойствами

Токены поддерживают массив атрибутов attrs, что позволяет добавлять CSS-классы, id, data-* атрибуты. Также можно создавать собственные свойства на токенах, которые используются внутри плагинов:

state.tokens.forEach(token => {
  if (token.type === 'heading_open') {
    token.customLevel = parseInt(token.tag[1]);
  }
});

Эти свойства не влияют на стандартный рендеринг, но позволяют хранить вспомогательные данные для плагинов, анализа или генерации оглавления.


Производительность и рекомендации

  1. Минимизировать глубокие обходы: рекурсивная обработка токенов затратна для больших документов. Использовать фильтры по типу токена, чтобы обрабатывать только нужные элементы.
  2. Сохранять консистентность вложенности: при добавлении токенов важно корректно выставлять nesting и tag, иначе HTML будет некорректным.
  3. Разделять модификацию и рендеринг: изменения токенов на стадии core лучше оставить для логики, а визуальные эффекты реализовывать через кастомные рендереры.
  4. Использовать собственные свойства: добавление произвольных полей на токены удобно для передачи информации между плагинами.

Модификация токенов в Markdown-it открывает возможность создавать сложные кастомные расширения, динамические контенты и продвинутые рендереры, сохраняя при этом высокую производительность и структурированность документа.