Навигация и поиск

Remark и Rehype — это мощные инструменты для анализа, трансформации и генерации Markdown и HTML в экосистеме JavaScript. Remark специализируется на Markdown, предоставляя удобные способы чтения, изменения и генерации документов. Rehype фокусируется на HTML, обеспечивая работу с деревом узлов HTML (AST) аналогично тому, как Remark работает с Markdown.


Структура AST и её роль

Обе библиотеки используют концепцию Abstract Syntax Tree (AST). AST представляет собой древовидную структуру, где каждый узел описывает элемент документа:

  • type — тип узла (heading, paragraph, link, text и т.д.).
  • children — массив дочерних узлов, если элемент содержит вложенные структуры.
  • value — текстовое содержание узла (для текстовых узлов).

Пример простого Markdown:

# Заголовок 1
Текст абзаца с [ссылкой](https://example.com)

AST через Remark будет выглядеть как:

{
  "type": "root",
  "children": [
    {
      "type": "heading",
      "depth": 1,
      "children": [
        { "type": "text", "value": "Заголовок 1" }
      ]
    },
    {
      "type": "paragraph",
      "children": [
        { "type": "text", "value": "Текст абзаца с " },
        {
          "type": "link",
          "url": "https://example.com",
          "children": [{ "type": "text", "value": "ссылкой" }]
        }
      ]
    }
  ]
}

Навигация по AST

AST позволяет извлекать информацию и изменять документ программно. Основные методы навигации:

  1. Рекурсивный обход дерева

    function traverse(node) {
      console.log(node.type);
      if (node.children) {
        node.children.forEach(traverse);
      }
    }
    traverse(ast);

    Рекурсивная функция обходит каждый узел, печатая его тип.

  2. Использование unist-util-visit Библиотека unist-util-visit упрощает поиск узлов нужного типа:

    import { visit } from 'unist-util-visit';
    
    visit(ast, 'heading', node => {
      console.log(node.depth, node.children[0].value);
    });

Поиск и фильтрация контента

Remark и Rehype позволяют искать узлы по типу, значению или атрибутам.

  • Поиск по типу:

    visit(ast, 'link', node => console.log(node.url));
  • Фильтрация с условием:

    const externalLinks = [];
    visit(ast, 'link', node => {
      if (node.url.startsWith('http')) {
        externalLinks.push(node.url);
      }
    });
  • Извлечение текста заголовков:

    const headings = [];
    visit(ast, 'heading', node => {
      const text = node.children.map(child => child.value).join('');
      headings.push({ depth: node.depth, text });
    });

Манипуляция контентом

Добавление узлов:

ast.children.push({
  type: 'paragraph',
  children: [{ type: 'text', value: 'Новый абзац добавлен программно' }]
});

Замена узлов:

visit(ast, 'heading', node => {
  if (node.depth === 2) {
    node.children = [{ type: 'text', value: 'Обновленный заголовок' }];
  }
});

Удаление узлов через unist-util-remove:

import { remove } from 'unist-util-remove';

remove(ast, node => node.type === 'link' && node.url.includes('spam'));

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

После обработки Markdown через Remark можно преобразовать AST в HTML с помощью remark-rehype:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';

const html = await unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeStringify)
  .process('# Заголовок и [ссылка](https://example.com)');

console.log(html.toString());

AST Rehype похож на DOM, что позволяет поиск и навигацию по HTML-узлам:

  • tagName — имя HTML-тега.
  • properties — атрибуты (className, href, id).
  • children — вложенные элементы.

Пример поиска всех ссылок в Rehype:

visit(ast, 'element', node => {
  if (node.tagName === 'a') {
    console.log(node.properties.href);
  }
});

Поиск и замена HTML-атрибутов

С помощью Rehype можно выполнять массовое обновление атрибутов:

visit(ast, 'element', node => {
  if (node.tagName === 'img') {
    node.properties.loading = 'lazy';
  }
});

Это полезно для оптимизации веб-страниц и добавления SEO-атрибутов.


Построение навигации

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

const toc = [];
visit(ast, 'heading', node => {
  const text = node.children.map(c => c.value).join('');
  const id = text.toLowerCase().replace(/\s+/g, '-');
  toc.push({ id, text, depth: node.depth });
});

Результат можно использовать для генерации боковой панели или внутренней навигации.


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

  • Использовать unist-util-visit для целенаправленного обхода вместо ручной рекурсии.
  • Разделять обработку Markdown и HTML: сначала Remark → AST → Rehype → HTML.
  • Для сложных трансформаций применять unist-util-visit-parents, чтобы иметь доступ к родительским узлам.
  • Создавать функции-утилиты для извлечения текста, ссылок и заголовков для повторного использования в проектах.

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