11 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-navigation-and-links | 01 | execute | 1 |
|
true |
|
|
Purpose: The renderer currently ignores links (Phase 2 pass-through). This plan makes links visible and extractable so Plan 02 can wire navigation. Wiki-link resolution is the vault-side counterpart that maps raw targets to actual files.
Output:
render_markdown()returns(Vec<Line<'static>>, Vec<LinkRecord>)instead ofVec<Line<'static>>LinkRecordstruct with line_index, col_offset, span_len, dest, is_wiki- All links rendered as
[Link Text]in LightCyan; broken wiki-links in Red+CROSSED_OUT resolve_wiki_link()in vault.rs with path traversal guard
<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/03-navigation-and-links/03-RESEARCH.md @src/renderer.rs @src/vault.rs Task 1: Extend renderer with LinkRecord extraction and link styling src/renderer.rs Modify `src/renderer.rs` to produce link metadata alongside styled lines:- Add LinkRecord struct (public, at top of file):
pub struct LinkRecord {
pub line_index: usize, // Index into Vec<Line> where this link appears
pub col_offset: usize, // Character column offset within that line
pub span_len: usize, // Display length of the link text (including brackets)
pub dest: String, // Raw destination (path for inline, wiki target for wiki-links)
pub is_wiki: bool, // True if WikiLink type (needs resolution at nav time)
}
- Add PendingLink to RenderState:
- Add fields:
pending_link: Option<PendingLink>andpending_link_records: Vec<PendingLinkRecord>andlink_records: Vec<LinkRecord> PendingLinkholdsdest: String,is_wiki: bool,col_offset: usize(computed from sum of current_spans character lengths at Tag::Link Start)PendingLinkRecordholdsdest,is_wiki,col_offset(finalized at TagEnd::Link, awaiting line_index from flush)
- Enable ENABLE_WIKILINKS in
render_markdown():
- Add
opts.insert(Options::ENABLE_WIKILINKS);to the options block - IMPORTANT: Test TextMergeStream interaction — if wiki-link text is garbled, wrap only the parser without TextMergeStream for wiki-link detection and apply TextMergeStream separately for code blocks. Research indicates this is LOW risk but must be verified.
- Handle Tag::Link Start event (replace the current pass-through):
Event::Start(Tag::Link { link_type, dest_url, .. }) => {
let is_wiki = matches!(link_type, pulldown_cmark::LinkType::WikiLink { .. });
let col_offset: usize = state.current_spans.iter()
.map(|s| s.content.chars().count())
.sum();
state.pending_link = Some(PendingLink {
dest: dest_url.to_string(),
is_wiki,
col_offset,
});
// Determine link color: broken wiki-links get Red+CROSSED_OUT
// For wiki-links, check at render time whether the link resolves.
// Pass vault_path into render_markdown for this check.
// Push opening bracket "[" in appropriate color
}
For broken wiki-link detection at render time: render_markdown needs access to vault_path to call resolve_wiki_link for wiki-links. Add vault_path: Option<&Path> parameter. When is_wiki and vault_path is Some, call crate::vault::resolve_wiki_link(vault_path, &dest_url) — if None, use red/strikethrough style; if Some, use LightCyan style. For non-wiki links, always use LightCyan (broken standard links are revealed only at navigation time per research recommendation).
The link style determines the color of brackets and text:
- Normal link:
Style::default().fg(Color::LightCyan) - Broken wiki-link:
Style::default().fg(Color::Red).add_modifier(Modifier::CROSSED_OUT)
Push opening bracket span: state.current_spans.push(Span::styled("[", link_style))
Set a flag so Text events inside the link use the link style instead of current_style().
- Handle TagEnd::Link:
- Push closing bracket span
"]"with same link style - Compute
span_len= chars count of all spans pushed between Start and End (including brackets) - Create a
PendingLinkRecord { dest, is_wiki, col_offset, span_len }and push tostate.pending_link_records - Clear
state.pending_link
-
Update flush_line() to finalize link records: After pushing the line to
self.lines, iterateself.pending_link_records, drain them, and for each create aLinkRecordwithline_index = self.lines.len() - 1(the just-pushed line). Push toself.link_records. -
Update finish() to return the pair: Change return type to
(Vec<Line<'static>>, Vec<LinkRecord>). Return(self.lines, self.link_records). -
Update render_markdown() signature:
pub fn render_markdown(input: &str, width: u16, vault_path: Option<&Path>) -> (Vec<Line<'static>>, Vec<LinkRecord>)
Note: Using chars().count() for col_offset instead of len() — handles multi-byte Unicode correctly per research Pitfall 4.
cargo build compiles with zero new errors. The function signature change will cause errors in app.rs and main.rs which are expected — Plan 02 will fix those callers. To verify in isolation, temporarily adjust the call sites in app.rs handle_resize and main.rs to destructure the tuple (e.g., let (lines, _links) = renderer::render_markdown(...)) so cargo build passes. If this is done, add a // TODO(03-02): use link_records comment.
render_markdown returns (Vec<Line>, Vec<LinkRecord>). Wiki-links parsed via ENABLE_WIKILINKS. Links display as bracket-wrapped colored text. Broken wiki-links show red/strikethrough. LinkRecord contains line_index, col_offset, span_len, dest, is_wiki for every link in the document.
is_within_vault(vault_path: &Path, candidate: &Path) -> bool:
pub fn is_within_vault(vault_path: &Path, candidate: &Path) -> bool {
match (candidate.canonicalize(), vault_path.canonicalize()) {
(Ok(canon_candidate), Ok(canon_vault)) => canon_candidate.starts_with(&canon_vault),
_ => false,
}
}
resolve_wiki_link(vault_path: &Path, raw_target: &str) -> Option<PathBuf>:
Algorithm (per locked decisions — case-insensitive, hyphens first, then underscores, then literal):
- Split raw_target on last
/to get (subdir, name). If no/, subdir is empty. - Generate candidates:
name.replace(' ', "-"),name.replace(' ', "_"),name.to_string()(literal spaces) - Compute search_dir:
vault_path.join(subdir)if subdir is non-empty, otherwise vault_path - Call
std::fs::read_dir(&search_dir)— return None on error - For each entry, get filename, strip
.mdsuffix, lowercase compare against each candidate (lowercased) - On first match: construct full path, call
is_within_vault()to guard against path traversal - If within vault: return
Some(canonical.strip_prefix(vault_canonical).to_path_buf())— vault-relative path - If no match found: return None
Per locked decision: first match wins (filesystem order). This is documented as intentional in the research.
resolve_standard_link(vault_path: &Path, current_doc: &str, dest: &str) -> Option<PathBuf>:
For standard markdown links [text](path.md):
- Compute base_dir from current_doc:
vault_path.join(current_doc).parent()(directory of current document) - Join:
base_dir.join(dest) - Apply is_within_vault() guard
- If file exists and within vault: return vault-relative path
- Otherwise: return None
This function is needed for Plan 02 when following standard links. Adding it here keeps all vault path resolution in one place.
cargo build compiles. Add a small test at the bottom of vault.rs (or verify mentally): resolve_wiki_link with a known vault directory returns the expected path; is_within_vault rejects ../../etc/passwd.
resolve_wiki_link() resolves raw wiki targets to vault-relative paths with case-insensitive multi-strategy matching. resolve_standard_link() resolves relative markdown link paths. Both are guarded by is_within_vault() which uses canonicalize + starts_with to prevent path traversal.
<success_criteria> The renderer produces a parallel link metadata structure alongside styled lines. Wiki-links and standard links are visually distinct from plain text. The vault module can resolve wiki-link targets to file paths safely. Plan 02 can consume these outputs to wire navigation. </success_criteria>
After completion, create `.planning/phases/03-navigation-and-links/03-01-SUMMARY.md`