Recipe, документ и HTML
У одной страницы три формы: правила сборки, готовое дерево и итоговая разметка. Эта глава объясняет, что хранит каждая форма и какую из них сохранять.
Зачем три представления
У каждого представления своя работа.
| Представление | Что хранит | Кто с ним работает |
|---|---|---|
| Recipe | правила сборки: шаблоны, места вставки, варианты, ссылки на настройки и содержимое | автор, редактор, продукт |
| Document | готовое проверенное дерево: варианты выбраны, данные подставлены, у узлов устойчивые идентификаторы | Framework, кеш, публикация |
| HTML | итоговая разметка | браузер |
Короче: Recipe — инструкция, Document — результат сборки, HTML — вывод.
Правила выбора:
- Recipe нужен, когда страница может выглядеть по-разному.
- Если вариантов нет, можно сразу хранить Document.
- HTML нельзя использовать как исходное описание страницы.
Recipe не обязательно писать вручную. Продукт может создавать его из своих файлов: например, генератор статического сайта хранит Markdown и настройки, а при сборке превращает их в Recipe. Автор правит исходники; Document и HTML создаются заново и не становятся ещё двумя копиями страницы для ручной правки.
Пример: статья с двумя шапками
Для статьи Recipe говорит: «возьми общий шаблон, выбери шапку по настройке и
вставь текст статьи». Настройка headerMode принимает compact или
expanded, а текст «Одинаковый основной текст.» приходит из одного источника
в обоих случаях.
"select": {
"value": {"input": "headerMode"},
"cases": {
"compact": {"id": "chosen-header", "ref": {"kind": "fragment", "owner": "example/library",
"ref": "header.compact", "policy": "pinned", "revision": "r1"}},
"expanded": {"id": "chosen-header", "ref": {"kind": "fragment", "owner": "example/library",
"ref": "header.expanded", "policy": "pinned", "revision": "r1"}}
}
}
После сборки Document уже содержит выбранную шапку и текст. При expanded
его дерево такое: страница → секция с заголовком «Расширенная шапка» и
пояснением «Дополнительная строка шапки» → абзац «Одинаковый основной текст.».
При compact пояснения нет. Команды «выбери вариант» в Document нет: выбор
уже сделан. В браузере это две обычные статьи.
Переключите настройку в работающем примере и откройте «Собранный Document» или «Полученный HTML», чтобы сравнить три представления на одних данных.
const modeSelect = document.querySelector('#recipe-mode');
const status = document.querySelector('#recipe-status');
const preview = document.querySelector('#recipe-preview');
const documentOutput = document.querySelector('#recipe-document');
const htmlOutput = document.querySelector('#recipe-html');
let fixture;
const stable = (value) => JSON.stringify(value, (_, entry) =>
entry && !Array.isArray(entry) && typeof entry === 'object'
? Object.fromEntries(Object.keys(entry).sort().map((key) => [key, entry[key]]))
: entry);
async function show() {
const mode = modeSelect.value;
status.textContent = 'Собираем страницу…';
preview.replaceChildren();
documentOutput.textContent = '';
htmlOutput.textContent = '';
try {
const expected = fixture.expected[mode];
const result = await SF.Composition.resolveRecipe(fixture.recipe, {
trustedContext: fixture.trustedContext,
inputs: fixture.inputs[mode],
executionContract: expected.dependencyReceipt.executionContract,
ports: {
readReference: async (reference) => ({
source: fixture.sources[reference.ref],
revision: reference.revision,
}),
},
});
if (result.diagnostics.length) throw new Error(result.diagnostics.map(({ code }) => code).join(', '));
if (stable(result.document) !== stable(expected.document)) {
throw new Error('Собранный Document отличается от проверяемого результата');
}
const rendered = await SF.Composition.render(result.document, {
resolveBinding: async (binding) => ({ status: 'demo-only', binding }),
});
if (rendered.diagnostics.length) throw new Error(rendered.diagnostics.map(({ code }) => code).join(', '));
documentOutput.textContent = JSON.stringify(result.document, null, 2);
htmlOutput.textContent = rendered.html;
const frame = document.createElement('iframe');
frame.title = 'Собранная статья';
frame.setAttribute('sandbox', '');
frame.style.cssText = 'width:100%;height:220px;border:1px solid #888;box-sizing:border-box';
frame.srcdoc = `<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><style>body{font:16px/1.5 system-ui;padding:16px;margin:0;color:#202124;background:#fff;overflow-wrap:anywhere}</style></head><body>${rendered.html}</body></html>`;
preview.append(frame);
status.textContent = 'Страница собрана из проверяемого Recipe.';
} catch (error) {
status.textContent = `Пример не собран: ${error.message}`;
}
}
modeSelect.addEventListener('change', show);
fetch('./header-switch.json').then((response) => {
if (!response.ok) throw new Error('Проверяемый пример не загружен');
return response.json();
}).then((value) => {
fixture = value;
return show();
}).catch((error) => {
status.textContent = `Пример не собран: ${error.message}`;
});
Полные Recipe, источники, оба набора настроек и ожидаемые Documents лежат в проверяемом JSON. По нему ИИ или разработчик повторяет сборку и сверяет результат. Данные и ссылки на источники передаёт проект; Framework собирает дерево и создаёт HTML.
Документ макета
Document имеет формат simai.composition.document. Его оболочка
одинакова для любого дерева:
| Поле | Смысл |
|---|---|
schema |
имя формата simai.composition.document, как в примере ниже; версию несёт schema_version (имя с меткой версии тоже принимается, см. Имена и версии) |
schema_version |
версия формата: 1 |
id |
идентификатор документа |
profile |
ui-layout — интерфейс; structured-content — самостоятельный структурированный материал |
locale |
необязательный язык документа |
root |
единственный корневой узел |
extensions |
необязательные расширения с пространством имён |
Каждый узел дерева состоит из полей id, type, data, props, slots,
presentation, bindings и extensions. Других полей нет — неизвестное поле
отклоняется.
type— зарегистрированный тип узла (Тип узла).data— содержимое, например текст заголовка.props— настройки структуры, например число колонок.slots— именованные места для дочерних узлов (Слоты и порядок).presentation— выбор оформления из закрытого списка:view,preset,modifiers.bindings— связь поля с внешним источником данных (Привязки к источнику).
Пример ниже показывает минимальную информационную страницу:
{
"schema": "simai.composition.document",
"schema_version": 1,
"id": "about-page",
"profile": "ui-layout",
"locale": "ru",
"root": {
"id": "page",
"type": "layout.page",
"slots": {"default": [
{"id": "intro", "type": "layout.section", "slots": {"default": [
{"id": "title", "type": "content.heading",
"data": {"level": 1, "content": [{"type": "text", "value": "О компании"}]}}
]}}
]}
}
}
Обратите внимание: все id внутри документа уникальны. Документ ограничен:
до 1 МиБ в UTF-8, глубина вложенности до 32, до 2 000 узлов и до 500 детей в
одном слоте. Эти поля и ограничения сверяют при создании генератора, хранении
макета и разборе ошибки верхнего уровня.
Минимальная информационная страница из примеров инспектора:
{
"schema": "simai.composition.document",
"schema_version": 1,
"id": "information",
"profile": "ui-layout",
"locale": "ru",
"root": {
"id": "page",
"type": "layout.page",
"slots": {
"default": [
{
"id": "intro",
"type": "layout.section",
"slots": {
"default": [
{
"id": "title",
"type": "content.heading",
"data": {
"level": 2,
"content": [
{
"type": "text",
"value": "О компании"
}
]
}
},
{
"id": "text",
"type": "content.paragraph",
"data": {
"content": [
{
"type": "text",
"value": "Мы создаём интерфейсы для людей."
}
]
}
}
]
}
}
]
}
}
}
Расширения. Расширение — ключ вида пространство:имя. Неизвестное
необязательное расширение сохраняется, но не исполняется. Расширение с
required: true, поддержку которого обработчик не объявил, останавливает
обработку: обработчик не делает вид, что понял документ.
Контрольный документ каталога показывает все части оболочки сразу:
структурированный текст со ссылкой, привязку к внешним данным, оформление
секции и расширение с пространством имён. Это контрольный пример Framework
tests/fixtures/composition/catalog-page.json: документация сверяет свою
копию с ним побайтно.
Смотрите также
- Как проходит сборка — как Recipe превращается в Document и HTML.
- Уровни макета — типы узлов.
- Версии и отпечатки — версии и отпечатки.