Кастомные токенизаторы

Remark и Rehype используют систему плагинов для обработки Markdown и HTML. В основе этой системы лежат токенизаторы, которые разбивают текст на синтаксические единицы — токены. Создание кастомного токенизатора позволяет расширять синтаксис Markdown, добавлять новые конструкции и точечно управлять обработкой текста.

Структура токенизатора

Токенизатор — это функция, которая получает текущую позицию в исходном тексте и возвращает объект с распознанным узлом (node) и количеством обработанных символов. Стандартный интерфейс токенизатора в Remark выглядит следующим образом:

function myTokenizer(eat, value, silent) {
    // eat — функция для "поглощения" текста
    // value — текущий фрагмент текста
    // silent — режим проверки без создания узла
}

Ключевые моменты работы токенизатора:

  • eat(text) — создает узел с текстом, который токенизатор распознал, и продвигает курсор.
  • silent — если true, токенизатор должен только проверять возможность распознавания, не создавая узел.
  • Возвращаемое значение токенизатора — объект типа Node или undefined, если совпадение не найдено.

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

Рассмотрим токенизатор, который обрабатывает кастомный синтаксис ==выделение== для подсветки текста:

function highlightTokenizer(eat, value, silent) {
    const match = /^==(.+?)==/.exec(value);
    if (!match) return;

    if (silent) return true;

    return eat(match[0])({
        type: 'highlight',
        value: match[1]
    });
}

highlightTokenizer.locator = function(value, fromIndex) {
    return value.indexOf('==', fromIndex);
}

Здесь:

  • Регулярное выражение ищет текст между двойными ==.
  • locator ускоряет поиск позиции начала токена, что улучшает производительность при больших файлах.
  • Возвращаемый узел имеет тип highlight, который можно затем обрабатывать через плагин или рендерить в HTML с помощью Rehype.

Регистрация токенизатора в Remark

После создания токенизатора его нужно интегрировать в парсер:

import { unified } from 'unified';
import remarkParse from 'remark-parse';

const processor = unified()
    .use(remarkParse)
    .use(() => (tree, file) => {
        // Пример плагина для добавления токенизатора
        const Parser = this.Parser;
        const tokenizers = Parser.prototype.inlineTokenizers;
        const methods = Parser.prototype.inlineMethods;

        tokenizers.highlight = highlightTokenizer;
        methods.splice(methods.indexOf('text'), 0, 'highlight');
    });

Ключевые моменты:

  • inlineTokenizers — объект, где ключи — имена токенизаторов, значения — функции.
  • inlineMethods — массив методов, определяющий порядок обработки токенов. Новые токенизаторы должны быть вставлены перед или после существующих методов в зависимости от приоритета.

Композиция нескольких токенизаторов

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

tokenizers.emoji = emojiTokenizer;
methods.splice(methods.indexOf('text'), 0, 'emoji');

Принципы работы с Rehype

Rehype работает аналогично, но оперирует HTML-деревом. Кастомные токенизаторы здесь чаще реализуются как трансформеры, которые обходят дерево узлов (hast) и модифицируют их. Пример токенизатора для Rehype:

function rehypeHighlight() {
    return (tree) => {
        visit(tree, 'element', (node) => {
            if (node.tagName === 'mark') {
                node.tagName = 'span';
                node.properties = { className: ['highlight'] };
            }
        });
    };
}
  • visit из пакета unist-util-visit используется для обхода дерева.
  • Трансформация выполняется по типу узла (element) и тегу (tagName).

Практические рекомендации

  1. Использовать locator для производительности — поиск позиции начала токена значительно ускоряет обработку больших файлов.
  2. Сохранять совместимость с Markdown — не заменять стандартные токенизаторы полностью, лучше расширять их.
  3. Тестировать silent режим — он позволяет проверить распознавание без модификации дерева.
  4. Следить за порядком в inlineMethods — приоритет токенизаторов влияет на то, какой паттерн будет обработан первым.
  5. Разделять токенизаторы для разной семантики — облегчает поддержку и интеграцию с Rehype для последующего рендеринга.

Сложные сценарии

  • Многострочные конструкции: токенизатор должен уметь учитывать символы новой строки.
  • Вложенные токены: нужно корректно продвигать курсор и возвращать вложенные узлы.
  • Совместная работа с плагинами: при интеграции с другими Remark/ Rehype плагинами важно сохранять корректную последовательность токенизации.

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