Контекст между плагинами

Remark и Rehype строятся на архитектуре плагинов, где каждый плагин получает AST (Abstract Syntax Tree) документа, модифицирует его и передаёт следующему плагину. Понимание того, как поддерживать и использовать контекст между плагинами, критично для создания сложных и согласованных обработок Markdown и HTML.

Передача данных через контекст

Каждый плагин в Remark и Rehype вызывается с двумя основными аргументами:

function myPlugin(options) {
  return (tree, file) => {
    // работа с деревом и файлом
  };
}
  • tree — AST текущего документа.
  • file — объект типа VFile, который содержит информацию о текущем файле, в том числе путь, содержимое и метаданные.

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

file.data.myPluginData = { headingCount: 0 };

Другие плагины могут затем получить доступ к этим данным:

function anotherPlugin() {
  return (tree, file) => {
    const data = file.data.myPluginData;
    console.log('Количество заголовков, найденных ранее:', data.headingCount);
  };
}

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

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

file.data.plugins = {
  headingTracker: { count: 0 },
  linkCollector: { urls: [] },
};

Плагины могут работать с собственными объектами:

function headingTracker() {
  return (tree, file) => {
    const tracker = file.data.plugins.headingTracker;
    visit(tree, 'heading', () => {
      tracker.count++;
    });
  };
}

Такой подход позволяет безопасно хранить состояния нескольких плагинов в одном документе, не мешая друг другу.

Использование плагинов с опциональной зависимостью

Иногда один плагин зависит от результатов другого. В этом случае важно:

  1. Задать порядок плагинов при использовании use:
remark()
  .use(headingTracker)
  .use(anotherPlugin) // использует headingTracker
  .process(markdown);
  1. Проверять наличие данных перед использованием:
function dependentPlugin() {
  return (tree, file) => {
    const tracker = file.data.plugins?.headingTracker;
    if (!tracker) {
      throw new Error('headingTracker должен быть вызван перед этим плагином');
    }
  };
}

Сохранение промежуточных результатов в AST

Иногда вместо file.data имеет смысл сохранять информацию на уровне узлов AST. Например, пометить определённые заголовки:

visit(tree, 'heading', (node) => {
  node.myCustomFlag = true;
});

Следующие плагины могут фильтровать или изменять только помеченные узлы:

visit(tree, 'heading', (node) => {
  if (node.myCustomFlag) {
    node.children.push({ type: 'text', value: ' (обновлено)' });
  }
});

Этот метод полезен, когда данные относятся не к документу целиком, а к отдельным элементам.

Пример комплексного использования

  1. Подсчёт всех заголовков:
function headingCounter() {
  return (tree, file) => {
    const headings = [];
    visit(tree, 'heading', (node) => {
      headings.push(node);
    });
    file.data.plugins = file.data.plugins || {};
    file.data.plugins.headingCounter = { headings };
  };
}
  1. Добавление нумерации к заголовкам на основе предыдущего подсчёта:
function headingNumbering() {
  return (tree, file) => {
    const headings = file.data.plugins?.headingCounter?.headings;
    if (!headings) return;

    headings.forEach((node, index) => {
      node.children.unshift({
        type: 'text',
        value: `${index + 1}. `
      });
    });
  };
}

Здесь один плагин собирает данные, второй использует их для модификации AST, демонстрируя эффективное использование контекста между плагинами.

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

  • Всегда инициализировать пространство данных для каждого плагина. Это предотвращает ошибки при использовании нескольких плагинов.
  • Использовать AST для узконаправленных данных, file.data для глобальных данных документа.
  • Учитывать порядок плагинов: данные должны быть созданы до их использования.
  • Избегать мутаций в чужих плагинах, использовать собственные пространства имён.
  • Документировать ключи и структуры, чтобы при расширении проекта новые плагины могли безопасно обращаться к существующим данным.

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