279 lines
14 KiB
Markdown
279 lines
14 KiB
Markdown
---
|
|
phase: 02-vault-core-and-rendering
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on:
|
|
- "02-01"
|
|
files_modified:
|
|
- src/renderer.rs
|
|
autonomous: true
|
|
requirements:
|
|
- REND-01
|
|
- REND-02
|
|
- REND-03
|
|
- REND-04
|
|
- REND-05
|
|
- REND-06
|
|
- REND-07
|
|
- REND-08
|
|
- REND-09
|
|
- REND-10
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Headings H1-H6 render with distinct CGA colors and decorators (H1=LightCyan+══, H2=LightYellow+──, H3-H6 color-only)"
|
|
- "Bold text uses Modifier::BOLD, italic uses Modifier::ITALIC, inline code uses LightCyan styling"
|
|
- "Fenced code blocks render with rounded box-drawing borders (╭─╮│╰─╯) and syntax-highlighted content via highlighter.rs"
|
|
- "Ordered and unordered lists render with bullets/numbers and proper indentation per nesting depth"
|
|
- "Blockquotes render with a yellow │ left border and gray content"
|
|
- "Horizontal rules render as full-width ─── lines in DarkGray"
|
|
- "Images render as [IMAGE: alt text] placeholders in DarkGray+DIM"
|
|
- "GFM tables render with full box-drawing grid (┌┬┐├┼┤└┴┘) and bold LightCyan header row"
|
|
artifacts:
|
|
- path: "src/renderer.rs"
|
|
provides: "Complete markdown-to-styled-lines conversion pipeline"
|
|
exports: ["render_markdown"]
|
|
min_lines: 200
|
|
key_links:
|
|
- from: "src/renderer.rs"
|
|
to: "pulldown-cmark"
|
|
via: "Parser::new_ext with TextMergeStream consuming all Event variants"
|
|
pattern: "TextMergeStream"
|
|
- from: "src/renderer.rs"
|
|
to: "src/highlighter.rs"
|
|
via: "highlight_code() called for fenced code blocks"
|
|
pattern: "highlight_code"
|
|
- from: "src/renderer.rs"
|
|
to: "ratatui::text"
|
|
via: "Produces Vec<Line<'static>> with Span styling"
|
|
pattern: "Vec<Line<'static>>"
|
|
---
|
|
|
|
<objective>
|
|
Build the complete markdown-to-styled-lines renderer that converts a markdown string into `Vec<Line<'static>>` using pulldown-cmark event processing, CGA-themed styling, syntax-highlighted code blocks, and box-drawing table grids.
|
|
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/Users/ruohki/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Build core markdown renderer with inline and block elements</name>
|
|
<files>src/renderer.rs</files>
|
|
<action>
|
|
Create `src/renderer.rs` with the public function `render_markdown(input: &str, width: u16) -> Vec<Line<'static>>`.
|
|
|
|
The `width` parameter is needed for horizontal rules (full-width `─` repeats) and could be used for code block border sizing.
|
|
|
|
**Internal state struct (private):**
|
|
|
|
```rust
|
|
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 insert `ENABLE_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`
|
|
- On `End(TagEnd::Heading(level))`: flush current_spans as a Line, then emit decorator:
|
|
- H1: Line of `═` repeated to width, in `Color::LightCyan`
|
|
- H2: Line of `─` repeated to width, in `Color::LightYellow`
|
|
- H3-H6: no decorator line
|
|
- Always add an empty Line after for spacing
|
|
|
|
**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 with `Modifier::BOLD` added onto style_stack
|
|
- `End(TagEnd::Strong)`: pop style_stack
|
|
- `Start(Tag::Emphasis)`: push current style with `Modifier::ITALIC` added
|
|
- `End(TagEnd::Emphasis)`: pop style_stack
|
|
- `Event::Code(text)`: push Span with `Style::default().fg(Color::LightCyan)` — inline code
|
|
|
|
**Text:**
|
|
- `Event::Text(text)`: if `in_code_block`, append to `code_buf`; if `in_image`, append to `image_alt`; if `in_table` and inside a cell, append to `current_cell`; otherwise push `Span::styled(text.to_string(), current_style())` to current_spans
|
|
- `current_style()` returns the top of `style_stack`, or `Style::default()` if empty
|
|
|
|
**Soft/Hard breaks:**
|
|
- `Event::SoftBreak`: push `Span::raw(" ")`
|
|
- `Event::HardBreak`: flush current_spans as a Line
|
|
|
|
**Lists (REND-04):**
|
|
- `Start(Tag::List(start))`: push `start` onto `list_counters`
|
|
- `End(TagEnd::List(_))`: pop `list_counters`, push empty Line for spacing after top-level list
|
|
- `Start(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))`: use `format!("{}. ", n)`, then increment n in the counter
|
|
- Push indent + bullet as a Span in `Color::Cyan` to current_spans
|
|
- `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 before
|
|
- `End(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 apply `Color::Gray` to the content spans
|
|
|
|
**Horizontal rules (REND-06):**
|
|
- `Event::Rule`: push `Line::from(Span::styled("─".repeat(width as usize), Style::default().fg(Color::DarkGray)))`, then push empty Line
|
|
|
|
**Images (REND-07):**
|
|
- `Start(Tag::Image { .. })`: set `in_image = true`, clear `image_alt`
|
|
- Collect text inside image into `image_alt`
|
|
- `End(TagEnd::Image)`: push `Span::styled(format!("[IMAGE: {}]", image_alt), Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM))` to current_spans, set `in_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(_))`: set `in_code_block = true`, clear `code_buf`, capture lang
|
|
- `End(TagEnd::CodeBlock)`: call a placeholder `emit_code_block()` that pushes `code_buf` lines as plain styled text
|
|
- Table events: set `in_table = true`, accumulate cells, call placeholder `emit_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_spans
|
|
- `current_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>`.
|
|
</action>
|
|
<verify>`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.</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add code block borders with syntax highlighting and GFM table grid rendering</name>
|
|
<files>src/renderer.rs</files>
|
|
<action>
|
|
Replace the placeholder `emit_code_block()` and `emit_table()` with full implementations.
|
|
|
|
**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>>)`:
|
|
|
|
1. Get syntax-highlighted lines by calling `crate::highlighter::highlight_code(code_buf, lang)`
|
|
2. Determine the box width: `max(highlighted lines' max display width + 4, lang.len() + 6)`, capped at `width`
|
|
3. Build the top border line:
|
|
- `╭─ {lang} ` + `─` repeated to fill + `╮`
|
|
- Style: `Color::DarkGray` for border chars, `Color::Yellow` for lang label
|
|
- If lang is empty, just `╭` + `─` fill + `╮`
|
|
4. For each highlighted line: `│ ` + highlighted spans + padding to box width + ` │`
|
|
- Border chars (`│`) in `Color::DarkGray`
|
|
- Content spans keep their syntax highlighting colors from highlight_code()
|
|
5. Build the bottom border: `╰` + `─` fill + `╯` in `Color::DarkGray`
|
|
6. 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>>)`:
|
|
|
|
1. Compute column widths: for each column index, find `max(cell.len() + 2)` across all rows (header + data), minimum 5 chars
|
|
2. Build helper `build_border_line(left: char, mid: char, right: char, fill: char, widths: &[usize]) -> String`:
|
|
- e.g., `build_border_line('┌', '┬', '┐', '─', &widths)` → `┌──────┬──────┐`
|
|
3. Emit top border: `build_border_line('┌', '┬', '┐', '─', ...)` styled `Color::Cyan`
|
|
4. 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 styled `Color::Cyan`
|
|
- Trailing `│`
|
|
5. Emit header separator: `build_border_line('├', '┼', '┤', '─', ...)` styled `Color::Cyan`
|
|
6. Emit data rows (rows[1..]):
|
|
- Same format as header but with `Style::default()` for cell text
|
|
7. Emit bottom border: `build_border_line('└', '┴', '┘', '─', ...)` styled `Color::Cyan`
|
|
8. Push empty Line after table for spacing
|
|
|
|
**Alignment handling in table cells:**
|
|
- `Alignment::Left` or `Alignment::None`: left-pad with 1 space, right-pad to width
|
|
- `Alignment::Right`: left-pad to width, right-pad with 1 space
|
|
- `Alignment::Center`: center the text within width
|
|
|
|
Ensure the table state machine in the event handler correctly:
|
|
- Captures `alignments` from `Start(Tag::Table(alignments))`
|
|
- Pushes new `Vec<String>` on `Start(Tag::TableRow)` AND `Start(Tag::TableHead)`
|
|
- Accumulates cell text on `Event::Text` when `in_table` is true
|
|
- Pushes cell to current row on `End(TagEnd::TableCell)`
|
|
- Calls `emit_table()` on `End(TagEnd::Table)`
|
|
</action>
|
|
<verify>`cargo check` passes. Mentally verify: a markdown code block ````rust\nlet x = 1;\n```` should 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.</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. `cargo check` passes with renderer.rs compiling
|
|
2. `render_markdown("# Test\n\nHello **world**\n", 80)` would produce: heading line (LightCyan bold), ══ decorator, empty, paragraph with bold span, empty
|
|
3. All pulldown-cmark Event variants for Phase 2 requirements are handled (Heading, Paragraph, Strong, Emphasis, Code, CodeBlock, List, Item, BlockQuote, Rule, Image, Table, TableHead, TableRow, TableCell, Text, SoftBreak, HardBreak)
|
|
4. Code block output includes ╭╮│╰╯ border characters with syntax-highlighted inner content
|
|
5. Table output includes ┌┬┐├┼┤└┴┘ border characters with bold header row
|
|
6. Every Span uses owned String content (.to_string()) — no borrowed lifetimes
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-vault-core-and-rendering/02-02-SUMMARY.md`
|
|
</output>
|