Формат документа
Сохраняемое значение имеет тип EditorDocument: это версионированная оболочка JSON с упорядоченными блоками. Она служит единственным поддерживаемым форматом обмена между редактором, подсистемой хранения, миграциями и рендерером документа.
Оболочка документа
interface EditorDocument {
time?: number
version: string
blocks: BlockData[]
}{
"time": 1783846522956,
"version": "1.0.0",
"blocks": [
{
"id": "intro",
"type": "heading",
"data": {
"text": "Заголовок документа",
"level": 2
}
}
]
}После нормализации version обязателен. Текущая версия — 1.0.0. time — необязательные метаданные; при наличии это должно быть конечное число. Порядок элементов blocks является порядком документа.
Данные блока
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. Например:
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 блока хранит тип и данные виджета под тем же устойчивым идентификатором.
{
"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().
Прикладной код может выполнить ту же проверку оболочки и цепочку миграций до создания редактора:
import { DocumentSchema } from '@shelamkoff/rector'
const schema = new DocumentSchema({
currentVersion: '1.0.0',
versionPolicy: 'strict',
migrations,
})
const normalized = schema.normalize(untrustedInput)currentVersion по умолчанию равен текущей версии формата Rector, versionPolicy — preserve, а migrations — пустому массиву. Отдельная схема не проверяет принадлежащие плагинам data, потому что к ней не подключён реестр блочных плагинов.
Политика версий
Версия относится ко всей оболочке, а не к одному плагину. Для экземпляра редактора выбирается одна политика:
preserveприменяет все миграции, доступные из заявленной версии, и, если цепочка заканчивается до текущей версии, возвращает последний достигнутый структурно корректный документ;strictтребует полного пути миграции до текущей версии.
Сохранение неизвестной версии полезно для просмотра без потерь и поэтапного развёртывания. Строгий режим подходит приложению, способному работать только с текущей схемой.
Миграции
Миграция — один направленный синхронный шаг:
interface DocumentMigration {
from: string
to: string
migrate(document: EditorDocument): EditorDocument
}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() выбрасывает исключение до возвращения документа. Эта политика не заменяет очистку входа или серверную проверку на границе доверия.
Правила совместимости
При развитии опубликованного плагина:
- продолжайте читать ранее опубликованные формы данных;
- записывайте из
save()одну каноническую текущую форму; - добавляйте миграцию документа, если старую форму нельзя безопасно прочитать;
- не используйте существующий
typeдля несвязанных данных; - сохраняйте
idблоков при миграции; - проверяйте загрузку, сохранение и отображение всех поддерживаемых исторических примеров.