Модель данных: BEMJSON
Любая страница в nuckty — дерево узлов. Тип узла — BemNode; дерево в целом — BEMTree. Один и тот же движок обрабатывает всё дерево целиком: от абзаца в Markdown до обёртки документа HTML <html>. Почему дерево, а не строки, объясняет решение о модели данных.
Три понятия из БЭМ
Модель наследует терминологию БЭМ — методологии, которая описывает интерфейс через именованные сущности трёх видов:
- Блок — самостоятельный компонент: меню, кнопка, статья.
- Элемент — часть блока, которая вне него не существует: пункт меню, заголовок статьи.
- Модификатор — вариант блока или элемента: размер, тема, состояние.
Из этих трёх понятий движок собирает классы CSS (menu, menu__item, menu_size_m) и по ним же находит нужные шаблоны. Дальше слова «блок», «элемент» и «модификатор» встречаются постоянно.
Узел BemNode
Узел — типизированный объект. При статической проверке TypeScript сверяет его поля с BemNode. CLI преобразует конфигурацию, расширения и модули блоков в JavaScript без проверки типов; проверку TypeScript запускают отдельно.
import type { AttrMap, BEMTree, BemEntity, ModMap } from 'nuckty';
interface BemNode {
block?: string; // имя блока: 'article', 'button', 'root'
elem?: string; // элемент блока: 'heading', 'item'
mods?: ModMap; // модификаторы блока: { size: 'm', view: 'action' }
elemMods?: ModMap; // модификаторы элемента
mix?: BemEntity | Array<BemEntity | string> | string; // подмешать классы другого блока
content?: BEMTree; // дочернее дерево, включая пустые позиции
tag?: string | false; // HTML-тег; false — не выводить тег
attrs?: AttrMap; // HTML-атрибуты
cls?: string; // дополнительный класс (не БЭМ)
js?: boolean | Record<string, unknown>; // данные для клиентского JavaScript блока
bem?: boolean; // генерировать ли БЭМ-классы (по умолчанию true)
props?: Record<string, unknown>; // данные для шаблонов; сами не сериализуются
raw?: string; // доверенная строка: не экранируется и не минимизируется
tightList?: boolean; // совместимость; опускание li определяют правила HTML
}
Основные поля
block/elem— БЭМ-сущность.block: 'menu'— блок,block: 'menu', elem: 'item'— его элемент. Из них движок собирает класс CSS:menu,menu__item.mods/elemMods— модификаторы.{ size: 'm' }превращается в классmenu_size_m. Модификатор задаёт вариант блока или элемента.content— рекурсивныйBEMTree: узел, текст, вложенные массивы или пустые позицииnull,undefined,false.tag— тег HTML. По умолчанию движок выводит<div>, но шаблон может задать любой тег.falseозначает «тег не нужен, только содержимое».
Как собираются классы
Из block, elem, mods, elemMods и mix движок составляет строку классов по классической схеме БЭМ:
block → 'menu'
block + elem → 'menu__item'
block + mods → 'menu_size_m'
block + elem + elemMods → 'menu__item_active'
mix → классы другого блока: 'menu__item button'
Разделители стандартные для БЭМ: __ перед элементом, _ перед модификатором. Схема именования одна, без вариантов. Экспортируемый classicPreset неизменяем; менять через него разделители движка нельзя.
Два особых поля
props — данные для шаблонов: параметры страницы, конфигурация, альтернативы hreflang. Само поле не сериализуется в HTML. Шаблон читает n.props и может вывести его значения через content или attrs; например, стандартный root так задаёт заголовок и язык документа. Поле не предназначено для сокрытия данных от посетителя.
raw — поле для доверенных строк. Движок вставляет содержимое без экранирования и минимизации: готовый HTML, встроенные скрипты аналитики, SVG. Внимание: не передавайте в raw ненадёжный ввод — движок выведет его без обработки.
Дерево как данные
Экспортируемый рекурсивный тип:
import type { BemNode } from 'nuckty';
type BEMTree = BemNode | BEMTree[] | string | null | undefined | false;
Структурные поля — данные, похожие на JSON. В props могут находиться объекты вне модели JSON, например Map; их сериализацию должен обеспечить вызывающий код. Дерево можно сохранить, вывести в лог, закэшировать, передать другому инструменту. Режим output: 'bemjson' возвращает готовое дерево, не превращая его в HTML. Подробнее — в решении о двухпроходном рендеринге.
// Получить дерево вместо HTML
const tree = render(page, { templates, output: 'bemjson' });
// tree имеет тип BEMTree: узел, текст, массив или пустое значение
Это удобно для отладки («почему страница вышла такой?») и собственных инструментов кеширования по хешу дерева. Обычная команда nuckty build очищает выходной каталог и собирает артефакт заново; инкрементальную сборку движок автоматически не выполняет.
Откуда берётся дерево
У дерева два источника.
- Markdown. Парсер remark превращает
.mdв mdast (типизированное дерево Markdown), а преобразование mdast → BEMJSON строит из него дерево блоков. Автор пишет Markdown, движок получаетBEMTree. См. Markdown. - Вёрстка страницы. Шаблон блока (через режимы
wrap,replace,content) формирует дерево обёртки страницы. Блокrootоборачивает контент в<html>. См. шаблоны nu.
В текущей стадии HTML контент сначала сериализуется, затем передаётся обработчику документа через raw. Общая модель позволяет использовать один рендерер для контента и вёрстки, но не исключает промежуточную строку HTML.