HAST: спецификация HTML дерева

HAST (HTML Abstract Syntax Tree) — это абстрактное синтаксическое дерево для HTML-документов, которое используется в экосистеме JavaScript, включая библиотеки Rehype и Remark, для структурированной работы с HTML-контентом. HAST позволяет представить HTML в виде дерева узлов, где каждый узел соответствует элементу, атрибуту или текстовому фрагменту, облегчая анализ, трансформацию и генерацию HTML.


Структура узлов HAST

Узлы HAST имеют тип Node, который подразделяется на несколько ключевых категорий:

  1. Root Представляет корень документа. Содержит массив дочерних узлов в свойстве children. Пример структуры:

    {
      type: 'root',
      children: [/* массив узлов */]
    }
  2. Element Основной узел для HTML-элементов. Содержит следующие свойства:

    • type: 'element'
    • tagName: имя HTML-тега ('div', 'p', 'span')
    • properties: объект с атрибутами элемента
    • children: массив дочерних узлов
    {
      type: 'element',
      tagName: 'a',
      properties: { href: 'https://example.com' },
      children: [
        { type: 'text', value: 'Example' }
      ]
    }
  3. Text Узел для текстового содержимого. Содержит свойство value с текстовой строкой.

    {
      type: 'text',
      value: 'Hello, world!'
    }
  4. Comment Узел для HTML-комментариев.

    {
      type: 'comment',
      value: 'This is a comment'
    }
  5. Doctype Узел для объявления <!DOCTYPE>.

    {
      type: 'doctype',
      name: 'html'
    }

Свойства элементов и узлов

HAST использует унифицированный способ описания атрибутов и свойств:

  • properties — объект, где ключи соответствуют именам атрибутов HTML, а значения отражают их содержимое.
  • children — массив дочерних узлов, что позволяет строить вложенную структуру DOM.

Важно учитывать, что атрибуты могут иметь типы, отличные от строки: например, для className используется массив строк, для булевых атрибутов — true/false.

{
  type: 'element',
  tagName: 'input',
  properties: { type: 'checkbox', checked: true },
  children: []
}

Преобразование HTML в HAST

Для работы с HAST часто используют библиотеку Rehype, которая предоставляет парсер и трансформеры:

import { unified } from 'unified';
import rehypeParse from 'rehype-parse';

const html = '<p>Hello <strong>world</strong></p>';

const tree = unified()
  .use(rehypeParse, { fragment: true })
  .parse(html);

console.log(tree);

В результате HTML <p>Hello <strong>world</strong></p> превращается в дерево:

{
  type: 'root',
  children: [
    {
      type: 'element',
      tagName: 'p',
      properties: {},
      children: [
        { type: 'text', value: 'Hello ' },
        {
          type: 'element',
          tagName: 'strong',
          properties: {},
          children: [{ type: 'text', value: 'world' }]
        }
      ]
    }
  ]
}

Трансформация HAST

HAST удобно модифицировать программно. Для этого применяются плагины Rehype, которые обходят дерево и изменяют узлы:

  • Обход дерева с помощью unist-util-visit:
import { visit } from 'unist-util-visit';

visit(tree, 'element', (node) => {
  if (node.tagName === 'strong') {
    node.tagName = 'b';
  }
});

В этом примере все <strong> заменяются на <b>.

  • Добавление новых узлов:
tree.children.push({
  type: 'element',
  tagName: 'p',
  properties: {},
  children: [{ type: 'text', value: 'Новый параграф' }]
});
  • Удаление узлов:
tree.children = tree.children.filter(node => node.type !== 'comment');

Генерация HTML из HAST

После трансформаций дерево можно превратить обратно в HTML с помощью rehype-stringify:

import rehypeStringify from 'rehype-stringify';

const htmlOutput = unified()
  .use(rehypeStringify)
  .stringify(tree);

console.log(htmlOutput);

Результат содержит корректный HTML с учетом всех изменений.


Взаимодействие с MDAST

HAST тесно интегрируется с Markdown через Remark. Markdown сначала преобразуется в MDAST (Markdown AST), затем через remark-rehype конвертируется в HAST, что позволяет выполнять HTML-трансформации на основе исходного Markdown:

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();

console.log(html);

Markdown преобразуется в HAST с элементами <h1> и <p>.


Основные преимущества HAST

  • Универсальность: работает с любым HTML-контентом.
  • Детерминированность структуры: дерево строго определяет вложенность и атрибуты.
  • Совместимость с плагинами: легко интегрируется с Rehype и Remark.
  • Безопасность: позволяет фильтровать или изменять HTML перед выводом.
  • Поддержка программных трансформаций: добавление, удаление, модификация узлов без прямой работы с текстом HTML.

HAST является фундаментальным инструментом для современных инструментов обработки контента в JavaScript, обеспечивая удобный и предсказуемый способ работы с HTML как с программной структурой.