14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-vault-core-and-rendering | 02 | execute | 2 |
|
|
true |
|
|
Purpose: This is the core content pipeline — every markdown construct the user sees flows through this module.
Output: src/renderer.rs with a public render_markdown(input: &str, width: u16) -> Vec<Line<'static>> function.
<execution_context> @/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md @/Users/ruohki/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-vault-core-and-rendering/02-RESEARCH.md @.planning/phases/02-vault-core-and-rendering/02-CONTEXT.md @.planning/phases/02-vault-core-and-rendering/02-01-SUMMARY.md @src/highlighter.rs Task 1: Build core markdown renderer with inline and block elements src/renderer.rs Create `src/renderer.rs` with the public function `render_markdown(input: &str, width: u16) -> Vec>`.The width parameter is needed for horizontal rules (full-width ─ repeats) and could be used for code block border sizing.
Internal state struct (private):
struct RenderState {
lines: Vec<Line<'static>>,
current_spans: Vec<Span<'static>>,
style_stack: Vec<Style>, // push on Start, pop on End
in_code_block: bool,
code_lang: String,
code_buf: String,
list_counters: Vec<Option<u64>>, // None=unordered, Some(n)=ordered from n
in_blockquote: bool,
blockquote_depth: u32,
in_image: bool,
image_alt: String,
// Table state
in_table: bool,
table_alignments: Vec<pulldown_cmark::Alignment>,
table_rows: Vec<Vec<String>>,
current_cell: String,
in_table_head: bool,
// Width for rules
width: u16,
}
Parser setup:
- Use
Options::empty()then insertENABLE_TABLES,ENABLE_STRIKETHROUGH,ENABLE_TASKLISTS - Wrap parser in
TextMergeStream::new(Parser::new_ext(input, opts)) - Iterate events, dispatch to handler methods on RenderState
Element handling (implement ALL of these):
Headings (REND-01, REND-10):
- On
Start(Tag::Heading { level, .. }): push heading style onto style_stack based on level:- H1:
Color::LightCyan + Modifier::BOLD - H2:
Color::LightYellow + Modifier::BOLD - H3:
Color::LightGreen + Modifier::BOLD - H4:
Color::Green - H5:
Color::Cyan - H6:
Color::DarkGray
- H1:
- On
End(TagEnd::Heading(level)): flush current_spans as a Line, then emit decorator:- H1: Line of
═repeated to width, inColor::LightCyan - H2: Line of
─repeated to width, inColor::LightYellow - H3-H6: no decorator line
- Always add an empty Line after for spacing
- H1: Line of
Paragraphs:
- On
Start(Tag::Paragraph): nothing (spans accumulate) - On
End(TagEnd::Paragraph): flush current_spans to a Line, push empty Line for spacing - Special handling: if inside blockquote, prepend blockquote decorators to each flushed line
Inline formatting (REND-02):
Start(Tag::Strong): push current style withModifier::BOLDadded onto style_stackEnd(TagEnd::Strong): pop style_stackStart(Tag::Emphasis): push current style withModifier::ITALICaddedEnd(TagEnd::Emphasis): pop style_stackEvent::Code(text): push Span withStyle::default().fg(Color::LightCyan)— inline code
Text:
Event::Text(text): ifin_code_block, append tocode_buf; ifin_image, append toimage_alt; ifin_tableand inside a cell, append tocurrent_cell; otherwise pushSpan::styled(text.to_string(), current_style())to current_spanscurrent_style()returns the top ofstyle_stack, orStyle::default()if empty
Soft/Hard breaks:
Event::SoftBreak: pushSpan::raw(" ")Event::HardBreak: flush current_spans as a Line
Lists (REND-04):
Start(Tag::List(start)): pushstartontolist_countersEnd(TagEnd::List(_)): poplist_counters, push empty Line for spacing after top-level listStart(Tag::Item): compute indent =" ".repeat(list_counters.len()). Compute bullet:- If
list_counters.last() == Some(None): use"• "(depth 1),"◦ "(depth 2),"▪ "(depth 3+) - If
list_counters.last() == Some(Some(n)): useformat!("{}. ", n), then increment n in the counter - Push indent + bullet as a Span in
Color::Cyanto current_spans
- If
End(TagEnd::Item): flush current_spans as a Line
Blockquotes (REND-05):
Start(Tag::BlockQuote(_)): increment blockquote_depth, set in_blockquote = true, push empty line beforeEnd(TagEnd::BlockQuote(_)): decrement blockquote_depth, set in_blockquote = false if depth == 0, push empty line after- When flushing lines inside blockquote: prepend
Span::styled("│ ".repeat(depth), Style::default().fg(Color::Yellow))to each Line, and applyColor::Grayto the content spans
Horizontal rules (REND-06):
Event::Rule: pushLine::from(Span::styled("─".repeat(width as usize), Style::default().fg(Color::DarkGray))), then push empty Line
Images (REND-07):
Start(Tag::Image { .. }): setin_image = true, clearimage_alt- Collect text inside image into
image_alt End(TagEnd::Image): pushSpan::styled(format!("[IMAGE: {}]", image_alt), Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM))to current_spans, setin_image = false
Links:
Start(Tag::Link { .. }): for Phase 2, just pass through — link text renders as normal styled text. Links become interactive in Phase 3.End(TagEnd::Link): nothing special
All other events: ignore (_ => {})
Do NOT add table or code block rendering yet — Task 2 handles those. For now, stub them:
Start(Tag::CodeBlock(_)): setin_code_block = true, clearcode_buf, capture langEnd(TagEnd::CodeBlock): call a placeholderemit_code_block()that pushescode_buflines as plain styled text- Table events: set
in_table = true, accumulate cells, call placeholderemit_table()on End
Helper methods on RenderState:
flush_line(): take current_spans, make a Line, apply blockquote prefix if needed, push to lines, clear current_spanscurrent_style(): return top of style_stack or Style::default()finish() -> Vec<Line<'static>>: return the accumulated lines
Ensure ALL Span content uses .to_string() for owned strings — every output must be Line<'static>.
cargo check passes after temporarily adding mod renderer; to main.rs. Test with a simple markdown string mentally: # Hello\n\nSome **bold** text. should produce heading line + decorator + empty + paragraph line + empty.
renderer.rs exists with render_markdown() function that handles headings (H1-H6 with colors + decorators), paragraphs, bold/italic/inline code, lists (ordered/unordered/nested), blockquotes with │ prefix, horizontal rules, images as placeholders, soft/hard breaks. Code blocks and tables have working stubs that render content without fancy borders.
Code blocks (REND-03, REND-09):
Replace the code block stub with emit_code_block(code_buf: &str, lang: &str, width: u16, lines: &mut Vec<Line<'static>>):
- Get syntax-highlighted lines by calling
crate::highlighter::highlight_code(code_buf, lang) - Determine the box width:
max(highlighted lines' max display width + 4, lang.len() + 6), capped atwidth - Build the top border line:
╭─ {lang}+─repeated to fill +╮- Style:
Color::DarkGrayfor border chars,Color::Yellowfor lang label - If lang is empty, just
╭+─fill +╮
- For each highlighted line:
│+ highlighted spans + padding to box width +│- Border chars (
│) inColor::DarkGray - Content spans keep their syntax highlighting colors from highlight_code()
- Border chars (
- Build the bottom border:
╰+─fill +╯inColor::DarkGray - Push empty Line before and after the code block for spacing
If highlight_code() returns empty (no syntax found), fall back to plain Color::Gray text for the code content.
Tables (REND-08, REND-09):
Replace the table stub with emit_table(alignments: &[Alignment], rows: &[Vec<String>], lines: &mut Vec<Line<'static>>):
- Compute column widths: for each column index, find
max(cell.len() + 2)across all rows (header + data), minimum 5 chars - Build helper
build_border_line(left: char, mid: char, right: char, fill: char, widths: &[usize]) -> String:- e.g.,
build_border_line('┌', '┬', '┐', '─', &widths)→┌──────┬──────┐
- e.g.,
- Emit top border:
build_border_line('┌', '┬', '┐', '─', ...)styledColor::Cyan - Emit header row (rows[0]):
- For each cell:
│+ padded cell text (aligned per alignments[i]) + space - Cell text styled
Color::LightCyan + Modifier::BOLD │border chars styledColor::Cyan- Trailing
│
- For each cell:
- Emit header separator:
build_border_line('├', '┼', '┤', '─', ...)styledColor::Cyan - Emit data rows (rows[1..]):
- Same format as header but with
Style::default()for cell text
- Same format as header but with
- Emit bottom border:
build_border_line('└', '┴', '┘', '─', ...)styledColor::Cyan - Push empty Line after table for spacing
Alignment handling in table cells:
Alignment::LeftorAlignment::None: left-pad with 1 space, right-pad to widthAlignment::Right: left-pad to width, right-pad with 1 spaceAlignment::Center: center the text within width
Ensure the table state machine in the event handler correctly:
- Captures
alignmentsfromStart(Tag::Table(alignments)) - Pushes new
Vec<String>onStart(Tag::TableRow)ANDStart(Tag::TableHead) - Accumulates cell text on
Event::Textwhenin_tableis true - Pushes cell to current row on
End(TagEnd::TableCell) - Calls
emit_table()onEnd(TagEnd::Table)cargo checkpasses. Mentally verify: a markdown code blockrust\nlet x = 1;\nshould produce 4 lines (top border, content, bottom border) plus spacing. A 2-column 3-row table should produce 6 lines (top border, header, separator, 2 data rows, bottom border) plus spacing. Code blocks render with ╭─╮│╰─╯ rounded borders, language label in yellow, syntax-highlighted content from highlighter.rs. GFM tables render with ┌┬┐├┼┤└┴┘ full box-drawing grid, bold LightCyan header row, aligned cell content. Both emit proper spacing.
<success_criteria>
- renderer.rs contains a single public function
render_markdown()that processes all markdown constructs - Headings H1-H6 have distinct CGA colors and H1/H2 have decorators
- Bold, italic, inline code have correct terminal modifiers/colors
- Lists support ordered/unordered with nesting
- Code blocks have syntax highlighting with CGA colors and rounded box-drawing borders
- Tables have full box-drawing grid with aligned columns
- All output is Vec<Line<'static>> with no lifetime dependencies </success_criteria>