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

Основные концепции

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

Процесс преобразования можно разделить на два этапа:

  1. Лексический анализ — текст разбивается на токены с помощью лексера (marked.lexer).
  2. Рендеринг — токены конвертируются в HTML с использованием рендерера (marked.Renderer).

Лексический анализ

Метод marked.lexer принимает строку с Markdown и возвращает массив токенов. Каждый токен имеет тип (type) и набор свойств, зависящих от типа:

const marked = require('marked');

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

- Пункт списка
- Еще один пункт
`;

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

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

{
  "type": "heading",
  "depth": 1,
  "text": "Заголовок 1"
}

Токен списка может выглядеть так:

{
  "type": "list",
  "ordered": false,
  "items": [
    { "type": "list_item", "text": "Пункт списка" },
    { "type": "list_item", "text": "Еще один пункт" }
  ]
}

Каждый токен может содержать вложенные токены, например, списки внутри списков или параграфы внутри блоков цитаты.

Рендеринг токенов в HTML

После получения массива токенов их нужно превратить в HTML. Для этого используется рендерер:

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

Метод marked.parser проходит по всем токенам и вызывает соответствующие методы рендерера для каждого типа токена:

  • renderer.heading(text, level, raw) — генерирует HTML для заголовка.
  • renderer.paragraph(text) — для параграфа.
  • renderer.list(body, ordered, start) — для списков.
  • renderer.listitem(text) — для элемента списка.
  • renderer.code(code, language, isEscaped) — для блока кода.
  • renderer.blockquote(quote) — для цитат.
  • renderer.link(href, title, text) — для ссылок.
  • renderer.image(href, title, text) — для изображений.

Каждый метод рендерера может быть переопределён для кастомного HTML. Например, можно изменить разметку заголовков или добавить классы для стилей:

const customRenderer = new marked.Renderer();

customRenderer.heading = (text, level) => {
  return `<h${level} class="custom-heading">${text}</h${level}>`;
};

const customHtml = marked.parser(tokens, { renderer: customRenderer });

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

Токены могут быть вложенными. Например, элемент списка может содержать параграф и подсписок:

{
  "type": "list_item",
  "text": "Пункт с подсписком",
  "tokens": [
    { "type": "paragraph", "text": "Подпункт текста" },
    {
      "type": "list",
      "ordered": false,
      "items": [
        { "type": "list_item", "text": "Вложенный пункт" }
      ]
    }
  ]
}

При рендеринге вложенных токенов marked.parser рекурсивно вызывает рендерер для всех внутренних токенов, что позволяет получать корректный HTML для сложных структур Markdown.

Кастомизация процесса рендеринга

Помимо изменения методов рендерера, библиотека Marked поддерживает следующие возможности:

  • Фильтрация токенов: перед передачей их в парсер можно модифицировать или удалить определённые типы токенов.
  • Добавление атрибутов HTML: через переопределение методов рендерера можно добавлять классы, идентификаторы и другие атрибуты.
  • Обработка нестандартных блоков: можно создавать новые типы токенов и соответствующие методы рендерера для генерации специального HTML.

Пример добавления класса к каждому параграфу:

const paragraphRenderer = new marked.Renderer();

paragraphRenderer.paragraph = text => {
  return `<p class="custom-paragraph">${text}</p>`;
};

const htmlWithCustomParagraphs = marked.parser(tokens, { renderer: paragraphRenderer });

Асинхронный рендеринг

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

const asyncRenderer = new marked.Renderer();

asyncRenderer.code = async (code, language) => {
  const highlighted = await highlightCodeAsync(code, language);
  return `<pre><code>${highlighted}</code></pre>`;
};

const htmlAsync = await marked.parser(tokens, { renderer: asyncRenderer, async: true });

Асинхронный рендеринг особенно полезен при интеграции с внешними сервисами, например для подсветки синтаксиса кода или генерации динамического контента внутри Markdown.

Практические советы

  • Всегда использовать marked.lexer перед marked.parser, если требуется точечная обработка токенов.
  • Для сложных кастомизаций рендерера рекомендуется создавать отдельные функции для каждого типа токена.
  • Проверять вложенные токены, особенно для списков и блоков цитаты, чтобы избежать потери структуры при генерации HTML.
  • Асинхронный рендеринг открывает возможности интеграции с библиотеками для синтаксиса, трансформаций и динамического контента, но требует передачи { async: true } в marked.parser.

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