Создание собственных токенов

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


Понимание токенов

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

{
  type: 'paragraph_open',
  tag: 'p',
  attrs: null,
  map: [0, 1],
  nesting: 1,
  level: 0,
  children: null,
  content: '',
  markup: '',
  info: ''
}
  • type — тип токена (например, paragraph_open, inline, text).
  • tag — HTML-тег, с которым ассоциирован токен.
  • nesting — уровень вложенности: 1 — открывающий, 0 — самозакрывающий, -1 — закрывающий.
  • children — массив вложенных токенов для элементов типа inline.

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


Регистрация нового правила

Markdown-it строит процесс обработки Markdown на цепочке правил. Правила бывают двух типов:

  1. Block rules — для блочных элементов (paragraph, heading, blockquote).
  2. Inline rules — для встроенных элементов (strong, em, link).

Добавление собственного правила осуществляется через API:

md.inline.ruler.before('emphasis', 'my_token', myRule);
  • 'emphasis' — существующее правило, перед которым вставляется новое.
  • 'my_token' — уникальное имя правила.
  • myRule — функция, реализующая логику разбора.

Пример создания собственного inline-токена

Рассмотрим создание токена для кастомного маркера %%text%%, который будет преобразовываться в <mark>text</mark>.

function myRule(state, silent) {
  let pos = state.pos;
  let max = state.posMax;

  if (state.src[pos] !== '%' || state.src[pos + 1] !== '%') return false;

  let start = pos + 2;
  let end = state.src.indexOf('%%', start);
  if (end === -1) return false;

  if (!silent) {
    let token = state.push('mark_open', 'mark', 1);
    token.markup = '%%';

    let textToken = state.push('text', '', 0);
    textToken.content = state.src.slice(start, end);

    state.push('mark_close', 'mark', -1);
  }

  state.pos = end + 2;
  return true;
}

md.inline.ruler.before('emphasis', 'mark', myRule);

Объяснение кода:

  • state.src — исходная строка Markdown.
  • state.pos — текущая позиция парсинга.
  • silent — режим “проверки”, не создающий токены (важно для синтаксиса).
  • state.push() — метод создания нового токена. Передается тип токена, HTML-тег и уровень вложенности.

После этого парсер будет преобразовывать %%highlight%% в:

<mark>highlight</mark>

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

Для блочных элементов используется block parser. Например, для создания специального блока :::note:

function noteBlock(state, startLine, endLine, silent) {
  let pos = state.bMarks[startLine] + state.tShift[startLine];
  let max = state.eMarks[startLine];

  if (state.src.slice(pos, pos + 5) !== ':::note') return false;

  if (silent) return true;

  let nextLine = startLine + 1;
  while (nextLine < endLine) {
    if (state.src.slice(state.bMarks[nextLine], state.eMarks[nextLine]).trim() === ':::') break;
    nextLine++;
  }

  let token = state.push('note_open', 'div', 1);
  token.attrs = [['class', 'note']];
  token.map = [startLine, nextLine];

  let contentToken = state.push('inline', '', 0);
  contentToken.content = state.getLines(startLine + 1, nextLine, state.tShift[startLine + 1], true);
  contentToken.children = [];

  state.push('note_close', 'div', -1);

  state.line = nextLine + 1;
  return true;
}

md.block.ruler.before('fence', 'note', noteBlock, { alt: [] });

Особенности блочного парсинга:

  • startLine и endLine — номера строк для текущего блока.
  • state.getLines() — позволяет получить содержимое блока.
  • Токены создаются вложенно: открывающий, inline для контента, закрывающий.

Методы работы с токенами

  1. state.push(type, tag, nesting) — основной метод добавления токена.
  2. token.attrs — массив атрибутов для HTML-тега.
  3. token.content — текстовое содержимое токена.
  4. token.children — вложенные токены, важны для inline-разметки.

Сочетание кастомных токенов и рендереров

Markdown-it поддерживает расширяемые рендереры:

md.renderer.rules.mark_open = () => '<mark>';
md.renderer.rules.mark_close = () => '</mark>';

md.renderer.rules.note_open = (tokens, idx) => '<div class="note">';
md.renderer.rules.note_close = () => '</div>';

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


Рекомендации при создании токенов

  • Всегда проверять silent режим.
  • Убедиться, что state.pos или state.line корректно обновляются.
  • Вложенные токены создаются через children или отдельные push’и.
  • Для блочных элементов учитывать границы строк и возможные пустые линии.

Интеграция с существующими правилами

Кастомные токены можно вставлять до или после существующих правил. Это важно для корректного парсинга Markdown, чтобы новые токены не конфликтовали с встроенными элементами (em, strong, link).

md.inline.ruler.after('emphasis', 'my_token', myRule);
md.block.ruler.before('fence', 'my_block', myBlockRule);

Использование before и after позволяет точно позиционировать парсинг нового синтаксиса.


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