Использование метаданных в плагинах

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


Структура и хранение метаданных

Метаданные в Remark и Rehype обычно хранятся в объекте data, ассоциированном с корневым узлом AST (root). В Remark это объект VFile.data, в Rehype — аналогичная структура внутри node.data.

Пример структуры метаданных в Remark:

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

const processor = unified()
  .use(remarkParse)
  .use(() => (tree, file) => {
    file.data.title = "Пример документа";
    file.data.tags = ["javascript", "remark"];
  })
  .use(remarkStringify);

const markdown = "# Заголовок";
const file = processor.processSync(markdown);

console.log(file.data.title); // "Пример документа"

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


Чтение метаданных из Markdown

Markdown-документы часто содержат frontmatter — YAML-блок в начале файла, используемый для хранения метаданных. Remark предоставляет плагин remark-frontmatter и remark-mdx-frontmatter для его разбора.

Пример извлечения метаданных:

import frontmatter from 'remark-frontmatter';
import yaml from 'js-yaml';

unified()
  .use(remarkParse)
  .use(frontmatter, ['yaml'])
  .use(() => (tree, file) => {
    tree.children.forEach(node => {
      if (node.type === 'yaml') {
        const data = yaml.load(node.value);
        file.data.metadata = data;
      }
    });
  });

В этом примере YAML-блок автоматически парсится и сохраняется в file.data.metadata. Такой подход позволяет использовать метаданные в дальнейшем, например, при генерации оглавления или добавлении динамических элементов на основе тегов.


Передача данных между плагинами

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

Пример передачи метаданных между плагинами:

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

const addMetadata = () => (tree, file) => {
  file.data.custom = { processed: true };
};

const useMetadata = () => (tree, file) => {
  if (file.data.custom?.processed) {
    tree.children.push({
      type: 'paragraph',
      children: [{ type: 'text', value: 'Документ обработан с использованием метаданных' }],
    });
  }
};

unified()
  .use(remarkParse)
  .use(addMetadata)
  .use(useMetadata)
  .use(remarkStringify)
  .processSync('# Заголовок');

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


Метаданные узлов AST

Помимо глобальных метаданных, Remark и Rehype позволяют хранить данные непосредственно в узлах AST. Это полезно для анализа конкретных элементов документа, таких как заголовки, ссылки или изображения.

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

const annotateHeadings = () => (tree) => {
  visit(tree, 'heading', node => {
    node.data = node.data || {};
    node.data.level = node.depth;
    node.data.id = node.children.map(c => c.value).join('-').toLowerCase();
  });
};

Метаданные узлов (node.data) могут содержать идентификаторы, уровни заголовков, ссылки на внешние ресурсы или любые другие дополнительные сведения. Это позволяет динамически формировать оглавления, карты ссылок и аннотации.


Стандарты и соглашения

  • Использовать file.data для глобальных метаданных документа.
  • Использовать node.data для локальных метаданных, связанных с конкретными узлами.
  • Метаданные должны быть сериализуемыми объектами, чтобы их можно было легко передавать между плагинами.
  • Если плагин добавляет новые данные, рекомендуется использовать уникальные ключи или namespace, чтобы избежать конфликтов с другими плагинами (file.data.myPlugin = { ... }).

Интеграция с Rehype

Rehype работает аналогично Remark, но с HTML AST (hast). Метаданные в Rehype также хранятся в node.data и могут использоваться для трансформации HTML.

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

const addParagraphData = () => (tree) => {
  visit(tree, 'element', node => {
    if (node.tagName === 'p') {
      node.data = node.data || {};
      node.data.customId = `p-${Math.random().toString(36).substr(2, 5)}`;
    }
  });
};

Такой подход позволяет строить динамические ID для элементов HTML, которые могут использоваться в скриптах, стилях или при генерации интерактивного контента.


Резюме практик

  • Метаданные позволяют обогащать AST без изменения исходного текста.
  • Разделение между глобальными (file.data) и локальными (node.data) данными обеспечивает чистую архитектуру плагинов.
  • Поддержка YAML frontmatter делает Remark удобным для блогов и документации.
  • Сквозная передача данных между плагинами обеспечивает гибкость и расширяемость обработки документов.

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