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++;
});
};
}
Такой подход позволяет безопасно хранить состояния нескольких плагинов в одном документе, не мешая друг другу.
Иногда один плагин зависит от результатов другого. В этом случае важно:
use:remark()
.use(headingTracker)
.use(anotherPlugin) // использует headingTracker
.process(markdown);
function dependentPlugin() {
return (tree, file) => {
const tracker = file.data.plugins?.headingTracker;
if (!tracker) {
throw new Error('headingTracker должен быть вызван перед этим плагином');
}
};
}
Иногда вместо file.data имеет смысл сохранять информацию
на уровне узлов AST. Например, пометить определённые
заголовки:
visit(tree, 'heading', (node) => {
node.myCustomFlag = true;
});
Следующие плагины могут фильтровать или изменять только помеченные узлы:
visit(tree, 'heading', (node) => {
if (node.myCustomFlag) {
node.children.push({ type: 'text', value: ' (обновлено)' });
}
});
Этот метод полезен, когда данные относятся не к документу целиком, а к отдельным элементам.
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 };
};
}
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, демонстрируя эффективное использование контекста между плагинами.
file.data для глобальных данных документа.Контекст между плагинами является ключевым механизмом для построения модульных и расширяемых цепочек обработки в Remark и Rehype, обеспечивая согласованность данных и возможность сложных трансформаций Markdown и HTML без конфликтов между плагинами.