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. Asset-based images (ImageSource::Asset) are base64-encoded into an in-memory data URI before writing, so memory scales with individual asset size.

§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)
  • Text — inline text content with bold/italic/code/strikethrough/underline styles
  • Image — image blocks
  • LineBreak / SoftBreak — line breaks within content blocks
  • ThematicBreak — divider blocks
  • StartOrderedListItem / EndOrderedListItemnumberedListItem blocks with optional start prop
  • StartUnorderedListItem / EndUnorderedListItembulletListItem blocks

§Table Cell Content Semantics

BlockNote’s tableCell.content is InlineContent[] — it cannot hold block-level types. EVENTS.md declares 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 entirely dropped: their rows, cells, text, and closing events are all absorbed. Only the outer table is emitted. The current markdown reader never produces multi-block cell content or nested tables — these guards exist for future DOCX/ODT readers.

§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, TextStyle};

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(),
    style: TextStyle::default(),
})?;
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(),
    style: TextStyle::default(),
})?;
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(),
    style: TextStyle::default(),
})?;
writer.handle_event(Event::EndOrderedListItem)?;

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

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

Structs§

BlockNoteWriter
A streaming BlockNote JSON writer.