Перейти к содержанию
Mercury
Русский
Дизайн книга
Меню
Русский

Документы страниц: узлы, схема и parseDocument

Документ страницы — плоское отображение узлов. Каждый указывает тип блока, содержит JSON-свойства и перечисляет потомков в именованных слотах. parseDocument проверяет документ по реестру схем и возвращает документ либо ровно один отказ.

Страница Mercury — не дерево компонентов в коде. Это JSON-документ: данные, которые сервер может хранить, рендерер — читать, а редактор — менять, с одинаковыми проверками на всех этапах. Формат определяет @fiberfx/schema.

Документ

Документ содержит пять полей:

Поле Содержимое
format версия переносимого формата, целое число от 1; FORMAT_VERSION равен 1
id идентификатор документа, непустая строка
kind page (обычное содержимое), fragment (общее содержимое, на которое ссылаются другие документы) либо template
roots идентификаторы узлов верхнего уровня в заданном порядке
nodes все узлы в одном плоском объекте с ключами-идентификаторами

Неописанные форматом поля сохраняются без изменений, чтобы окружение могло хранить собственные данные рядом с содержимым. Они тоже должны быть совместимы с JSON.

Узлы и слоты

Дерево не вложено в JSON. Каждый узел присутствует в nodes один раз, а родитель перечисляет идентификаторы потомков в именованных слотах:

  • id: идентификатор узла, равный его ключу в nodes;
  • type: тип блока для рендеринга, например heading;
  • version: версия схемы этого блока, целое число от 1;
  • props: значения, только JSON;
  • slots: отображение имён слотов в упорядоченные списки идентификаторов потомков.

Свойство может содержать ссылку вместо простого значения: { "$ref": "asset", "id": "…" }, где $ref — asset, route, entity либо fragment. Документ хранит только ссылку. Объект, на который она указывает, разрешается отдельно от документа, никогда не внутри него.

Реестр схем

Реестр схем описывает доступные блоки и допустимые данные каждого. createSchemaRegistry({ blocks, limits?, breakpoints?, tokens? }) строит его из определений блоков. Определение содержит type, version, props и, необязательно, defaults, slots и migrations.

  • Схема свойства имеет kind: string (с enum, maxLength), number (с integer, min, max), boolean, reference (с to), token (с set), list (с items, maxItems), record (с fields) либо json. Любое свойство может быть optional и responsive.
  • Слот может перечислять допустимые типы блоков (allow) и границы количества потомков (min, max). Без ограничений допускаются любой тип и любое количество.
  • Ограничения по умолчанию берутся из DEFAULT_LIMITS: maxDepth — 32 уровня вложенности узлов, maxBytes — 1 048 576, maxNodes — 10 000, maxPropDepth — 16.

Ошибка определения — ошибка программы, поэтому createSchemaRegistry выбрасывает TypeError: при повторной регистрации типа, версии ниже 1, разрыве цепочки миграций или начальных значениях, не соответствующих собственной схеме.

Для каталога не нужно строить реестр самостоятельно. contentSchema() из @fiberfx/content возвращает реестр для проверки каждого документа Content: определения всех блоков каталога с точками перелома sm, md, lg и xl.

parseDocument

parseDocument(input, registry) читает документ из текста JSON либо уже разобранного значения. Ответ имеет одну из двух форм:

  • { ok: true, document }, где document — сам входной документ, включая неизвестные поля. Значения не приводятся к другим типам и не дополняются значениями по умолчанию.
  • Отказ: { ok: false, code, path, reason, subject, node? }.

Некорректный ввод не приводит к исключению. Текст, не являющийся JSON, getter, выбрасывающий исключение, или отозванный proxy возвращают отказ malformed. Текст длиннее восьмикратного maxBytes отклоняется как too-large до разбора; сам maxBytes считается по компактной JSON-записи документа, поэтому пробелы не меняют результат проверки.

import { parseDocument } from "@fiberfx/schema";
import { contentSchema } from "@fiberfx/content";

const parsed = parseDocument(
  {
    format: 1,
    id: "hello",
    kind: "page",
    roots: ["page"],
    nodes: {
      page: { id: "page", type: "page", version: 1, props: { title: "Hello" }, slots: { content: ["title", "intro"] } },
      title: { id: "title", type: "heading", version: 1, props: { text: "Hello", level: 7 }, slots: {} },
      intro: { id: "intro", type: "paragraph", version: 1, props: { text: "A first page." }, slots: {} },
    },
  },
  contentSchema(),
);

if (!parsed.ok) console.log(parsed.code, parsed.path, parsed.node, parsed.reason);
// invalid-prop #/nodes/title/props/level title expected at most 6

Свойство level заголовка — число от 1 до 6, поэтому 7 отклоняется. При level: 1 тот же вызов возвращает { ok: true, document }.

Результаты проверки

Отказ описывает одну проблему: типизированный code, путь JSON Pointer path в форме фрагмента (# — весь документ, #/nodes/title/props/level — одно свойство), reason в виде предложения, объект subject, внутрь которого ведёт путь (при разборе всегда document), и node, если проблема находится внутри узла.

Проверки выполняются по порядку; ответом становится первый отказ:

  1. Форма данных: malformed для отсутствующего или неверного поля, newer-format для формата выше 1, too-many-nodes при превышении maxNodes, not-json-safe для несовместимого с JSON значения, too-deep при вложенности значения больше maxPropDepth. Ключ каждого узла должен совпадать с его id.
  2. Размер: too-large, если компактный JSON превышает maxBytes.
  3. Структура: каждый узел должен быть достижим от корней ровно один раз. Иначе — unknown-child (идентификатора нет в nodes), multiple-parents, cycle, too-deep при превышении maxDepth либо orphan (узел не указан ни среди корней, ни в слоте).
  4. Каждый узел известного блока: unknown-slot, slot-cardinality (за пределами min или max), slot-not-allowed (тип потомка отсутствует в allow), затем свойства: unknown-prop, missing-prop (кроме необязательных) и invalid-prop (неверный вид, перечисление, длина, диапазон, токен либо переопределение с именем, не являющимся точкой перелома).
  5. Фрагмент, ссылающийся на себя, даёт cycle.

Приложение сайта Mercury показывает обработку отказа: записывает code, path и reason в журнал и оставляет начальную разметку страницы вместо частичного рендеринга.

Блоки, неизвестные реестру

Если реестр не знает type узла либо его version новее зарегистрированной, узел сохраняется без изменений и не проверяется по определению. Его положение в структуре по-прежнему проверяется. Поэтому документ для более позднего каталога успешно разбирается, а рендерер показывает такой узел как неподдерживаемый — подробнее в уроке о рендеринге.

Узел с более старой версией обрабатывается иначе: он отклоняется с outdated-block, и документ сначала нужно мигрировать.