Сериализация HAST в HTML строку

HAST (Hypertext Abstract Syntax Tree) представляет собой абстрактное дерево, описывающее структуру HTML-документа на уровне синтаксиса, независимо от текстового представления. В экосистеме JavaScript HAST активно используется вместе с библиотекой Rehype, позволяя работать с HTML как с объектной моделью и преобразовывать её обратно в строку.


Основные функции Rehype для сериализации

Для преобразования HAST в HTML применяются два ключевых пакета:

  1. rehype-stringify – основной модуль для сериализации HAST.
  2. unified – движок обработки AST, позволяющий строить цепочки обработки.

Простейший пример сериализации выглядит следующим образом:

import { unified } from 'unified';
import rehypeStringify from 'rehype-stringify';

const hast = {
  type: 'root',
  children: [
    {
      type: 'element',
      tagName: 'p',
      properties: {},
      children: [
        { type: 'text', value: 'Пример текста в параграфе.' }
      ]
    }
  ]
};

const html = unified()
  .use(rehypeStringify)
  .stringify(hast);

console.log(html); // <p>Пример текста в параграфе.</p>

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

  • type: 'root' обязательно для корневого узла HAST.
  • children содержит элементы типа element или text.
  • rehypeStringify превращает AST обратно в HTML.

Настройка сериализации

Библиотека rehype-stringify поддерживает множество опций:

  1. allowDangerousHtml – разрешает вставку необработанного HTML из HAST, предотвращая экранирование.
  2. closeSelfClosing – контролирует формат самозакрывающихся тегов (<img /> или <img>).
  3. entities – настраивает обработку HTML-сущностей (&amp;, &lt; и т.д.).

Пример с кастомными настройками:

const html = unified()
  .use(rehypeStringify, { allowDangerousHtml: true, closeSelfClosing: false })
  .stringify(hast);

Преобразование сложных структур

HAST может описывать многоуровневые структуры с вложенными элементами, атрибутами и комментариями. Важные моменты:

  • Атрибуты передаются через поле properties.

  • Комментарии и инструкции можно включать через типы comment и doctype.

  • Для обработки стилей и классов используются объекты в properties:

    {
      type: 'element',
      tagName: 'div',
      properties: { className: ['container'], id: 'main' },
      children: []
    }

Пример сериализации сложного дерева:

const complexHast = {
  type: 'root',
  children: [
    { type: 'doctype', name: 'html' },
    {
      type: 'element',
      tagName: 'html',
      properties: {},
      children: [
        {
          type: 'element',
          tagName: 'head',
          properties: {},
          children: [
            { type: 'element', tagName: 'title', properties: {}, children: [{ type: 'text', value: 'Документ' }] }
          ]
        },
        {
          type: 'element',
          tagName: 'body',
          properties: {},
          children: [
            {
              type: 'element',
              tagName: 'h1',
              properties: {},
              children: [{ type: 'text', value: 'Заголовок уровня 1' }]
            },
            {
              type: 'element',
              tagName: 'p',
              properties: { className: ['lead'] },
              children: [{ type: 'text', value: 'Абзац с классом lead.' }]
            }
          ]
        }
      ]
    }
  ]
};

После сериализации rehype-stringify создаст корректный HTML с учётом всех атрибутов, тегов и структуры.


Производительность и оптимизация

  • Минимизация узлов: уменьшение количества text узлов может ускорить сериализацию.
  • Переиспользование AST: при генерации нескольких HTML-страниц с похожей структурой можно модифицировать существующее дерево вместо создания нового.
  • Пакет hast-util-to-html: альтернативный инструмент для конвертации HAST в строку, предлагающий тонкую настройку вывода и меньший вес, чем полный rehype.

Пример использования hast-util-to-html:

import { toHtml } from 'hast-util-to-html';

const htmlString = toHtml(hast, { allowDangerousHtml: true });

Работа с небезопасным HTML

HAST может содержать небезопасные фрагменты. Чтобы предотвратить XSS:

  • Использовать sanitize перед сериализацией.
  • Применять rehype-sanitize вместе с rehype-stringify.
  • Ограничивать allowDangerousHtml только проверенными блоками.

Пример цепочки:

import rehypeSanitize from 'rehype-sanitize';

const safeHtml = unified()
  .use(rehypeSanitize)
  .use(rehypeStringify)
  .stringify(hast);

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

HAST часто получается из MDAST (Markdown AST) через remark-rehype:

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

const markdown = '# Заголовок\n\nТекст абзаца.';

const html = unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeStringify)
  .processSync(markdown)
  .toString();

Здесь remark-rehype конвертирует Markdown AST в HAST, а rehype-stringify сериализует его в HTML строку. Такой подход позволяет полностью контролировать процесс преобразования от Markdown до HTML, включая обработку атрибутов, классов и вложенных элементов.


Итоговые рекомендации по сериализации

  • Всегда проверять корректность структуры HAST перед вызовом stringify.
  • Использовать безопасные цепочки при работе с внешним контентом.
  • Кастомизировать вывод через опции rehype-stringify для точного соответствия требованиям HTML.
  • Оптимизировать AST, чтобы снизить нагрузку при генерации больших документов.