Трансформация путей

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


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

  • AST (Abstract Syntax Tree) Remark и Rehype используют дерево синтаксического анализа (AST) для представления документа. Для Markdown используется mdast (Markdown AST), для HTML — hast (HTML AST). В узлах дерева хранятся свойства элементов, включая пути к ресурсам:

    • link.url для ссылок
    • image.url для изображений
  • Плагины Трансформация осуществляется через плагины, которые принимают AST и изменяют его узлы. Плагины могут быть синхронными или асинхронными.


Принципы работы с путями

  1. Абсолютные и относительные пути

    • Относительные пути необходимо преобразовывать в абсолютные, если контент будет публиковаться на сервере или в сборке.
    • Абсолютные пути можно нормализовать для разных платформ (path.resolve, path.join).
  2. Замена ресурсов на CDN или локальные копии

    • Изображения можно переписывать на ссылки CDN для ускорения загрузки.
    • Ссылки на статьи или файлы могут быть динамически перенаправлены на новую структуру папок.
  3. Обход дерева AST

    • В Remark используется unist-util-visit для обхода Markdown AST.
    • В Rehype применяется hast-util-visit для обхода HTML AST. Это позволяет находить все узлы с путями и изменять их программно.

Пример трансформации ссылок в Remark

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import { visit } from 'unist-util-visit';
import path from 'path';

const markdown = `
[Ссылка на файл](./docs/file.md)
![Картинка](./images/photo.png)
`;

const processor = unified()
  .use(remarkParse)
  .use(() => tree => {
    visit(tree, 'link', node => {
      node.url = path.join('/static', node.url);
    });
    visit(tree, 'image', node => {
      node.url = path.join('/static', node.url);
    });
  })
  .use(remarkStringify);

processor.process(markdown).then(file => {
  console.log(String(file));
});

Разбор примера:

  • visit(tree, 'link', callback) — обходит все ссылки (link) в AST.
  • node.url = path.join('/static', node.url) — трансформирует относительный путь в путь внутри папки /static.
  • Аналогично обрабатываются изображения через узлы image.

Преобразование HTML с Rehype

HTML AST строится через hast. Пути к изображениям и ссылкам хранятся в атрибутах src и href.

import { unified } from 'unified';
import rehypeParse from 'rehype-parse';
import rehypeStringify from 'rehype-stringify';
import { visit } from 'hast-util-visit';
import path from 'path';

const html = `
<a href="./docs/page.html">Страница</a>
<img src="./images/photo.png" alt="Фото">
`;

const processor = unified()
  .use(rehypeParse, { fragment: true })
  .use(() => tree => {
    visit(tree, 'element', node => {
      if (node.tagName === 'a' && node.properties.href) {
        node.properties.href = path.join('/static', node.properties.href);
      }
      if (node.tagName === 'img' && node.properties.src) {
        node.properties.src = path.join('/static', node.properties.src);
      }
    });
  })
  .use(rehypeStringify);

processor.process(html).then(file => {
  console.log(String(file));
});

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

  • element.tagName позволяет фильтровать конкретные элементы (a, img).
  • node.properties содержит все атрибуты элемента, включая пути.
  • Любая логика трансформации может быть встроена: добавление префиксов, хеширование файлов, замена на CDN.

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

  • Для больших проектов имеет смысл вынести трансформацию путей в отдельный плагин.
  • Если путь ведет к локальному файлу, проверять его существование через fs.existsSync.
  • Для асинхронной загрузки ресурсов можно использовать async плагин с await.
  • Удобно объединять Remark и Rehype через remark-rehype для конвейера: Markdown → HTML → трансформация путей.

Комбинированная обработка Markdown и HTML

import remarkRehype from 'remark-rehype';

const processor = unified()
  .use(remarkParse)
  .use(() => tree => {
    visit(tree, 'image', node => {
      node.url = `/cdn/images/${path.basename(node.url)}`;
    });
  })
  .use(remarkRehype)
  .use(() => tree => {
    visit(tree, 'element', node => {
      if (node.tagName === 'a' && node.properties.href.startsWith('./')) {
        node.properties.href = `/docs/${node.properties.href.slice(2)}`;
      }
    });
  })
  .use(rehypeStringify);

В этом конвейере Markdown изображения перенаправляются на CDN, а HTML-ссылки преобразуются в абсолютные пути для документации. Такой подход гарантирует единообразие всех путей на выходе.


Итоговые рекомендации

  • Всегда использовать visit для обхода AST, это универсальный метод и для Remark, и для Rehype.
  • Разделять трансформацию изображений и ссылок на разные плагины или функции.
  • Применять path.join и path.resolve для безопасного формирования путей между ОС.
  • Для публикации на CDN или сборке статических сайтов учитывать префиксы и структуру каталогов заранее.

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