Skip to content

Формат документа

Сохраняемое значение имеет тип EditorDocument: это версионированная оболочка JSON с упорядоченными блоками. Она служит единственным поддерживаемым форматом обмена между редактором, подсистемой хранения, миграциями и рендерером документа.

Оболочка документа

ts
interface EditorDocument {
  time?: number
  version: string
  blocks: BlockData[]
}
json
{
  "time": 1783846522956,
  "version": "1.0.0",
  "blocks": [
    {
      "id": "intro",
      "type": "heading",
      "data": {
        "text": "Заголовок документа",
        "level": 2
      }
    }
  ]
}

После нормализации version обязателен. Текущая версия — 1.0.0. time — необязательные метаданные; при наличии это должно быть конечное число. Порядок элементов blocks является порядком документа.

Данные блока

ts
interface BlockData {
  id: string
  revision?: string | number
  type: string
  data: Record<string, unknown>
  tunes?: Record<string, unknown>
  inline?: Record<string, EditorInlineWidget>
}
  • id — устойчивый идентификатор для выделения, событий, истории и ссылок приложения. В пределах документа значения должны быть уникальны.
  • revision — необязательная редакция содержимого или устойчивый хеш, принадлежащий источнику данных и ускоряющий повторные вызовы renderTo().
  • type выбирает зарегистрированный блочный плагин и рендерер.
  • data принадлежат блочному плагину и проверяются им.
  • tunes содержит необязательные настройки, которыми владеет редактор, а не отдельный блочный плагин. Встроенный инструмент выравнивания записывает tunes.textAlign: 'left' | 'center' | 'right' | 'justify', поэтому выравнивание сохраняется и у составных блоков с несколькими текстовыми полями.
  • inline содержит постоянные виджеты, на которые ссылаются текстовые поля блока.

Не используйте индекс массива как идентификатор блока. Индексы меняются после вставки, удаления и перемещения.

revision не является ни идентификатором блока, ни версией формата документа. При наличии это значение должно меняться вместе с data, tunes или inline. Rector сохраняет входящую редакцию, пока редактор не меняет блок, а после локального изменения удаляет её, поскольку не может создать следующее значение от имени источника. Поле можно не передавать: тогда рендерер использует полную сигнатуру содержимого.

Данные, принадлежащие плагину

Каждый плагин определяет собственную форму data. Например:

ts
type ParagraphData = {
  text: string
  align?: 'left' | 'center' | 'right' | 'justify'
}

type HeadingData = {
  text: string
  level: 2 | 3 | 4 | 5 | 6
  align?: 'left' | 'center' | 'right' | 'justify'
}

ParagraphData.align и HeadingData.align остаются частью формата данных этих плагинов. Встроенный инструмент выравнивания дополнительно записывает принадлежащее редактору значение tunes.textAlign, поэтому один инструмент работает и со структурированными блоками, у которых есть текстовый корневой элемент. Если присутствуют оба значения, tunes.textAlign имеет приоритет: редактор и средство отображения применяют его после создания элемента плагином.

Поля и значения по умолчанию следует брать со страницы соответствующего плагина. Неизвестные ключи нельзя использовать для метаданных приложения: плагин вправе отбросить их при цикле сохранения. Храните метаданные приложения вне EditorDocument, если ими не владеет явный контракт плагина.

Хранение внутристрочных виджетов

Постоянные виджеты отделены от текстовых полей с HTML. Текстовое поле содержит заполнитель {{<id>}}, а карта inline блока хранит тип и данные виджета под тем же устойчивым идентификатором.

json
{
  "id": "greeting",
  "type": "paragraph",
  "data": {
    "text": "Здравствуйте, {{person-42}}"
  },
  "inline": {
    "person-42": {
      "type": "mention",
      "data": {
        "id": "42",
        "name": "Ада"
      }
    }
  }
}

Владеющий текстом плагин реализует mapTextFields(), чтобы Rector заменял живой DOM виджета заполнителем при сохранении и восстанавливал его при загрузке. Прикладной код должен считать синтаксис заполнителей внутренней частью контракта сериализации и не изменять его отдельно от карты inline.

Текстовые поля с HTML

Форматирование внутристрочными инструментами хранится в текстовых полях плагина как очищенный HTML. Поддерживаемая разметка намеренно ограничена. URL, стили, теги и атрибуты фильтруются общей подсистемой очистки при переходе через границы разбора.

Не объединяйте недоверенные данные со строкой сохранённого HTML. Помещайте текст в textContent, передавайте URL через контракт плагина и позволяйте Rector повторно очищать сохранённую разметку при отображении.

Нормализация

DocumentSchema.normalize(input) возвращает скопированный структурно корректный документ:

  • в режиме preserve значение, не являющееся объектом, превращается в пустой документ, а отсутствующий blocks — в пустой массив;
  • в режиме strict такие ошибочные оболочки приводят к исключению;
  • отсутствующая версия заменяется текущей;
  • ошибочное значение time отбрасывается;
  • результат каждого шага миграции всегда строго проверяется перед следующим шагом.

Редактор применяет нормализацию к исходному data и каждому аргументу render().

Прикладной код может выполнить ту же проверку оболочки и цепочку миграций до создания редактора:

js
import { DocumentSchema } from '@shelamkoff/rector'

const schema = new DocumentSchema({
  currentVersion: '1.0.0',
  versionPolicy: 'strict',
  migrations,
})

const normalized = schema.normalize(untrustedInput)

currentVersion по умолчанию равен текущей версии формата Rector, versionPolicypreserve, а migrations — пустому массиву. Отдельная схема не проверяет принадлежащие плагинам data, потому что к ней не подключён реестр блочных плагинов.

Политика версий

Версия относится ко всей оболочке, а не к одному плагину. Для экземпляра редактора выбирается одна политика:

  • preserve применяет все миграции, доступные из заявленной версии, и, если цепочка заканчивается до текущей версии, возвращает последний достигнутый структурно корректный документ;
  • strict требует полного пути миграции до текущей версии.

Сохранение неизвестной версии полезно для просмотра без потерь и поэтапного развёртывания. Строгий режим подходит приложению, способному работать только с текущей схемой.

Миграции

Миграция — один направленный синхронный шаг:

ts
interface DocumentMigration {
  from: string
  to: string
  migrate(document: EditorDocument): EditorDocument
}
js
const migrations = [
  {
    from: '0.9.0',
    to: '1.0.0',
    migrate(document) {
      return {
        ...document,
        version: '1.0.0',
        blocks: document.blocks.map(block => (
          block.type === 'text'
            ? { ...block, type: 'paragraph' }
            : block
        )),
      }
    },
  },
]

Rector копирует вход перед каждым шагом, проверяет, что результат является документом, и принудительно задаёт объявленную версию to. Повторяющиеся from и переход в ту же версию отклоняются при создании схемы. Цикл всегда приводит к исключению во время нормализации. Неполная цепочка приводит к исключению в режиме strict, а в режиме preserve Rector возвращает последний достигнутый структурно корректный документ.

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

Проверка блока

Проверка оболочки и проверка плагина решают разные задачи. Схема проверяет внешнюю форму документа. Зарегистрированный блочный плагин может реализовать validate(data) для собственных данных.

При validationMode: 'preserve' ошибочные данные плагина сохраняются и передаются в onValidationError. В режиме strict метод save() выбрасывает исключение до возвращения документа. Эта политика не заменяет очистку входа или серверную проверку на границе доверия.

Правила совместимости

При развитии опубликованного плагина:

  1. продолжайте читать ранее опубликованные формы данных;
  2. записывайте из save() одну каноническую текущую форму;
  3. добавляйте миграцию документа, если старую форму нельзя безопасно прочитать;
  4. не используйте существующий type для несвязанных данных;
  5. сохраняйте id блоков при миграции;
  6. проверяйте загрузку, сохранение и отображение всех поддерживаемых исторических примеров.

Rector is released under the MIT License.