View-рендерер: показ документов без редактора
Показывайте сохранённые документы, не платя за редактор. Подпуть @bloklabs/core/view превращает OutputData в семантический HTML или простой текст.
Он работает синхронно и без DOM, поэтому запускается в Node, воркерах и React Server Components. Страницам «только для чтения» (публикации, превью, поисковая индексация, письма) больше не нужны экземпляр редактора, его бандл и асинхронное ожидание готовности.
Каждое строчное поле санитизируется по составленному allowlist до подстановки. Политика URL-схем такая же, как в редакторе. Используйте функции вместе с defineBlokSchema: тогда документы отображаются под тем же составом санитизации, который их создал. Если набор строчных инструментов меняется на лету через tools.setInlineToolbar, пересоберите схему, чтобы отображение не отстало.
В React для отображения берите <BlokView> или useBlokView без обёртки. Не берите <BlokEditor readOnly>: он отгружает каждому читателю полный редакторский рантайм (панель инструментов, историю, механику мутаций).
Вывод по умолчанию идёт без стилей.
- Для паритета с редактором включите classes и root вместе с опциональным @bloklabs/core/view.css.
- Для бесклассовой базы возьмите только toolAttributes с той же таблицей стилей: она воспроизводит межблочные отступы редактора из тех же токенов --blok-block-padding-*.
Включите blockIds для глубоких ссылок «скопировать ссылку на блок» и передайте transformUrl, чтобы переписывать href и URL изображений в CDN.
Как получить экземпляр редактора
Методы ниже вызываются на редакторе, созданном через 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');Методы
blocksToHtml(data, options?)
stringОтрисовывает сохранённый документ в семантический HTML. Работает синхронно и без DOM, поэтому безопасен в Node, воркерах и React Server Components.
Каждое строчное поле санитизируется по составленному allowlist до подстановки. Политика URL-схем такая же, как в редакторе. Возвращает '' для пустых или некорректных документов (nullish на входе допускается).
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
data | OutputData | LooseOutputData | null | undefined | Обязательный | — | Сохранённый документ в строгом формате save() или в нестрогом формате передачи. |
options.schema | BlokViewSchema | — | — | viewSchema из defineBlokSchema. В нём один составленный baseSanitize, собранный из включённых СТРОЧНЫХ ИНСТРУМЕНТОВ и ТЮНОВ, а не из собственных static sanitize блочных инструментов. Он накладывается на стандартный строчный allowlist рендерера, поэтому строчное содержимое отображается под тем же составом, который его создал. viewSchema.tools переносится для потребителей, но рендерер его не читает. Чтобы управлять разметкой собственного блока, используйте renderers. |
options.renderers | Record<string, (data, ctx) => string> | — | — | Собственные рендереры для отдельных инструментов. Такой рендерер побеждает встроенный эмиттер для своего имени инструмента. ctx даёт sanitizeInline (санитизировать строчную HTML-строку), renderBlocks (отрисовать произвольный массив блоков), plainText (простой текст HTML-строки) и renderChildren (отрисовать структурные дочерние блоки текущего блока). Поэтому собственный вывод безопасно сочетается с остальным документом. |
options.inlineRenderers | Record<string, (element) => string | undefined> | — | — | Собственные рендереры для СТРОЧНЫХ элементов с ключами по имени тега в нижнем регистре. Это строчный аналог renderers для меток, отображение которых не совпадает с их сохранённой разметкой: строчная формула хранит только свой исходник LaTeX, а упоминание только id. Каждый рендерер выполняется после санитизации, по пережившим её элементам, и ЗАМЕНЯЕТ элемент тем, что вернёт. Верните undefined, чтобы оставить элемент таким, каким его оставила санитизация, или '', чтобы удалить его. Возвращённая разметка вставляется как есть и повторно НЕ санитизируется. Это тот же контракт доверия, что и у вывода блочного рендерера, поэтому она может нести разметку, которую строчный allowlist вырезал бы (например, span-элементы KaTeX). У element такая форма: { tag, attrs, html, text }. Это касается только отрисованного HTML: blocksToPlainText читает сохранённый исходник метки, а не её отрисовку. |
options.onUnknownBlock | 'skip' | 'comment' | — | 'skip' | Что делать с блоком, для инструмента которого нет рендерера: молча отбросить его или оставить в выводе маркер-комментарий HTML. |
options.toolAttributes | boolean | — | false | Проставляет на корне каждого блока атрибут data-blok-tool со значением <type> (для последовательностей списков на их <ul>/<ol>) как зацепку для стилей. Подключите опциональный @bloklabs/core/view.css, чтобы воспроизвести межблочные отступы редактора из тех же токенов --blok-block-padding-*, а не восстанавливать их CSS по голым тегам. Атрибут ставится только на встроенной разметке Blok. Собственные рендереры и голые контейнеры (database) остаются нетронутыми. |
options.blockIds | boolean | — | false | Проставляет на корне каждого блока атрибут data-blok-id со значением <id>, а на элементах списка на их <li>, а не на сгруппированном <ul>/<ol>. Так глубокие ссылки «скопировать ссылку на блок» работают и вне живого редактора. Блоки без id остаются без атрибута, как и голые контейнеры, которые не выдают собственного корня (database). |
options.transformUrl | (url, ctx) => string | — | — | Чистый хук переписывания URL. Он применяется к каждому URL блока: src изображения, видео и аудио, href файла, закладки и встраивания. Он также применяется к каждому href строчной ссылки. Так вы можете переписывать href или направлять URL изображений в CDN. ctx имеет вид { attr: 'href' | 'src', blockType?: string }, а для строчных ссылок blockType равен undefined. Хук выполняется ДО вырезания небезопасных схем, поэтому переписывание никогда не сможет заново внести опасный сток javascript:/data:. Возврат '' удаляет URL. |
options.root | boolean | — | false | Оборачивает вывод в <div> с атрибутом data-blok-interface со значением view. Это не косметика. Ограниченный областью preflight и слои токенов и цветов опираются на голый атрибут [data-blok-interface]. Без обёртки выдаваемые классы вычисляются иначе, и @bloklabs/core/view.css не может воспроизвести внешний вид редактора. Опция включается явно, потому что добавляет элемент в существующий вывод. <BlokView> проставляет атрибут на своей обёртке, поэтому потребители React её никогда не задают. |
options.classes | boolean | — | false | Отрисовывает блоки с презентационными классами редактора и поблочным каркасом holder → content, чтобы результат совпадал с рендером редактора в режиме только для чтения. Чтобы это действительно отрисовалось, нужны @bloklabs/core/view.css и root: true (или предок с [data-blok-interface]). Под этим флагом несколько инструментов получают ещё и элемент-обёртку. <BlokView> включает опцию по умолчанию, хук useBlokView нет. |
import { blocksToHtml } from '@bloklabs/core/view';
const html = blocksToHtml(savedData, {
schema: schema.viewSchema,
onUnknownBlock: 'comment',
renderers: {
// Wins over the built-in paragraph emitter
paragraph: (data, ctx) =>
`<p class="lead">${ctx.sanitizeInline(String(data.text ?? ''))}</p>`,
},
});blocksToPlainText(data, options?)
stringИзвлекает простой текст сохранённого документа. Блоки разделяются \n\n, элементы списка \n, ячейки таблицы \t. Работает синхронно и без DOM, с теми же опциями, что у blocksToHtml, плюс includeHiddenText. Используйте для превью, поисковой индексации и подсчёта символов.
Когда использовать
В ядре нет хелпера «размер документа», потому что два размера, которые можно иметь в виду, измеряются по-разному.
blocksToPlainText(data).lengthэто длина видимого содержимого, то есть того, что напечатал пользователь.new TextEncoder().encode(JSON.stringify(data)).lengthэто размер при передаче в байтах, то есть то, что должен проверять лимит сохранения или загрузки (например, 500KB).
Используйте длину простого текста для правил по содержимому, а длину JSON в байтах для правил по хранению.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
data | OutputData | LooseOutputData | null | undefined | Обязательный | — | Сохранённый документ в строгом формате save() или в нестрогом формате передачи. |
options.includeHiddenText | boolean | — | — | Читает также медиатекст, который вывод по умолчанию опускает, потому что редактор рисует его как атрибут или не рисует вовсе. Это alt изображения, url видео или файла, source встраивания, title, artist и url аудиодорожки, description и url закладки. Каждое значение добавляется после видимой подписи блока, по одному в строке. По умолчанию опция выключена, поэтому стандартный вывод остаётся ровно тем, что читатель видит на экране. Включайте её для поискового индекса, где люди ищут и alt-текст, и вставленный URL. |
import { blocksToPlainText } from '@bloklabs/core/view';
// A 160-character preview for a card or meta description
const preview = blocksToPlainText(savedData).slice(0, 160);
// Content length (characters the user typed) vs transport size (bytes on the wire)
const contentLength = blocksToPlainText(savedData).length;
const transportBytes = new TextEncoder().encode(JSON.stringify(savedData)).length;
if (transportBytes > 500 * 1024) {
throw new Error('Document exceeds the 500KB save limit');
}blocksToMarkdown(data)
stringСериализует сохранённый документ в Markdown. Работает синхронно и без DOM, это исходящий близнец markdownToBlocks. Заголовки становятся #, списки -/1., задачи - [x], таблицы сетками из вертикальных черт в стиле GFM.
В Markdown нет выноски, тоггла, колонки и отступа. Поэтому выноска становится цитатой с её эмодзи, тоггл жирной строкой-сводкой, за которой идёт его тело, колонки схлопываются в порядок чтения, а отступ отбрасывается.
Возвращает '' для пустых или некорректных документов.
import { blocksToMarkdown } from '@bloklabs/core/view';
// Feed an article to an LLM, or write it to a .md file
const markdown = blocksToMarkdown(savedData);blocksToMarkdownWithReport(data)
{ markdown: string; warnings: MarkdownDegradation[] }Тот же Markdown плюс список того, что не удалось перенести. Каждая запись называет конструкцию, говорит, было ли это 'dropped' (ничего не выведено) или 'degraded' (выведено с потерями), и почему.
Берите его, когда результат уходит туда, где нельзя задать уточняющий вопрос, например в AI-клиент или в экспорт, и где нужно сообщить, чего не хватает. Блок, который не даёт вывода и не несёт строчного текста, тоже попадает в отчёт, поэтому собственный инструмент без формы в Markdown называется по имени, а не исчезает.
import { blocksToMarkdownWithReport } from '@bloklabs/core/view';
const { markdown, warnings } = blocksToMarkdownWithReport(savedData);
// warnings: [{ construct: 'callout', action: 'degraded', detail: 'callout is rendered as a blockquote; …' }]htmlTextContent(html)
stringИзвлекает простой текст HTML-фрагмента. Работает синхронно и без DOM, это замена element.textContent в View-рендерере. Сущности декодируются (a < b → a < b), а <br> превращается в перевод строки. Используйте функцию вместо самодельного вырезания через DOMParser (которому нужен DOM), когда сводите строчное HTML-поле к тексту.
import { htmlTextContent } from '@bloklabs/core/view';
htmlTextContent('<b>Intro</b> & more'); // → 'Intro & more'sanitizeHtmlFragment(html, config)
stringОчищает HTML-фрагмент по конфигурации санитайзера без DOM. Работает на parse5 и повторяет семантику html-janitor из редактора. config это allowlist вида тег → правило либо маркер 'plaintext', полностью вырезающий разметку. Это аналог api.sanitizer.clean() без DOM, поэтому используйте его в Node, воркерах и RSC, где санитайзер редактора работать не может.
import { sanitizeHtmlFragment } from '@bloklabs/core/view';
sanitizeHtmlFragment('<b>bold</b><script>x()</script>', { b: {} });
// → '<b>bold</b>'outlineFromOutputData(data)
OutlineItem[]Извлекает структуру заголовков сохранённого документа, источник для оглавления. Работает синхронно и без DOM. Обходит документ в порядке чтения (блоки верхнего уровня, затем структурные дочерние блоки), выбирает блоки header и сводит строчный HTML каждого заголовка к простому тексту.
Каждый элемент имеет вид { id?, level, text }. id блока задаёт якорные ссылки и цели прокрутки, поэтому отдельный проход DOMParser не нужен. Заголовки с пустым текстом пропускаются.
import { outlineFromOutputData } from '@bloklabs/core/view';
const toc = outlineFromOutputData(savedData);
// → [{ id: 'h1', level: 1, text: 'Getting Started' }, ...]restoreHeadingAnchors(data)
{ data, report }Чинит внутридокументные ссылки, чья цель потерялась при импорте. HTML адресует собственные разделы по id на заголовке. Google Docs пишет <h2> с id вида h.2y1ok8y7pef0 и ведёт своё оглавление на этот фрагмент.
Конвертер, который выпускает собственные id блоков и выбрасывает исходные, оставляет такие ссылки указывающими в никуда. Текст ссылки по-прежнему называет заголовок. Поэтому этот проход отдаёт каждый мёртвый фрагмент тому заголовку, который назван в тексте, как HeaderData.anchor.
Поскольку проход пишет содержимое, он гадает как можно меньше.
- Он берёт только заголовки, у которых ещё нет якоря.
- Ему нужно точное совпадение текста. Разметка, сущности и пробелы нормализуются, пунктуация нет.
- Он срабатывает, только когда ровно один заголовок и ровно один фрагмент претендуют друг на друга.
Всё менее надёжное остаётся нетронутым и попадает в report.skipped. Повторный запуск ничего больше не меняет.
Вызывайте проход сами как разовое обновление: в автоматический путь загрузки он не входит. Он работает без DOM, поэтому запускается в Node-скрипте по сохранённым записям. Сначала мигрируйте legacy-данные.
import { restoreHeadingAnchors } from '@bloklabs/core/view';
const { data, report } = restoreHeadingAnchors(savedData);
// report.restored → [{ anchor: 'h.2y1ok8y7pef0', blockId: 'header-18' }, ...]
// report.skipped → [{ anchor: 'h.other', reason: 'ambiguous' }]
await save(data);defineBlokSchema(config)
{ editorConfig, viewSchema }Сводит конфигурацию tools, inlineToolbar и tunes в одну общую схему. Функция чистая и безопасная на уровне модуля: вызывайте её в области модуля и импортируйте результат везде.
Разложите editorConfig в new Blok(...) и передайте viewSchema во view-функции. Это гарантирует, что документы отображаются под ТЕМ ЖЕ составом санитизации, который их создал, а не под двумя разошедшимися конфигурациями.
Гарантия действует в рамках одного состава: если вы меняете набор строчных инструментов в рантайме через tools.setInlineToolbar, пересоберите схему из текущей конфигурации. Опции, которые не участвуют в разрешении схемы (link, i18n, data, …), проходят в editorConfig без изменений.
import { defineBlokSchema, blocksToHtml } from '@bloklabs/core/view';
import { Header, Paragraph } from '@bloklabs/core/tools';
const schema = defineBlokSchema({
tools: { paragraph: Paragraph, header: Header },
});
const editor = new Blok({ holder: 'editor', ...schema.editorConfig });
const html = blocksToHtml(savedData, { schema: schema.viewSchema });composeBaseSanitizeConfig(configs)
SanitizerConfigСворачивает упорядоченный список конфигураций санитизации с точной семантикой слияния редактора: Object.assign, где побеждает более поздний, сначала строчные инструменты, затем тюны. Правила-функции переносятся по ссылке. Правила для одного и того же тега ЗАМЕНЯЮТСЯ и никогда не сливаются вглубь.
Это та же свёртка, которой defineBlokSchema строит viewSchema.baseSanitize, открытая для списков, собранных вручную. Экспортируется и из @bloklabs/core, и из @bloklabs/core/view.
import { composeBaseSanitizeConfig } from '@bloklabs/core/view';
const baseSanitize = composeBaseSanitizeConfig([
{ b: {}, i: {} },
{ a: { href: true } },
]);
// → { b: {}, i: {}, a: { href: true } }blocksToViewNodes(data, options?)
ViewNode[]Отрисовывает в независимое от фреймворка JSON-дерево вместо строки HTML. Каждый узел имеет вид { tag, attrs, children } или { text }, с теми же опциями и конвейером санитизации, что у blocksToHtml. Именно это React-биндинги превращают в настоящие элементы.
Функция экспериментальная: форма не зафиксирована, пока её не начнёт использовать второй адаптер фреймворка, поэтому она может измениться в минорном релизе.
import { blocksToViewNodes } from '@bloklabs/core/view';
const nodes = blocksToViewNodes(savedData);
// → [{ tag: 'p', attrs: {}, children: [{ text: 'Hello' }] }]renderLatex(latex, options?)
Promise<string>Отрисовывает строку LaTeX в HTML той сборкой KaTeX, которую Blok уже включает в бандл. Её используют инструмент кода, строчный инструмент формул и импортёр Markdown.
Она усилена для недоверенного ввода.
- Опция trust: false запрещает команды, внедряющие разметку (\href, \includegraphics, \html*).
- Опция maxExpand ограничивает раскрытие макросов.
- Опция maxSize ограничивает размеры элементов.
- Опция throwOnError: false отрисовывает некорректную формулу как экранированный исходник, вместо того чтобы сломать документ.
Берите её вместо того, чтобы добавлять katex собственной зависимостью: это вторая копия библиотеки и второй, непроверенный набор опций.
KaTeX импортируется лениво при первом вызове. Если документа нет (SSR, воркеры), вставка таблицы стилей пропускается. Тогда katex.min.css вы подключаете сами, а разметка получается идентичной.
Для inlineRenderers используйте createLatexRenderer, он синхронный.
import { renderLatex } from '@bloklabs/core/view';
const html = await renderLatex('c = \\pm\\sqrt{a^2 + b^2}', { displayMode: false });createLatexRenderer()
Promise<(latex: string, options?: LatexRenderOptions) => string>Загружает KaTeX один раз и возвращает синхронный рендерер LaTeX. Именно эта форма нужна inlineRenderers, ведь он заменяет элемент строкой, которую тот вернул, а промис там превращается в [object Promise].
Устроено как «дождитесь загрузчика, получите рендерер», поэтому в порядке вызовов невозможно ошибиться: рендерер не может появиться раньше, чем готов KaTeX. Опции усилены так же, как в renderLatex.
Именно это позволяет отображающей поверхности отрисовывать формулы через blocksToHtml/BlokView, а не поднимать ради них редактор в режиме только для чтения.
import { blocksToHtml, createLatexRenderer } from '@bloklabs/core/view';
const renderLatexSync = await createLatexRenderer();
const html = blocksToHtml(savedData, {
inlineRenderers: {
span: ({ attrs }) => attrs['data-latex'] === undefined
? undefined
: renderLatexSync(attrs['data-latex'], { displayMode: false }),
},
});BlokView
ReactNodeReact-компонент отображения из @bloklabs/react. Он отрисовывает сохранённый документ внутри одной <div>-обёртки: без экземпляра редактора, без обрамления, без асинхронности и без эффектов. И никогда через dangerouslySetInnerHTML: содержимое переносится из санитизированного view-дерева в настоящие React-элементы.
Обёртка всегда несёт data-blok-interface со значением view. Именно поэтому выдаваемые классы вычисляются так же, как в редакторе. Атрибут пишется ДО разложения divProps, поэтому вызывающий код может его переопределить. Но никогда не меняйте его на «blok»: тот несёт all: initial !important и заблокировал бы типографику хоста.
Там, где нужно лишь отображение, берите этот путь «только для чтения», а не <BlokEditor readOnly>. Он не стоит редакторского бандла, у него нет защёлки готовности, и он одинаково отрисовывается под SSR.
Его API пропсов стабилен. Экспериментальным остаётся только сырое дерево ViewNode, из которого он строит вывод (через blocksToViewNodes). Использование BlokView вас с ним никогда не сталкивает.
Параметры
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
data | OutputData | LooseOutputData | null | undefined | Обязательный | — | Сохранённый документ для отображения (nullish допускается). |
schema | BlokViewSchema | — | — | viewSchema из defineBlokSchema, чтобы документ отображался под тем составом, который его создал. |
renderers | Record<string, (data, ctx) => string> | — | — | Собственные рендереры для отдельных инструментов. Они побеждают встроенные. |
onUnknownBlock | 'skip' | 'comment' | — | 'skip' | Политика для неизвестных инструментов (маркеры 'comment' в React-дереве отбрасываются). |
toolAttributes | boolean | — | false | Проставлять data-blok-tool на корне каждого блока (работает в паре с @bloklabs/core/view.css). Передаётся в blocksToHtml. |
blockIds | boolean | — | false | Проставлять data-blok-id на корне каждого блока (элементы списка на их <li>) для глубоких ссылок «скопировать ссылку на блок». |
transformUrl | (url, ctx) => string | — | — | Хук переписывания URL для URL блоков и строчных ссылок, выполняется до вырезания небезопасных схем. Передаётся в blocksToHtml. |
inlineRenderers | Record<string, (element) => string | undefined> | — | — | Рендереры по тегам для СТРОЧНЫХ элементов. Через них можно отрисовать LaTeX формулы с KaTeX на сервере или превратить span упоминания в чип. Передаётся в blocksToHtml. |
classes | boolean | — | true | Отрисовывает с презентационными классами редактора и поблочным каркасом holder → content, чтобы вывод совпадал с рендером редактора в режиме только для чтения. Здесь опция включена по умолчанию, потому что компонент владеет обёрткой и его задача выглядеть как редактор. Чтобы это отрисовалось, нужен подключённый @bloklabs/core/view.css. Передайте classes={false} для семантической разметки без стилей. |
...divProps | HTMLAttributes<HTMLDivElement> | — | — | Любой стандартный атрибут <div> (className, id, style, data-*, aria-*, обработчики событий, …) передаётся на единственный элемент-обёртку. |
import { BlokView } from '@bloklabs/react';
import { schema } from './schema';
import '@bloklabs/core/view.css'; // opt-in block-spacing baseline
export function Article({ saved }: { saved: OutputData }) {
return (
<BlokView
data={saved}
schema={schema.viewSchema}
toolAttributes
blockIds
id="article-body"
className="prose"
/>
);
}useBlokView(data, options?)
ReactNodeФорма BlokView без обёртки. Она возвращает Fragment из элементов блоков, без лишнего <div>, для мест, где обёртка недопустима или нежелательна: подписей чекбоксов, ячеек таблицы, заголовков. Работает синхронно и без эффектов (безопасно для SSR), с мемоизацией по ссылке на data и по отдельным значениям опций.
Опции те же, что у blocksToHtml, кроме двух.
- root игнорируется, ведь не выдавать обёртку и есть контракт хука.
- classes здесь по умолчанию false, а в <BlokView>, который владеет обёрткой, по умолчанию true.
Передача root: true проходит проверку типов и молча ничего не делает. Когда нужно, чтобы отрисовался view.css, оберните возвращённый Fragment сами в элемент с атрибутом data-blok-interface со значением view.
import { useBlokView } from '@bloklabs/react';
function RowLabel({ saved }: { saved: OutputData }) {
const content = useBlokView(saved, { schema: schema.viewSchema });
return <label>{content}</label>;
}// schema.ts — pure and module-scope-safe; share it between editor and server
import { defineBlokSchema } from '@bloklabs/core/view';
import { Header, Paragraph, List } from '@bloklabs/core/tools';
export const schema = defineBlokSchema({
tools: { paragraph: Paragraph, header: Header, list: List },
});
// Editing side (browser)
import Blok from '@bloklabs/core';
const editor = new Blok({ holder: 'editor', ...schema.editorConfig });
// Display side — Node, a worker, an RSC, or the browser; no DOM needed
import { blocksToHtml, blocksToPlainText } from '@bloklabs/core/view';
const html = blocksToHtml(savedData, { schema: schema.viewSchema });
const preview = blocksToPlainText(savedData).slice(0, 160);