nuckty
EN
Все разделы →

Модель данных: 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 очищает выходной каталог и собирает артефакт заново; инкрементальную сборку движок автоматически не выполняет.

Откуда берётся дерево

У дерева два источника.

  1. Markdown. Парсер remark превращает .md в mdast (типизированное дерево Markdown), а преобразование mdast → BEMJSON строит из него дерево блоков. Автор пишет Markdown, движок получает BEMTree. См. Markdown.
  2. Вёрстка страницы. Шаблон блока (через режимы wrap, replace, content) формирует дерево обёртки страницы. Блок root оборачивает контент в <html>. См. шаблоны nu.

В текущей стадии HTML контент сначала сериализуется, затем передаётся обработчику документа через raw. Общая модель позволяет использовать один рендерер для контента и вёрстки, но не исключает промежуточную строку HTML.