HAST (Hypertext Abstract Syntax Tree) представляет собой абстрактное синтаксическое дерево для HTML-документов, которое используется в библиотеке Rehype для анализа, модификации и генерации HTML. В отличие от MDAST, ориентированного на Markdown, HAST работает на уровне HTML, предоставляя детальную модель документа, где каждый узел представляет собой элемент, текст или атрибут.
Основные типы узлов HAST:
Каждый узел HAST имеет ключевое свойство type, а
элементы дополнительно содержат tagName,
properties (для атрибутов) и children.
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 можно рассматривать как промежуточный формат, подходящий для шаблонизации. Основная идея: создать дерево, заменить в нём узлы или свойства на динамические данные, а затем сгенерировать 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 хранится как массив.properties.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, обеспечивая безопасность и гибкость кода.