calternal_notes_core::markdown
Block-level markdown (de)serialization for the editor boundary (DESIGN §2, §4.1). This is not a full CommonMark inline parser: the live editing tree lives in TS (TipTap/ProseMirror); Rust splits a note into coarse blocks and re-joins them verbatim so round-trip is byte-for-byte for untouched content.
Each Block holds the exact source span (text) and a classified
BlockKind. Content blocks are separated in the source by blank-line
boundaries.
BLANK-LINE PRESERVATION (#49). Authors use blank lines for deliberate
vertical spacing; the editor expresses each as an EMPTY paragraph. We model
that 1:1: between two content blocks a gap of K blank lines (K ≥ 1) is
parsed into K - 1 empty Paragraph blocks (text=“”), and serialize_blocks
reproduces exactly K blank lines. So:
K = 1(the conventional single separator) → 0 empty blocks → re-emits one blank line: existing, untouched content round-trips BYTE-FOR-BYTE and is never rewritten.K = 2→ 1 empty paragraph → re-emits 2 blank lines, and so on.
The mapping K ↔ (K-1) empties is a bijection, so the round-trip is byte-exact AND idempotent for any interior blank run (no normalization of spacing). Leading/trailing blank runs are dropped (no edge empty blocks).
A lazy ^block-id trailing anchor on a block’s last line is lifted into
Block.id and re-emitted on serialize (DESIGN §4.1 “blocks carry lazy
^block-id anchors”). The block-ID character rule is shared with Task
checkbox parsing so both projections keep the same stable identity.
Source: crates/calternal-notes-core/src/markdown.rs
Structs
Section titled “Structs”pub struct BlockA lossless Markdown block with an optional trailing block identity.
Fields
pub id: Option<String>pub kind: BlockKindpub text: Stringpub children: Vec<Block>
Implements: Debug, Clone, PartialEq, Eq
Block::new
Section titled “Block::new”pub fn new(kind: BlockKind, text: impl Into<String>) -> SelfNo doc comment.
Source: crates/calternal-notes-core/src/markdown.rs:44
BlockKind
Section titled “BlockKind”pub enum BlockKindCoarse editor block kinds used for Markdown canonicalization.
Variants
HeadingParagraphListTaskCodeTableCalloutEvent
Implements: Debug, Clone, Copy, PartialEq, Eq
Source: crates/calternal-notes-core/src/markdown.rs:31
Functions
Section titled “Functions”canonicalize_markdown
Section titled “canonicalize_markdown”pub fn canonicalize_markdown(text: &str) -> StringCanonicalize one markdown body at the editor boundary.
This is intentionally the exact legacy oracle expressed as one Rust-side
operation: serialize_blocks(parse_markdown(text)). Keeping the composition
here means the editor pays for one WASM call and never materializes the
intermediate Block[] in JavaScript. The API returns a UTF-8 Rust String
because the editor already owns a JavaScript string: a Uint8Array or raw
pointer would still require a full decode back to UTF-16 at the call site,
while adding allocation/lifetime and unsafe-memory ownership rules. A plain
string therefore minimises the boundary work for this text-in/text-out API.
Source: crates/calternal-notes-core/src/markdown.rs:148
parse_markdown
Section titled “parse_markdown”pub fn parse_markdown(text: &str) -> Vec<Block>Parse a markdown note into a flat list of blocks (paragraph-level granularity). Content blocks are delimited by blank lines; interior blank runs beyond the single separator become empty Paragraph blocks so the author’s vertical spacing survives (see module docs / #49).
Round-trip: serialize_blocks(parse_markdown(x)) equals x byte-for-byte
for any x whose only blank-line irregularity is interior (no leading or
trailing blank runs) — which covers everything the editor produces.
Source: crates/calternal-notes-core/src/markdown.rs:70
serialize_blocks
Section titled “serialize_blocks”pub fn serialize_blocks(blocks: &[Block]) -> StringRe-join blocks into a markdown string. Each content block’s verbatim text
is emitted; a trailing ^id anchor is appended when Block.id is set and
the text doesn’t already carry it. Content blocks are separated by one blank
line; each empty-paragraph block between them adds ONE additional blank line
(the inverse of parse_markdown — see module docs / #49). Leading/trailing
empty-paragraph blocks are dropped so there are no edge blank lines.