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/endStartHeading/EndHeading— heading blocksStartParagraph/EndParagraph— paragraph blocksStartBlockQuote/EndBlockQuote— quote blocksStartPreformatted/EndPreformatted— code blocksStartTable/EndTable— table blocksStartTableRow/EndTableRow— table rowsStartTableCell/EndTableCell— table cells (data)StartTableHeader/EndTableHeader— table cells (header, emitted identically to data cells)StartTextStyle/EndTextStyle— inline style spansText— inline text content styled by currently open style spansImage— image blocksLineBreak/SoftBreak— line breaks within content blocksThematicBreak— divider blocksStartOrderedListItem/EndOrderedListItem—numberedListItemblocks with optionalstartpropStartUnorderedListItem/EndUnorderedListItem—bulletListItemblocks
§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:
- Preserved:
StartTextStyle/EndTextStyle,Text(with currently open inline styles),LineBreak,SoftBreak - Absorbed silently:
StartParagraph/EndParagraph(paragraph boundaries are dropped — adjacent paragraphs concatenate without separator) - Dropped:
StartBlockQuote,StartPreformatted,StartHeading,ThematicBreak— silently discarded - Lifted: nested
StartTableand its children, andImageevents that appear inside a cell — buffered and replayed as top-level sibling blocks after the enclosing outermost table closes
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:
StartUnorderedListItem→bulletListItemStartOrderedListItem→numberedListItem(thestartfield, when present on the first item, becomes thestartprop)
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 upstreamDocSpecevent support not yet defined.toggleListItem: noDocSpecevent equivalent exists.- Custom
style_typemarkers:BlockNote’s default schema has no equivalent field; thestyle_typevalue fromStartOrderedListItem/StartUnorderedListItemis 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
BlockNotedefault color palette and nearest-color snapping.
Structs§
- Block
Note Writer - A streaming
BlockNoteJSON writer.