docspec-blocknote-writer 1.0.2

DocSpec event stream to BlockNote JSON writer
Documentation

docspec-blocknote-writer

Converts a DocSpec event stream into BlockNote JSON. Implements the EventSink trait and emits JSON tokens directly to any Write target as events arrive — no intermediate document representation, constant memory regardless of file size.

Supported Features

Block type BlockNote type
Paragraph paragraph
Heading (levels 1–6) heading
Block quote quote
Preformatted / code block codeBlock
Image image
Table (with header and data cells) table
Thematic break divider
Unordered list item bulletListItem
Ordered list item numberedListItem
Inline link link inline type with href (title is dropped)

Inline styles supported within text content: bold, italic, code, strikethrough, underline. Inline links (StartLink/EndLink) emit a link inline type with the href. The optional title field is dropped (BlockNote's default link schema has no title).

List items support arbitrary nesting via BlockNote's native children: Block[] arrays. The start prop on numberedListItem is preserved when the first item in a sequence carries an explicit start number.

Not Yet Supported

  • Footnotes — StartFootnote/EndFootnote/FootnoteRef are silently ignored; BlockNote's default schema has no equivalent
  • Definition lists — StartDefinitionList/StartDefinitionTerm/StartDefinitionDetail (and their End* pairs) are silently ignored; BlockNote's default schema has no equivalent
  • Captions — StartCaption/EndCaption are silently ignored
  • checkListItem — requires upstream DocSpec event support not yet defined
  • toggleListItem — no DocSpec event equivalent
  • Custom list style markers — the style_type field on StartOrderedListItem/StartUnorderedListItem is always dropped (BlockNote's default schema has no equivalent); list kind is determined entirely by the event variant (ordered vs unordered)

Usage

Wrap BlockNoteWriter in StackTrackingSink before feeding list events. The writer opens the list item's content[] array directly at Start*ListItem, and StartParagraph for the first paragraph of an item is a no-op. StackTrackingSink is still required because it normalizes paragraph boundaries (especially from sources that don't always emit an explicit StartParagraph inside list items), which is what lets the writer detect multi-paragraph items and transition emission into children[] for subsequent paragraphs.

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

// Bullet list item
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)?;

// Numbered list item
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)?;
# Ok::<(), Box<dyn std::error::Error>>(())

Limitations

Table cell content: BlockNote's tableCell.content is InlineContent[] and cannot hold block-level types. Block-level events inside a cell are handled as follows:

  • StartParagraph / EndParagraph boundaries are absorbed silently (adjacent paragraphs concatenate without separator)
  • Text and LineBreak are preserved
  • Everything else (images, headings, code blocks, blockquotes, list items, nested tables) is dropped silently

Non-paragraph children inside list items: headings, images, code blocks, and blockquotes that appear as children of a list item are dropped. The first paragraph's inline content populates the item's content[] array; each subsequent paragraph becomes a child paragraph block in children[].

Blockquote + list interaction: a list item encountered while inside a blockquote force-closes the blockquote and emits the list item at the top level as a sibling.

Image-in-link: BlockNote does not allow block-level images inside inline links. The reader closes the link before emitting the image as a sibling block, and the writer serialises that sequence directly. Content preceding the image stays inside the link; the image becomes a sibling block after the link closes; content following the image appears outside the link, losing its link wrapper. The link is empty only when the image is the sole link label, e.g. [![alt](img.png)](url). All variants are lossy mappings of the original "image-inside-link" structure.

API Documentation

See the module-level docs for the full API surface, including BlockNoteWriter and its EventSink implementation.

License

See the repository LICENSE.