- 03-01-SUMMARY.md: documents LinkRecord struct, render_markdown signature change, resolve_wiki_link algorithm, is_within_vault guard, and all decisions - STATE.md: advance to Phase 3 Plan 2, update progress to 75%, add 3 decisions, mark NAV path traversal blocker resolved - ROADMAP.md: update phase 3 plan progress (1 of 2 summaries) - REQUIREMENTS.md: mark NAV-01, NAV-02, NAV-10 complete
7.3 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 03-navigation-and-links | 01 | renderer-vault |
|
|
|
|
|
|
Phase 03 Plan 01: Link Record Extraction and Wiki-Link Resolution Summary
One-liner: LinkRecord parallel structure added to render_markdown output with ENABLE_WIKILINKS, case-insensitive wiki-link resolver, and canonicalize-guarded path traversal protection in vault.rs.
What Was Built
Task 1: Renderer link extraction and styling (commit a63f411)
Extended render_markdown() to return (Vec<Line<'static>>, Vec<LinkRecord>) instead of the plain Vec<Line<'static>> from Phase 2.
LinkRecord struct (new, in src/renderer.rs):
pub struct LinkRecord {
pub line_index: usize, // index into Vec<Line>
pub col_offset: usize, // chars().count() offset within line
pub span_len: usize, // display length including brackets
pub dest: String, // raw destination (path or wiki target)
pub is_wiki: bool, // true = needs resolve_wiki_link at nav time
}
Key design decisions in renderer:
PendingLinkcaptures dest, is_wiki, link_style, and col_offset atTag::LinkStartPendingLinkRecordis enqueued atTagEnd::Linkwith span_len computedflush_line()finalizes pending records with the correctline_index(Pitfall 1 avoided — line_index is only known at flush time, not at TagEnd::Link)link_span_start_countfield records span array position at Start so span_len can be summed at Endchars().count()used for col_offset (Pitfall 4: multi-byte Unicode correctness)
Link rendering:
- Wiki-links:
[[Target]]→[Target]inLightCyanif resolved,Red+CROSSED_OUTif broken - Standard links:
[text](path.md)→[text]inLightCyan - Text inside links uses the same style (link style overrides style_stack during Text events)
Broken wiki-link detection: render_markdown gains a vault_path: Option<&Path> parameter. When is_wiki and vault_path is Some, calls crate::vault::resolve_wiki_link at render time. Standard links are never checked at render time (per research recommendation — stat on every link on every render avoided).
Caller updates:
app.rs handle_resize:let (lines, _link_records) = render_markdown(content, new_width, None)withTODO(03-02)commentmain.rs:let (lines, _link_records) = render_markdown(&content, initial_width, None)withTODO(03-02)comment
Task 2: Vault wiki-link resolution and path traversal guard (commit f2604d6)
Three new public functions added to src/vault.rs:
is_within_vault(vault_path, candidate) -> bool:
Canonicalizes both paths and checks starts_with. Returns false on any IO error, making it safe to call on non-existent paths.
resolve_wiki_link(vault_path, raw_target) -> Option<PathBuf>:
- Splits on last
/to extract optional subdir prefix - Generates 3 candidate stems (spaces→hyphens, spaces→underscores, literal spaces) — hyphens first per locked decision
- Scans
read_dir(search_dir)with case-insensitive stem comparison (.mdstripped,.to_lowercase()) - Guards matched path with
is_within_vault()+ canonicalize before returning - Returns vault-relative path on success,
Noneon no match or traversal detected
resolve_standard_link(vault_path, current_doc, dest) -> Option<PathBuf>:
- Resolves
destrelative tocurrent_doc's parent directory within the vault - Applies
is_within_vault()guard via canonicalize - Returns vault-relative path if file exists and is within vault
Verification Results
cargo buildsucceeds — zero errors, 4 expected "unused" warnings (Link fields and vault functions will be consumed by Plan 02)render_markdownsignature change handled: all call sites updated with(lines, _link_records)destructuringENABLE_WIKILINKSenabled;LinkType::WikiLinkmatched in event handlerTextMergeStream+ENABLE_WIKILINKSinteraction: no issues (Pitfall 2 did not manifest)- Path traversal guard:
is_within_vaultusescanonicalize + starts_withper security pattern
Deviations from Plan
Auto-fixed Issues
None — plan executed exactly as written, with one minor implementation detail:
link_span_start_count field: The plan described computing span_len from "spans pushed between Start and End". The implementation uses a dedicated link_span_start_count: usize field on RenderState to snapshot the span array length at Tag::Link Start, then computes span_len as the sum of chars().count() for current_spans[link_span_start_count..] at TagEnd::Link. This is a clean implementation of the plan's intent, not a deviation.
Self-Check: PASSED
Files verified:
src/renderer.rs: containspub struct LinkRecord,ENABLE_WIKILINKS,pending_link_recordssrc/vault.rs: containspub fn resolve_wiki_link,pub fn is_within_vault,pub fn resolve_standard_linksrc/app.rs: updated with_link_recordsdestructuring andTODO(03-02)src/main.rs: updated with_link_recordsdestructuring andTODO(03-02)
Commits verified:
a63f411: feat(03-01): extend renderer with LinkRecord extraction and link stylingf2604d6: feat(03-01): add wiki-link resolution and path traversal guard to vault.rs