OutputData: формат сохранённого JSON
Структура данных, которую возвращает метод save(). Входные позиции также принимают нестрогие транспортные варианты LooseOutputData / LooseOutputBlockData. Это опция конфигурации data, render(), blocks.render() и blocks.insertMany().
В нестрогих вариантах поля блока data, id, parent, content и time могут быть null.
nullвdataстановится{}.nullили пустойidзаменяется сгенерированным.nullвparentиnullили пустойcontentсчитаются отсутствующими: блок корневой и без детей.
Сохранённый вывод всегда имеет строгую форму.
При загрузке документа Blok выстраивает массив blocks в порядке обхода в глубину: каждый блок идёт прямо перед всем своим поддеревом. Так происходит для опции конфигурации data, render() и blocks.render(). blocks.insertMany() сохраняет тот порядок, в котором блоки переданы.
- Корневые блоки сохраняют входной порядок. Блок, чей
parentотсутствует в документе, считается корневым. - Дочерние блоки родителя идут за ним в том порядке, который задаёт его
content. - Дочерние блоки, которых нет в
content, идут после них во входном порядке. Они также добавляются в конецcontent, кроме таблицы и базы данных.
Поэтому если сохранённые данные идут в другом порядке, save() вернёт блоки не в том порядке, в котором они пришли. Документ с повторяющимися id загружается как есть.
Как получить экземпляр редактора
Методы ниже вызываются на редакторе, созданном через new Blok(). Они доступны после того, как разрешится editor.isReady.
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;
// Call any API method on it.
editor.caret.setToLastBlock('end');Методы
equalsOutputData(a, b, options?)
booleanСтруктурное равенство сохранённых документов, экспортируется из главной точки входа. Глубоко сравнивает массивы blocks.
- Изменчивые поля-конверты
timeиversionигнорируются, поэтому документ, прошедший через save(), равен своему эху. - Идентификаторы блоков участвуют, только если они есть у ОБЕИХ сторон.
- Редактор выдаёт свежие id контенту без идентификаторов, поэтому legacy-документ (или бэкенд, который вырезает id) всё равно равен своему сохранённому эху. Обёртка, снимающая id, на стороне потребителя не нужна.
- Метаданные правки (
lastEditedAt/lastEditedBy) тоже не участвуют.- Они фиксируют, кто и когда тронул блок, а не что в нём написано. Поэтому документ, который отличается только меткой, считается неизменённым.
- Nullish-документы и нестрогие транспортные формы принимаются:
null/undefinedравны{ blocks: [] }, аparent: null/content: nullиз DTO равны сохранённой форме, где этих ключей нет.
Третий аргумент это EqualsOutputDataOptions, он тоже экспортируется из главной точки входа. Опция ignoreEmptyDefaultBlocks (по умолчанию false) убирает с обеих сторон пустые блоки инструмента ПО УМОЛЧАНИЮ до сравнения.
Тогда нетронутый редактор с одним пустым параграфом равен пустому сохранённому эталону. Этот флаг и нужен для проверок «изменено или нет». Пустые НЕ-дефолтные блоки (разделитель без содержимого, пустое изображение) сохраняются.
Когда использовать
Используйте её, чтобы сохранять данные или обновлять состояние только при реальных изменениях. time и version различаются при каждом save(), поэтому наивное глубокое сравнение всегда сообщает об изменении.
import { equalsOutputData } from '@bloklabs/core';
const saved = await editor.save();
if (!equalsOutputData(saved, previousData)) {
await persist(saved); // only hit the backend on real changes
}isEmptyOutputData(data)
booleanTrue, когда документ не содержит пользовательского контента. Экспортируется из главной точки входа.
Это значит: документ nullish, в нём нет блоков, либо данные каждого блока содержат только пустые значения (пустые строки и строки из одних пробелов, пустые массивы и объекты). Числа и булевы значения (level, checked, стили) считаются презентационными метаданными. Сами по себе они контентом не считаются.
Когда использовать
Визуальные блоки без контента (например, разделитель с data: {}) считаются пустыми. Проверяйте blocks.length, когда важно само наличие блоков.
import { isEmptyOutputData } from '@bloklabs/core';
const data = await editor.save();
submitButton.disabled = isEmptyOutputData(data);
// → true for a fresh editor holding one blank paragraphnormalizeOutputData(data)
OutputDataНормализует целый нестрогий backend-DTO в строгую сохранённую форму OutputData, экспортируется из главной точки входа.
- Nullish-документ становится
{ blocks: [] }. - Поля-конверты со значением
null(time/version) отбрасываются. - Каждый блок тоже нормализуется.
nullили отсутствующийdataстановится{}.nullили пустой id отбрасывается, чтобы сгенерировался новый.- Nullish и пустые ссылки иерархии (
parent: null,content: null,content: []) отбрасываются как отсутствующие.
В отличие от маппера blocks.map(...), написанного вручную, функция сохраняет каждое сквозное поле. Это tunes, настоящие ссылки parent/content, indent и метаданные правки. Поэтому иерархия и тюны никогда не теряются молча. Функция идемпотентна: строгий документ проходит насквозь без изменений.
import { normalizeOutputData } from '@bloklabs/core';
// A loose Editor.js-era DTO (data: null, id: null) from your backend
const strict = normalizeOutputData(dtoFromApi);
// → strict OutputData, safe to persist or diff — no blind `as OutputData` castnormalizeOutputBlocks(blocks)
OutputBlockData[]Аналог normalizeOutputData на уровне блоков, экспортируется из главной точки входа. Нормализует массив нестрогих транспортных блоков в строгую сохранённую форму.
null или отсутствующий data становится {}, null или пустой id отбрасывается, чтобы сгенерировался новый, а nullish и пустые parent/content отбрасываются как отсутствующие. Все остальные поля проходят нетронутыми.
Используйте normalizeOutputData, когда у вас на руках весь конверт документа.
import { normalizeOutputBlocks } from '@bloklabs/core';
const blocks = normalizeOutputBlocks(looseBlocksFromApi);
// → OutputBlockData[] with tunes/parent/content/indent intactBlokData<T>
{ [K in keyof T]: T[K] }Хелпер типов, экспортируется из главной точки входа. Он позволяет форме данных блока, объявленной через interface, подойти в слот data.
У interface в TS нет неявной индексной сигнатуры, поэтому он не присваивается типу Record<string, unknown>. Из-за этого OutputBlockData<'task', TaskData> не компилируется, когда TaskData объявлен интерфейсом.
BlokData<T> перепроецирует T через гомоморфный отображённый тип. Компилятор считает такой тип имеющим неявную индексную сигнатуру, и при этом каждый объявленный ключ сохраняет свой точный тип. Переписывать ничего не нужно: существующее значение интерфейса присваивается типу BlokData<T>, а псевдоним type и так удовлетворяет слоту сам по себе.
import type { BlokData, OutputBlockData } from '@bloklabs/core';
interface TaskData { title: string; done: boolean }
const block: OutputBlockData<'task', BlokData<TaskData>> = {
type: 'task',
data: { title: 'Ship it', done: false },
};flattenTree(spec, options?)
Array<OutputBlockData & { id: string }>Превращает эргономичную вложенную спецификацию в плоский массив OutputBlockData[] в порядке DFS pre-order, который хранит Blok. Все ссылки parent/content она связывает за вас.
Экспортируется из главной точки входа вместе с типами BlockTreeSpec, BlockRunSpec, BlockTreeNode и FlattenTreeOptions. Узел спецификации выглядит так: { type?, data?, tunes?, id?, children? }.
Это чистый аналог живой мутации blocks.insertTree(): тот же DFS, но без редактора. Поэтому вложенный контент (колонки, таблицы, целый документ) можно засеять, не выписывая вручную массивы id в parent/content.
У каждого возвращённого блока есть разрешённый id, он генерируется, когда спецификация его не задала. Поэтому на массив безопасно ссылаться по id. Листья опускают пустой массив content.
Контент, который уже плоский, например сохранённый документ Blok, который миграция вклеивает в страницу, попадает внутрь как узел-прогон, { blocks: [...] }, в корне или как дочерний.
- Прогон вклеивается дословно: id,
data,tunesи существующие ссылкиparent/contentсохраняются. - Переподчиняются охватывающему узлу только те блоки, которые прогон оставил без родителя.
Это то же правило, которое blocks.insertMarkdown() применяет к преобразованному прогону. Ничего не выводится заново, поэтому id остаются стабильными, и миграция может выполняться пакетами, не дублируя уже записанные блоки.
options принимает parentId, то есть значение parent для корневого узла или узлов, и generateId, генератор id для узлов без явного id. Передайте детерминированный генератор, чтобы вывод был воспроизводимым.
Повторное использование явного id внутри спецификации выбрасывает исключение. То же самое делает передача уже плоского блока как узла дерева: его ссылки parent/content были бы отброшены молча.
import { flattenTree } from '@bloklabs/core';
// A two-column layout, written as a tree instead of parent/content id arrays
const blocks = flattenTree([
{
type: 'column_list',
children: [
{ type: 'column', children: [{ type: 'paragraph', data: { text: 'Left' } }] },
{ type: 'column', children: [{ type: 'paragraph', data: { text: 'Right' } }] },
],
},
]);
// Ready to hand to the `data` config option, render() or blocks.insertMany()
console.log(blocks); // flat, DFS pre-order, every parent/content link wired
// An already-flat saved document goes in as a run node — spliced verbatim,
// ids kept, only its top-level blocks re-parented under the column
const migrated = flattenTree({
type: 'column',
children: [{ blocks: legacyPage.blocks }],
});isBlockType(block, type)
block is OutputBlockData<K, BlokBlockDataMap[K]>Предикат типа, который экспортируется из @bloklabs/core/tools. Он сужает сохранённый блок до известного типа блока, поэтому его data типизируется через реестр BlokBlockDataMap, а не как Record<string, unknown>. Он заменяет проверку block.type === 'header' вместе с приведением data as HeaderData.
BlokBlockDataMap сопоставляет каждому встроенному типу блока его форму данных и экспортируется из того же подпути. Реестр расширяем, поэтому пользовательский инструмент регистрирует собственную форму через слияние деклараций и сужается точно так же.
Типы описывают документы, сохранённые этой версией или переведённые через migrateToRichText. В документе, сохранённом раньше, поля с форматированным текстом могут хранить HTML-строки. Переведите его до типизированного чтения.
import { isBlockType } from '@bloklabs/core/tools';
import type { OutputData } from '@bloklabs/core';
function logHeadings(saved: OutputData) {
for (const block of saved.blocks) {
if (isBlockType(block, 'header')) {
console.log(block.data.level); // number — no cast
}
}
}
// A custom tool joins the registry by declaration merging
declare module '@bloklabs/core/tools' {
interface BlokBlockDataMap {
'my-widget': { widgetId: string };
}
}blocksOfType(data, type)
Array<OutputBlockData<K, BlokBlockDataMap[K]>>Коллекционный аналог isBlockType, тоже экспортируется из @bloklabs/core/tools. Собирает из документа каждый сохранённый блок заданного типа, причём data каждого результата типизируется через BlokBlockDataMap.
Функция терпима к null: документ null или undefined принимается, как и нестрогая транспортная форма LooseOutputData. Поэтому она заменяет (data?.blocks ?? []).filter(...) вместе с приведением типа, которое каждая фича пишет заново.
Типы описывают документы, сохранённые этой версией или переведённые через migrateToRichText. В документе, сохранённом раньше, поля с форматированным текстом могут хранить HTML-строки. Переведите его до типизированного чтения.
import { blocksOfType } from '@bloklabs/core/tools';
import { migrateToRichText, richTextToPlainText } from '@bloklabs/core/migrate';
import type { OutputData } from '@bloklabs/core';
// `stored` may be null — blocksOfType tolerates it and returns []
function buildToc(stored: OutputData | null) {
// A document saved by an older version can still hold HTML strings
const saved = stored === null ? null : migrateToRichText(stored);
return blocksOfType(saved, 'header')
// data.text is RichText, data.level is number — no cast
.map((block) => ({ text: richTextToPlainText(block.data.text), level: block.data.level }));
}EMPTY_OUTPUT_DATA
OutputDataОбщий, глубоко замороженный пустой документ ({ blocks: [] }), экспортируется из главной точки входа. Используйте его вместо написанного вручную литерала { blocks: [] } для очищенных и нетронутых эталонов. Он заморожен вместе с массивом блоков, поэтому общую ссылку нельзя изменить и превратить в устаревший непустой эталон.
import { EMPTY_OUTPUT_DATA, equalsOutputData } from '@bloklabs/core';
const saved = await editor.save();
const isPristine = equalsOutputData(saved, EMPTY_OUTPUT_DATA, {
ignoreEmptyDefaultBlocks: true,
});toRenderableData(data)
OutputData | LooseOutputDataОтображает контролируемое значение data в то, что принимают render()/blocks.render(); экспортируется из главной точки входа. null вместо целого документа, то есть контролируемая «очистка до пустого», становится { blocks: [] }. Любой настоящий документ проходит нетронутым.
Строгая проверка внутри render() читает data.blocks и выбросила бы исключение на null. Поэтому сначала пропустите nullable-значение контролируемого поля через эту функцию.
import { toRenderableData } from '@bloklabs/core';
// `draft` may be null when the host clears the document
await editor.blocks.render(toRenderableData(draft));createEmittedEchoWindow(capacity?)
{ record; matches; clear }Создаёт ограниченное окно недавно отправленных нагрузок onSave, чтобы вы могли распознавать эхо контролируемого data. Экспортируется из главной точки входа.
Дедупликации только по ПОСЛЕДНЕЙ отправленной нагрузке недостаточно. Хост, который сохраняет данные при save и запрашивает их заново, может вернуть УСТАРЕВШЕЕ эхо: более раннее сохранение приходит после того, как более новое уже заменило эталон. Повторная отрисовка такого эха затёрла бы курсор и весь набранный с тех пор контент.
Сопоставление структурное (equalsOutputData), поэтому конверты, изменённые по пути (свежий time, вырезанные id), всё равно считаются эхом.
import { createEmittedEchoWindow } from '@bloklabs/core';
const echoes = createEmittedEchoWindow();
// in onSave: echoes.record(data)
// before re-rendering incoming props: if (echoes.matches(next)) return;migrateLegacyBlocks(blocks, options?)
OutputBlockData[]Переводит legacy-блоки и блоки в стиле Editor.js в иерархический формат Blok «плоский со ссылками», экспортируется из подпути @bloklabs/core/migrate. Это то же преобразование, которое рендерер выполняет автоматически при загрузке.
Вложенные legacy-формы (пункты списка, дочерние блоки тоггла и выноски) разворачиваются в отдельные блоки, связанные через parent/content. Поле parent принадлежит сохранённому документу, а parentId это поле снимка BlockNode из useBlocks.
Блокам без id проставляется id. Уже иерархические блоки проходят без изменений, поэтому запускать миграцию на текущих данных безопасно, а повторные прогоны идемпотентны.
options открывает контекст миграции.
generateIdделает проход ЧИСТЫМ: мигрируйте один и тот же документ дважды, и выводы будут равны.- Это нужно, чтобы сравнить сохранённый документ с его миграцией или перезапускать миграцию на каждый рендер, не выпуская свежие id.
onLossyFieldдоставляет каждое отброшенное поле вместо сброса его вconsole.warn.rulesдобавляет ваши собственные записи грамматики.
migrateLegacyOutputData(data, options?) это вариант, который сохраняет конверт. needsLegacyMigration(blocks, options?) сообщает, изменит ли миграция хоть что-нибудь. matchLegacyRule(block, options?) это примитив на один блок: он возвращает запись, которая претендует на этот блок (или null), не пересканируя таблицу для каждого блока.
Каждая точка входа, принимающая правила, принимает либо объект опций, либо голый массив rules. Поэтому передача массива напрямую не может молча прочитаться как «нет правил».
Когда использовать
Некоторые формы данных понимает только конкретный инструмент (раскладка колонок, собственный конверт медиа), и встроенная миграция ядра прочитать их не может. Задайте такому инструменту статический upgradeData(data), чистую функцию, которая возвращает текущую форму данных инструмента.
Blok выполняет её при загрузке, во время сборки каждого сохранённого блока, до того как инструмент будет сконструирован. Хук, выбросивший исключение, перехватывается, и блок загружается со своими сохранёнными данными.
import {
migrateLegacyBlocks,
migrateLegacyOutputData,
needsLegacyMigration,
matchLegacyRule,
} from '@bloklabs/core/migrate';
// Batch-upgrade persisted Editor.js documents
const upgraded = migrateLegacyOutputData(storedDocument);
// Or migrate just the blocks, skipping the pass when already current
const blocks = needsLegacyMigration(stored.blocks)
? migrateLegacyBlocks(stored.blocks)
: stored.blocks;
// Deterministic migration: same input → equal output, every time
let n = 0;
const pure = migrateLegacyBlocks(stored.blocks, {
generateId: () => `blk-${n++}`,
onLossyField: ({ blockType, field }) => report(blockType, field),
});
// Dispatch per block without allocating a throwaway array
const entry = matchLegacyRule(stored.blocks[0]); // → { legacyType, targetType, … } | nullmigrations (config) & migrateOutputData(data, migrations)
OutputDataОбъявляйте правила «старая форма данных → новая форма данных» по типам СНАРУЖИ класса инструмента. upgradeData должен жить внутри инструмента, которым владеете вы. А migrations это карта с ключами по типу блока, которую вы передаёте в конфигурации редактора. Поэтому вы можете мигрировать сторонний инструмент, который вам не подконтролен, или свой собственный инструмент, не редактируя (и не перевыпуская) его класс.
Каждое правило это чистое преобразование (data) => data. Верните вход без изменений или undefined, когда данные уже актуальны.
Blok применяет правило при загрузке, после собственного upgradeData инструмента и ДО анализа формата. Поэтому dataModel: 'auto' видит форму после миграции, и цикл в режиме 'auto' не может тихо откатить миграцию, сохранив старую форму обратно. Правило, выбросившее исключение, откатывается к сохранённым данным, а не к пустому редактору.
Та же карта работает офлайн: передайте её в migrateOutputData(data, migrations) (или migrateBlocks(blocks, migrations)) из @bloklabs/core/migrate, чтобы пакетно обновить сохранённые записи, не открывая редактор. Все три адаптера фреймворков принимают её как проп или инпут migrations.
Когда использовать
Правила должны быть чистыми и идемпотентными. Они выполняются при каждой загрузке, в том числе на уже актуальных данных.
- Для форм, которые хост решает снаружи, предпочитайте
migrations(конфигурация). - Для форм, понятных только одному инструменту, предпочитайте его собственный
upgradeData.
Они сочетаются: сначала выполняется upgradeData, затем правило migrations из конфигурации для этого типа.
// 1. At load, via editor config
new Blok({
tools: { myCard: MyCard },
migrations: {
// key = block type; old shape → new shape
myCard: (data) => ('name' in data ? { ...data, title: data.name } : data),
// `data` is BlockToolData (Record<string, unknown>), so narrow before reading
image: (data) => {
const file = data.file as { url?: string } | undefined;
return file?.url ? { ...data, url: file.url } : data;
},
},
});
// 2. Offline / batch — same rules, no editor
import { migrateOutputData } from '@bloklabs/core/migrate';
const upgraded = migrateOutputData(storedDocument, {
myCard: (data) => ({ ...data, title: data.name }),
});migrate(data, { migrations, rules, generateId, onLossyField })
{ data: OutputData; report: MigrationReport }Составная точка входа. Она выполняет ОБА прохода миграции в единственно верном порядке и сообщает, во что миграция обошлась.
- Сначала выполняются правила данных (
migrations). - Затем правила грамматики (
rules) перестраивают дерево.
Этот порядок принципиально важен. Правила данных заданы по ТИПУ блока, а грамматика переписывает типы (linkTool → bookmark) и разворачивает контейнеры во множество блоков. Поэтому правило, запущенное после грамматики, никогда не срабатывает, и блок молча остаётся немигрированным.
Правила данных формируют вход грамматики. За форму вывода для типов, которые она переписывает, отвечает грамматика.
report называет каждое поле, которое сопоставление не смогло перенести (lossyFields), и каждое правило данных, выбросившее исключение (errors). Поэтому пакетное обновление сохранённых записей больше не молчит о собственной потере данных.
Когда использовать
report.lossyFields содержит то, что раньше говорил console.warn и что ничем нельзя было прочитать. Логируйте его рядом с пакетным обновлением, и вы получите проверяемую запись о том, что именно обновление отбросило, по типам блоков.
import { migrate } from '@bloklabs/core/migrate';
let n = 0;
const { data, report } = migrate(storedDocument, {
// 1. data rules — old data shape → new data shape, by block type
migrations: {
myCard: (d) => ('name' in d ? { ...d, title: d.name } : d),
},
// 2. grammar rules — structural: type changes, 1:N splits, sibling absorption
rules: [alertRule],
generateId: () => `blk-${n++}`,
});
report.lossyFields; // [{ blockType: 'linkTool', field: 'meta.site_name', verb: 'dropped' }]
report.errors; // [{ type: 'myCard', error }] — that block kept its stored datarules (custom legacy grammar entries)
LegacyGrammarEntry[]Научите механизм миграции legacy-форме, которую Blok не знает. Запись грамматики выглядит так: { legacyType, detect, expand, targetType, cardinality, contributesNesting, lossyFields, docNote }.
Передача записей через rules переиспользует весь интерпретатор: рекурсию в тела контейнеров, инвариант переподчинения сирот, разбиения 1:N и выпуск id. Вам не нужно заново реализовывать цикл диспетчеризации вокруг правила, которое работает только с данными.
Записи хоста сопоставляются ДО встроенной таблицы, поэтому ими можно ещё и переопределить встроенное сопоставление. В отличие от правила migrations, запись может изменить type блока и выдать несколько блоков.
expand(block, ctx, { siblings, index }) может также вернуть { blocks, consumed }, чтобы поглотить следующие consumed соседних блоков. Это форма, которая нужна legacy-форматам «плоский со счётчиком», где контейнер хранит своё тело как «следующие N блоков». Значение consumed ограничивается тем, что осталось, поэтому усечённый документ не может поглотить лишнего.
Прочитайте LEGACY_GRAMMAR, чтобы изучить встроенное покрытие.
Когда использовать
Правило контейнера, тело которого хранится как КОЛИЧЕСТВО следующих соседних блоков, возвращает { blocks, consumed }. Интерпретатор пропускает ровно столько соседей, поэтому дочерние блоки переподчиняются один раз и никогда не выдаются дважды.
import { migrate, LEGACY_GRAMMAR, type LegacyGrammarEntry } from '@bloklabs/core/migrate';
// A legacy `alert` → callout + child paragraph (type change AND a 1:N split).
// The annotation is load-bearing: without it `cardinality` widens to `string`
// and `detect`/`expand` lose their contextual parameter types.
const alertRule: LegacyGrammarEntry = {
legacyType: 'alert',
targetType: 'callout',
cardinality: '1:N',
contributesNesting: true,
lossyFields: [],
docNote: '`alert` → `callout` + message paragraph.',
detect: (block) => block.type === 'alert',
expand: (block, ctx) => {
const calloutId = block.id ?? ctx.generateId();
const childId = ctx.generateId();
return [
{ id: calloutId, type: 'callout', data: { emoji: '🚨' }, content: [childId] },
{ id: childId, type: 'paragraph', data: { text: block.data.message }, parent: calloutId },
];
},
};
const { data } = migrate(storedDocument, { rules: [alertRule] });
// What does Blok migrate out of the box?
LEGACY_GRAMMAR.map((entry) => [entry.legacyType, entry.targetType, entry.lossyFields]);migrateToRichText(data, options?)
OutputDataПереводит поля с форматированным HTML-текстом в сохранённом документе во фрагменты, ту форму, которую возвращает save(). Экспортируется из @bloklabs/core/migrate и работает в Node без DOM.
Редактор переводит старый документ при следующем сохранении. До этого в базе данных могут лежать обе формы. Запустите эту функцию, чтобы перевести сохранённые документы разом, и запускайте её до типизированного чтения, например через blocksOfType.
Переводятся только встроенные типы блоков. Поля собственного инструмента остаются как есть. Старые формы Editor.js тоже переводятся, поэтому migrate() можно запускать до или после. Поля, где уже лежат фрагменты, проходят без изменений, поэтому повторный запуск безопасен.
onLossy сообщает о разметке, для которой нет простой отметки.
html-embed: разметка сохранена как есть во фрагменте{ embed: { html } }.custom-mark: неизвестный тег сохранён как отметкаtag:<name>.
richTextToHtml(rich) превращает фрагменты обратно в канонический HTML. Для тех же фрагментов редактор может сохранить другую строку. При отрисовке он заменяет произвольный цвет ближайшим пресетом и может добавить ссылке target и rel.
richTextToHtml не очищает разметку. Фрагменты могут прийти от соавторов, сервера или хоста. Чтобы показать их, используйте blocksToHtml из @bloklabs/core/view: эта функция очищает разметку.
richTextToPlainText(rich) возвращает простой текст.
import { migrateToRichText, richTextToHtml, richTextToPlainText } from '@bloklabs/core/migrate';
const converted = migrateToRichText(storedDocument, {
onLossy: ({ blockType, field, reason }) => report(blockType, field, reason),
});
richTextToHtml([{ text: 'Hi ' }, { text: 'there', marks: { bold: true } }]); // 'Hi <strong>there</strong>'
richTextToPlainText([{ text: 'Hi ' }, { text: 'there', marks: { bold: true } }]); // 'Hi there'// Save editor content
const data = await editor.save();
// Result structure:
interface OutputData {
id?: string; // Document id, used by tab sync
version?: string; // Editor version
time?: number; // Save timestamp
blocks: OutputBlockData[]; // Array of block data
}
// Example output:
{
"version": "1.16.0",
"time": 1704067200000,
"blocks": [
{
"id": "p6QK0Xz1Ab",
"type": "paragraph",
"data": { "text": [{ "text": "Hello, world!" }] }
},
{
"id": "hM3lTn9RdC",
"type": "header",
"data": { "text": [{ "text": "Title" }], "level": 2 }
}
]
}OutputData
| Свойство | Описание | |
|---|---|---|
id | string (optional) | Id документа. Если у загруженного документа его нет, Blok записывает его при первом сохранении и дальше не меняет. По нему синхронизация вкладок находит другие вкладки с этим документом. |
version | string (optional) | Версия редактора |
time | number (optional) | Метка времени сохранения |
blocks | OutputBlockData[] | Массив данных блоков |