Модификация токенов перед рендерингом

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


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

Каждый токен — это объект с набором полей:

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

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

{
  type: 'heading',
  raw: '### Пример заголовка',
  text: 'Пример заголовка',
  depth: 3,
  tokens: [ /* вложенные токены */ ]
}

Преобразование Markdown в токены

Для модификации токенов необходимо сначала преобразовать Markdown в массив токенов с помощью метода marked.lexer():

import { marked } from 'marked';

const markdown = `
# Заголовок 1
Текст параграфа с [ссылкой](https://example.com)
`;

const tokens = marked.lexer(markdown);
console.log(tokens);

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


Изменение токенов

Модификация токенов может быть полезна для:

  • Автоматического добавления классов к элементам
  • Замены ссылок или текста
  • Обогащения контента метаданными
  • Преобразования определённых типов элементов в кастомные HTML-блоки

Пример добавления атрибута class к заголовкам:

tokens.forEach(token => {
  if (token.type === 'heading') {
    token.text = `<span class="custom-heading">${token.text}</span>`;
  }
});

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

function modifyTokens(tokens) {
  tokens.forEach(token => {
    if (token.type === 'heading') {
      token.text = `<span class="custom-heading">${token.text}</span>`;
    }
    if (token.tokens) {
      modifyTokens(token.tokens);
    }
  });
}

modifyTokens(tokens);

Кастомные рендереры

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

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

const renderer = {
  heading(token) {
    return `<h${token.depth} class="custom-heading">${token.text}</h${token.depth}>`;
  }
};

const html = marked.parser(tokens, { renderer });
console.log(html);

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


Динамическая обработка ссылок и изображений

Токены ссылок и изображений можно изменять для внедрения динамических URL, добавления атрибутов target или rel:

tokens.forEach(token => {
  if (token.type === 'link') {
    token.href = token.href.replace('http://', 'https://');
    token.title = token.title || 'Динамическая ссылка';
  }
  if (token.tokens) modifyTokens(token.tokens);
});

Для изображений можно автоматически добавлять alt или класс:

tokens.forEach(token => {
  if (token.type === 'image') {
    token.text = token.text || 'Изображение';
    token.href = token.href;
    token.title = token.title || 'Изображение с подписью';
  }
});

Использование событийных хуков

Marked поддерживает hook-функции, которые позволяют вмешиваться в процесс парсинга и рендеринга. На уровне токенов полезны хуки:

  • walkTokens — вызывается для каждого токена после лексера, но до рендеринга.

Пример:

marked.use({
  walkTokens(token) {
    if (token.type === 'text') {
      token.text = token.text.replace(/\bJavaScript\b/g, '<strong>JavaScript</strong>');
    }
  }
});

Хук walkTokens особенно удобен для глобальных изменений текста или автоматического обогащения Markdown без необходимости рекурсивно обходить массив токенов вручную.


Советы по производительности

  • Избегать многократного рекурсивного обхода одного и того же массива токенов.
  • Для больших документов использовать walkTokens, чтобы обрабатывать токены в один проход.
  • Модифицировать только необходимые поля, чтобы не нарушить структуру токена.
  • Кастомные рендереры использовать совместно с модификацией токенов для минимизации двойной обработки текста.

Итоговая схема работы

  1. Лексинг: marked.lexer(markdown) — преобразование Markdown в токены.
  2. Модификация токенов: изменение полей text, href, tokens и др. через рекурсию или walkTokens.
  3. Рендеринг: marked.parser(tokens, { renderer }) — генерация HTML с кастомной логикой для каждого типа токена.

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