Шаблонизация с использованием HAST

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

Основные типы узлов HAST:

  • Root – корневой узел документа.
  • Element – HTML-элемент с тегом, атрибутами и дочерними узлами.
  • Text – текстовый контент.
  • Comment – HTML-комментарий.
  • Doctype – декларация типа документа.

Каждый узел HAST имеет ключевое свойство type, а элементы дополнительно содержат tagName, properties (для атрибутов) и children.


Работа с HAST через Rehype

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

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

const html = '<div><p>Hello World</p></div>';

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

console.log(tree);

В этом примере HTML превращается в дерево HAST, где каждый тег <div> и <p> представлен как Element, а текст «Hello World» – как Text.

Для генерации HTML обратно используется rehypeStringify:

const output = unified()
  .use(rehypeParse, { fragment: true })
  .use(rehypeStringify)
  .processSync(html)
  .toString();

console.log(output); // '<div><p>Hello World</p></div>'

Шаблонизация с HAST

HAST можно рассматривать как промежуточный формат, подходящий для шаблонизации. Основная идея: создать дерево, заменить в нём узлы или свойства на динамические данные, а затем сгенерировать HTML.

Пример: динамическая подстановка текста

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

const template = '<ul><li></li><li></li><li></li></ul>';
const items = ['Apple', 'Banana', 'Cherry'];

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

let index = 0;
visit(tree, 'element', node => {
  if (node.tagName === 'li' && items[index]) {
    node.children = [{ type: 'text', value: items[index] }];
    index++;
  }
});

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

console.log(output);
// '<ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>'

Здесь visit из unist-util-visit используется для обхода дерева HAST и подстановки динамического содержимого в узлы <li>.


Работа с атрибутами и свойствами элементов

HAST позволяет управлять атрибутами напрямую через объект properties. Например, можно динамически добавлять классы, id или другие атрибуты:

visit(tree, 'element', node => {
  if (node.tagName === 'div') {
    node.properties = node.properties || {};
    node.properties.className = ['container', 'theme-light'];
    node.properties.id = 'main-container';
  }
});

Особенности:

  • Свойство className в HAST хранится как массив.
  • Любые HTML-атрибуты могут быть заданы через объект properties.
  • Для вложенных элементов изменения выполняются рекурсивно через обход дерева.

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

Remark и Rehype часто используются вместе для конвейеров преобразования Markdown в HTML с шаблонизацией. Сначала Markdown превращается в MDAST, затем через remark-rehype преобразуется в HAST, где можно производить любые модификации перед генерацией HTML.

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

const markdown = '# Заголовок\n- Пункт 1\n- Пункт 2';

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

console.log(html);
// '<h1>Заголовок</h1><ul><li>Пункт 1</li><li>Пункт 2</li></ul>'

На этапе HAST можно вставлять дополнительные атрибуты, классы, или динамически изменять содержимое элементов списка.


Шаблонизация сложных структур

Для более сложных сценариев используется создание функций-шаблонов, которые строят HAST «на лету»:

function createCard(title, content) {
  return {
    type: 'element',
    tagName: 'div',
    properties: { className: ['card'] },
    children: [
      { type: 'element', tagName: 'h2', children: [{ type: 'text', value: title }] },
      { type: 'element', tagName: 'p', children: [{ type: 'text', value: content }] }
    ]
  };
}

const cards = [
  createCard('Card 1', 'Content 1'),
  createCard('Card 2', 'Content 2')
];

const root = { type: 'root', children: cards };
const htmlOutput = unified().use(rehypeStringify).stringify(root);

console.log(htmlOutput);
// '<div class="card"><h2>Card 1</h2><p>Content 1</p></div><div class="card"><h2>Card 2</h2><p>Content 2</p></div>'

Такой подход позволяет строить динамический HTML без промежуточного шаблонного синтаксиса вроде Handlebars или EJS, используя исключительно дерево HAST.


Обход и трансформация дерева

HAST удобно трансформировать с помощью утилит вроде unist-util-visit и unist-util-map:

  • visit(tree, type, callback) – обход узлов указанного типа.
  • map(tree, callback) – возвращает новый трансформированный объект дерева.

Пример добавления атрибута data-index ко всем <li>:

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

const indexedTree = map(tree, (node, index) => {
  if (node.type === 'element' && node.tagName === 'li') {
    node.properties = node.properties || {};
    node.properties['data-index'] = index;
  }
  return node;
});

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