This file provides context for AI assistants (like Claude) working on this codebase.
mdriver is a streaming markdown printer that renders GitHub Flavored Markdown to the terminal with ANSI escape codes. The critical requirement is incremental emission: blocks must be emitted immediately once parsed, not buffered until the entire document is complete.
Most markdown renderers are document-oriented: they parse the entire input and then render. This project requires streaming behavior where:
- Input arrives in arbitrary chunks (could be byte-by-byte, line-by-line, or paragraph-by-paragraph)
- Blocks emit as soon as they're complete (heading after
\n, paragraph after\n\n, code block after closing fence) - Incomplete blocks buffer without emitting
- The parser must maintain state across
feed()calls
This is useful for real-time rendering of markdown as it's typed or received over a network.
CRITICAL: This project maintains zero-tolerance for compiler warnings and linting errors.
-
Code Formatting
cargo fmt
Must be run before committing. The CI checks that code is properly formatted with
cargo fmt -- --check. -
No Compiler Warnings
cargo build cargo build --release
Must complete with zero warnings. If warnings appear, fix them immediately.
-
No Clippy Errors
cargo clippy --all-targets --all-features -- -D warnings
Must pass with zero errors. Clippy runs with
-D warningswhich treats warnings as errors. -
All Tests Pass
cargo testAll conformance tests must pass.
- Unused variables: Remove them or prefix with
_ - Unused imports: Remove them
- Manual string stripping: Use
strip_prefix()instead ofstarts_with()+ slicing - Unnecessary
mut: Removemutif variable is never mutated - Dead code: Add
#[allow(dead_code)]only if keeping for future use
Only use #[allow(...)] when:
- Code is intentionally kept for future features (e.g.,
infofield for syntax highlighting) - The lint is a false positive (rare, document why)
Never commit code without running cargo fmt, or with warnings or clippy errors.
Use the gh CLI tool for creating pull requests. When creating a PR:
- Export the session transcript using
/export - Create a gist with the transcript:
gh gist create <export_path>/conversation_full.md --public - Create the PR with
gh pr create, including a link to the transcript gist
PRs should include a "Session transcript" section with a link to the gist, e.g.:
## Session transcript
[Claude Code session transcript](https://gist.github.com/llimllib/...)This provides transparency and context for reviewers about how the changes were developed.
IMPORTANT: This project uses conformance tests that define exact expected behavior. The test suite was created BEFORE implementation.
The conformance tests serve three purposes:
- Specification: Define exactly how streaming should work
- Verification: Ensure implementation matches spec
- Regression prevention: Prevent streaming behavior from breaking
# All tests (currently failing - parser is stub)
cargo test
# Specific categories
cargo test test_block_fixtures # Basic block types
cargo test test_streaming_fixtures # Streaming behavior
cargo test test_ansi_fixtures # Terminal formatting
cargo test test_complex_fixtures # Real-world scenariosTests are TOML files in tests/fixtures/. Each test specifies:
name = "test-name"
description = "What this tests"
[[chunks]]
input = "markdown input"
emit = "expected output" # Empty string "" means "no emission yet"CRITICAL: Use \u001b NOT \x1b in TOML files!
# ✅ CORRECT
emit = "\u001b[1mbold\u001b[0m"
# ❌ WRONG - TOML doesn't support \x escape
emit = "\x1b[1mbold\x1b[0m"Common ANSI codes:
- Bold:
\u001b[1m...\u001b[0m - Italic:
\u001b[3m...\u001b[0m - Blue heading:
\u001b[1;34m...\u001b[0m - Code background:
\u001b[48;5;235m...\u001b[0m
CRITICAL: The authoritative specification for parsing behavior is in gfmspec.md.
Always reference gfmspec.md when:
- Implementing a new block type (headings, lists, code blocks, tables, etc.)
- Implementing inline formatting (emphasis, links, code spans, etc.)
- Handling edge cases or ambiguous input
- Unsure about parsing precedence or rules
- Debugging why a test expects certain output
The GFM spec is organized by feature. Use the Read tool to look up specific sections:
Block-level structures:
- ATX headings (lines starting with
#) - Fenced code blocks (
```) - Paragraphs and blank lines
- Lists (ordered and unordered)
- Block quotes
- Tables
- Thematic breaks
Inline structures:
- Emphasis and strong emphasis (
*and**) - Code spans (
`) - Links and images
- Autolinks
- Strikethrough (GFM extension)
Examples from spec:
- Search for specific examples (e.g., "Example 32" in the spec)
- Look at edge cases to understand boundary conditions
- Check how nesting and precedence work
The spec describes the final output of parsing, not the algorithm. For our streaming parser:
- Read the spec to understand what output should look like
- Design state machine to detect when blocks complete
- Implement incrementally - our tests are based on spec behavior
- Handle edge cases as defined in spec examples
Block boundary detection is key for streaming:
- Blank lines often terminate blocks
- Some blocks (code fences) need explicit closing
- Some blocks (paragraphs) can be interrupted by other blocks
- Understanding these rules is critical for knowing when to emit
Container blocks (blockquotes, lists) can contain other blocks:
- May need nested state tracking
- Our initial implementation can skip these if too complex
pub struct StreamingParser {
buffer: String, // Accumulates incomplete blocks
// TODO: Add parser state
}
impl StreamingParser {
/// Feed a chunk of markdown
/// Returns any completed blocks as formatted output
pub fn feed(&mut self, chunk: &str) -> String;
/// Flush remaining buffered content
pub fn flush(&mut self) -> String;
}-
State Management: Parser must track what type of block it's currently building
- In heading? In paragraph? In code block? In list?
- Need state machine or parser state tracking
-
Block Boundary Detection:
- Heading: Complete after
\n - Paragraph: Complete after
\n\n(blank line) - Code block: Complete after closing
``` - List: Complete after blank line or different block type
- Blockquote: Complete when exited
- Heading: Complete after
-
Incremental Parsing: Can't use traditional single-pass parsers
- Input is chunked arbitrarily
- Must handle partial blocks
- Examples:
- Chunk 1:
"#"→ No emission (heading incomplete) - Chunk 2:
" Hello"→ No emission (still no newline) - Chunk 3:
"\n"→ Emit heading now!
- Chunk 1:
-
ANSI Formatting: Convert markdown to terminal codes
- Inline:
**bold**→\u001b[1mbold\u001b[0m - Block: Different formatting for headings, code, etc.
- Must preserve semantic structure in terminal output
- Inline:
-
Start Simple: Make basic tests pass first
- Heading emission (test:
tests/fixtures/blocks/heading.toml) - Paragraph emission (test:
tests/fixtures/blocks/paragraph.toml)
- Heading emission (test:
-
State Machine: Build a state tracker
enum ParserState { Ready, // Not in any block InHeading, InParagraph, InCodeBlock { language: Option<String> }, InList, // etc. }
-
Chunk Processing: Process incoming chunks character-by-character or line-by-line
- Detect block boundaries
- Emit when complete
- Buffer when incomplete
-
Consider Using pulldown-cmark:
- Excellent markdown parser, but designed for complete documents
- You may need to wrap it or use it differently for streaming
- Alternatively, implement a custom streaming parser
pulldown-cmark: Full markdown parser (may need adaptation)termionorcrossterm: Terminal handlingsyntect: Syntax highlighting for code blocks
mdriver/
├── Cargo.toml
├── README.md # User-facing documentation
├── CLAUDE.md # This file - AI assistant context
├── src/
│ └── lib.rs # StreamingParser (currently stub)
├── tests/
│ ├── conformance.rs # Test runner
│ ├── common/
│ │ ├── mod.rs
│ │ └── fixture_loader.rs # Loads TOML fixtures
│ └── fixtures/
│ ├── blocks/ # Basic block tests
│ ├── streaming/ # Incremental emission tests
│ ├── ansi/ # Formatting tests
│ └── complex/ # Integration tests
When adding features, add tests FIRST:
- Create
.tomlfile in appropriatetests/fixtures/directory - Define chunks that test the streaming behavior
- Specify exact expected output with ANSI codes
- Run tests to see them fail
- Implement feature to make tests pass
Example test structure:
name = "new-feature"
description = "Tests new markdown feature"
[[chunks]]
input = "partial input"
emit = "" # Not complete yet
[[chunks]]
input = " complete\n\n"
emit = "formatted output\n" # Now it emits- TOML Escape Sequences: Always use
\u001bfor ESC, not\x1b - Test Directory Structure: Tests in
tests/are integration tests, needmod commonnotmod helpers - Streaming vs Document: Don't assume complete input - handle partial blocks
- Empty Emissions: Tests explicitly check for NO emission with
emit = "" - ANSI Reset: Always reset with
\u001b[0mafter formatting cargo runpassthrough mode: When stdout is not a terminal (e.g., piping tocat -vorxxd), mdriver defaults to--color=autowhich disables formatting and acts likecat. Usecargo run -- --color=alwaysto force markdown rendering when testing manually via pipes.
- ✅ Test Infrastructure: Complete and working (8 tests)
- ✅ Test Categories: Blocks, streaming, ANSI, complex
- ✅ Test Runner: Loads TOML, feeds chunks, validates output
- ❌ Parser Implementation: Stub only (all tests fail)
-
Basic Block Detection (src/lib.rs):
- Track parser state
- Detect heading completion (after
\n) - Emit formatted heading with ANSI codes
- Pass
tests/fixtures/blocks/heading.toml
-
Paragraph Support:
- Detect paragraph completion (after
\n\n) - Pass
tests/fixtures/blocks/paragraph.toml
- Detect paragraph completion (after
-
Code Blocks:
- Detect opening/closing fences
- Buffer content
- Emit only when closed
- Pass
tests/fixtures/blocks/code_block.toml
-
Incremental Emission:
- Ensure blocks emit separately
- Pass
tests/fixtures/streaming/incremental_emit.toml
-
Inline Formatting:
- Parse
**bold**,*italic*,`code` - Generate ANSI codes
- Pass
tests/fixtures/ansi/inline_formatting.toml
- Parse
When implementing, think about:
-
How much to buffer?
- Need enough context to parse correctly
- But emit as soon as possible
-
Error handling?
- What if invalid markdown?
- Should malformed input emit or buffer?
-
Performance?
- Byte-by-byte feeding should work but might be slow
- Can you optimize without breaking streaming?
-
GFM Extensions?
- Tables, task lists, strikethrough
- Add tests first, then implement
The test output is very helpful:
✗ heading-basic
Heading should emit after newline is received
Chunk 4 failed:
Input: "\n"
Expected: "\u{1b}[1;34m# Hello\u{1b}[0m\n"
Actual: ""
This tells you:
- Which test failed (
heading-basic) - What it's testing (description)
- Which chunk failed (4th chunk)
- What was fed (
"\n") - What should have been emitted (formatted heading with ANSI)
- What actually was emitted (nothing)
This project is about streaming and incremental rendering, not just markdown parsing. The conformance tests encode this philosophy: they verify not just final output, but the timing and chunking of emissions.
Keep this principle in mind: Emit as soon as you can, not when you must.