Skip to main content

Crate docspec_blocknote_writer

Crate docspec_blocknote_writer 

Source
Expand description

DocSpec event stream to BlockNote JSON writer.

This crate provides a streaming BlockNoteWriter that implements EventSink to convert DocSpec event streams into BlockNote JSON format. BlockNote is a block-based rich text editor format.

§Design

The writer emits JSON tokens directly to the underlying Write as events arrive using docspec-json for streaming JSON output. For text and URI-based images, memory usage is constant regardless of document size. Local buffering is used only for bounded conversion details such as asset-based image data URI encoding and lifting nested table event substreams after their enclosing table closes.

§Supported Events

  • StartDocument / EndDocument — array start/end
  • StartHeading / EndHeading — heading blocks
  • StartParagraph / EndParagraph — paragraph blocks
  • StartBlockQuote / EndBlockQuote — quote blocks
  • StartPreformatted / EndPreformatted — code blocks
  • StartTable / EndTable — table blocks
  • StartTableRow / EndTableRow — table rows
  • StartTableCell / EndTableCell — table cells (data)
  • StartTableHeader / EndTableHeader — table cells (header, emitted identically to data cells)
  • StartTextStyle / EndTextStyle — inline style spans
  • Text — inline text content styled by currently open style spans
  • Image — image blocks
  • LineBreak / SoftBreak — line breaks within content blocks
  • ThematicBreak — divider blocks
  • StartOrderedListItem / EndOrderedListItem — numberedListItem blocks with optional start prop
  • StartUnorderedListItem / EndUnorderedListItem — bulletListItem blocks

§Table Cell Content Semantics

BlockNote’s tableCell.content is InlineContent[] — it cannot hold block-level types. The docspec_core::event well-formedness rules declare that DocSpec cells may contain any block element, so this writer flattens block-level events that appear inside a cell:

Nested tables (a StartTable inside a cell) are buffered between the first nested StartTable and its matching EndTable, then replayed through the writer after the enclosing outermost table closes. Each lifted nested table emits as a top-level sibling block in document order. The buffer empties on every outer EndTable, so nesting any number of levels deep collapses to a flat top-level sequence: A containing B containing C emits as A, B, C. Inline text adjacent to a nested table in the same outer cell stays in that outer cell.

Images encountered inside a cell are likewise buffered and replayed as top-level sibling blocks after the enclosing outermost table closes. Their order in the output mirrors the order they appeared inside the table (interleaved with any lifted nested tables in document order). Inline text adjacent to a lifted image in the same cell stays in that cell — only the image moves out. Images inside a nested cell lift through the recursive drain: they emit as siblings of the (already-lifted) nested table.

§List Support

Required: wrap BlockNoteWriter in StackTrackingSink before feeding list events. Raw list events without that wrapper are undefined behavior. StackTrackingSink auto-inserts StartParagraph inside list items, which is how the writer knows where each item’s inline content begins.

DocSpec list events translate to BlockNote block types as follows:

Nesting uses BlockNote’s native children: Block[] arrays. DocSpec’s level: u32 field drives the nesting depth: a level increase opens a new children array; a level decrease closes the appropriate number of open items and children arrays.

Multi-paragraph items: the first paragraph’s inline content populates the list item’s content[] array. Each subsequent paragraph becomes a child paragraph block inside the item’s children[] array.

Non-paragraph blocks inside list items (headings, images, code blocks, blockquotes, tables, thematic breaks) are dropped silently along with all of their inline contents — including any Text events nested within them. The drop applies anywhere inside a list item: both in the inline content[] slot (around the first paragraph) and after the item has transitioned to children[] (for multi-paragraph items or items containing nested lists). This differs from the table cell policy: cells preserve Text while absorbing block boundaries (so a heading inside a cell becomes plain text), whereas list items suppress text inside dropped blocks entirely.

§Container Interactions

  • Inside a table cell: list items are dropped entirely, consistent with other block-level content inside cells.
  • Inside a blockquote: the blockquote is force-closed and the list item is emitted at the top level as a sibling. This matches the existing sibling-emit behavior for headings and images inside blockquotes.

§Out of Scope

  • checkListItem: requires upstream DocSpec event support not yet defined.
  • toggleListItem: no DocSpec event equivalent exists.
  • Custom style_type markers: BlockNote’s default schema has no equivalent field; the style_type value from StartOrderedListItem / StartUnorderedListItem is silently dropped.

§Example

use docspec_blocknote_writer::BlockNoteWriter;
use docspec_core::{Event, EventSink, ListStyleType, StackTrackingSink};

let mut buf = Vec::<u8>::new();
let mut writer = StackTrackingSink::new(BlockNoteWriter::new(&mut buf));

writer.handle_event(Event::StartDocument { id: None, language: None, metadata: None })?;

// Plain paragraph
writer.handle_event(Event::StartParagraph { alignment: None, id: None })?;
writer.handle_event(Event::Text {
    content: "Hello".to_string(),
})?;
writer.handle_event(Event::EndParagraph)?;

// Unordered list item (StackTrackingSink auto-inserts the paragraph)
writer.handle_event(Event::StartUnorderedListItem {
    id: None,
    level: 0,
    style_type: ListStyleType::Disc,
})?;
writer.handle_event(Event::Text {
    content: "First bullet".to_string(),
})?;
writer.handle_event(Event::EndUnorderedListItem)?;

// Ordered list item with explicit start number
writer.handle_event(Event::StartOrderedListItem {
    id: None,
    level: 0,
    start: Some(1),
    style_type: ListStyleType::Decimal,
})?;
writer.handle_event(Event::Text {
    content: "Step one".to_string(),
})?;
writer.handle_event(Event::EndOrderedListItem)?;

writer.handle_event(Event::EndDocument)?;
writer.finish()?;

let json = String::from_utf8(buf)?;
assert!(json.starts_with('['));

Modules§

palette
BlockNote default color palette and nearest-color snapping.

Structs§

BlockNoteWriter
A streaming BlockNote JSON writer.