К содержанию

Навигация

Recipe, документ и HTML

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

Зачем три представления

У каждого представления своя работа.

Представление Что хранит Кто с ним работает
Recipe правила сборки: шаблоны, места вставки, варианты, ссылки на настройки и содержимое автор, редактор, продукт
Document готовое проверенное дерево: варианты выбраны, данные подставлены, у узлов устойчивые идентификаторы Framework, кеш, публикация
HTML итоговая разметка браузер

Короче: Recipe — инструкция, Document — результат сборки, HTML — вывод.

Правила выбора:

  • Recipe нужен, когда страница может выглядеть по-разному.
  • Если вариантов нет, можно сразу хранить Document.
  • HTML нельзя использовать как исходное описание страницы.

Recipe не обязательно писать вручную. Продукт может создавать его из своих файлов: например, генератор статического сайта хранит Markdown и настройки, а при сборке превращает их в Recipe. Автор правит исходники; Document и HTML создаются заново и не становятся ещё двумя копиями страницы для ручной правки.

Пример: статья с двумя шапками

Для статьи Recipe говорит: «возьми общий шаблон, выбери шапку по настройке и вставь текст статьи». Настройка headerMode принимает compact или expanded, а текст «Одинаковый основной текст.» приходит из одного источника в обоих случаях.

JSON
"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», чтобы сравнить три представления на одних данных.

JavaScript
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 — связь поля с внешним источником данных (Привязки к источнику).

Пример ниже показывает минимальную информационную страницу:

JSON
{
  "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 детей в одном слоте. Эти поля и ограничения сверяют при создании генератора, хранении макета и разборе ошибки верхнего уровня.

Минимальная информационная страница из примеров инспектора:

JSON
{
  "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: документация сверяет свою копию с ним побайтно.

Смотрите также

Поиск по документации

Введите минимум 2 символа

    Настройки

    Тема
    Размер шрифта
    Ширина страницы