mdstream 0.3.0

Streaming-first Markdown middleware for LLM output (committed + pending blocks, render-agnostic).
Documentation
use mdstream::{FootnotesMode, MdStream, Options, ReferenceDefinitionsMode, Update};

fn assert_owned_and_borrowed_updates_match(case_name: &str, opts: Options, chunks: &[&str]) {
    let mut owned_stream = MdStream::new(opts.clone());
    let mut borrowed_stream = MdStream::new(opts);

    for (index, chunk) in chunks.iter().enumerate() {
        let owned = owned_stream.append(chunk);
        let borrowed = borrowed_stream.append_ref(chunk).to_owned();
        assert_eq!(
            borrowed, owned,
            "case={case_name} chunk_index={index} chunk={chunk:?}"
        );
    }

    let owned = owned_stream.finalize();
    let borrowed = borrowed_stream.finalize_ref().to_owned();
    assert_eq!(borrowed, owned, "case={case_name} finalize");
}

fn apply_updates(opts: Options, chunks: &[&str], borrowed: bool) -> Vec<Update> {
    let mut stream = MdStream::new(opts);
    let mut updates = Vec::new();

    for chunk in chunks {
        let update = if borrowed {
            stream.append_ref(chunk).to_owned()
        } else {
            stream.append(chunk)
        };
        updates.push(update);
    }

    let final_update = if borrowed {
        stream.finalize_ref().to_owned()
    } else {
        stream.finalize()
    };
    updates.push(final_update);
    updates
}

#[test]
fn append_and_append_ref_emit_equivalent_updates() {
    let cases = vec![
        (
            "plain_blocks",
            Options::default(),
            vec!["Hello", "\n\n", "World\n"],
        ),
        (
            "code_fence_pending_display",
            Options::default(),
            vec!["```rs\n", "fn main() {\n", "}\n"],
        ),
        (
            "reference_definition_invalidation",
            Options {
                reference_definitions: ReferenceDefinitionsMode::Invalidate,
                ..Default::default()
            },
            vec![
                "See [ref].\n\n",
                "[ref]: https://example.com\n",
                "\n",
                "Next\n",
            ],
        ),
        (
            "footnote_single_block_reset",
            Options {
                footnotes: FootnotesMode::SingleBlock,
                ..Default::default()
            },
            vec!["Intro\n\n", "[^1]: note\n", "    continued\n"],
        ),
    ];

    for (case_name, opts, chunks) in cases {
        assert_owned_and_borrowed_updates_match(case_name, opts, &chunks);
    }
}

#[test]
fn append_ref_trace_can_be_replayed_as_owned_updates() {
    let opts = Options {
        reference_definitions: ReferenceDefinitionsMode::Invalidate,
        ..Default::default()
    };
    let chunks = [
        "# Title\n\n",
        "See [Ref].\n\n",
        "[ref]: https://example.com\n",
        "\n",
        "```text\npartial",
    ];

    let owned_updates = apply_updates(opts.clone(), &chunks, false);
    let borrowed_updates = apply_updates(opts, &chunks, true);

    assert_eq!(borrowed_updates, owned_updates);
    assert!(
        borrowed_updates
            .iter()
            .any(|update| !update.invalidated.is_empty()),
        "trace should characterize reference invalidation"
    );
    assert!(
        borrowed_updates
            .iter()
            .any(|update| update.pending.as_ref().is_some_and(|pending| {
                pending
                    .display
                    .as_deref()
                    .is_some_and(|display| display.ends_with("```\n") && pending.raw != display)
            })),
        "trace should characterize incomplete fence display repair"
    );
}