docs(03-01): complete link record extraction and wiki-link resolution plan
- 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
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
---
|
||||
phase: 03-navigation-and-links
|
||||
plan: "01"
|
||||
subsystem: renderer-vault
|
||||
tags:
|
||||
- links
|
||||
- wiki-links
|
||||
- pulldown-cmark
|
||||
- path-traversal
|
||||
- navigation
|
||||
dependency_graph:
|
||||
requires:
|
||||
- 02-vault-core-and-rendering (renderer.rs RenderState, vault.rs load_document)
|
||||
provides:
|
||||
- LinkRecord struct with line_index/col_offset/span_len/dest/is_wiki
|
||||
- render_markdown returns (Vec<Line>, Vec<LinkRecord>)
|
||||
- resolve_wiki_link with case-insensitive multi-strategy matching
|
||||
- resolve_standard_link for inline link resolution
|
||||
- is_within_vault path traversal guard
|
||||
affects:
|
||||
- 03-02 (consumes LinkRecord and resolution functions for Tab-cycling navigation)
|
||||
tech_stack:
|
||||
added:
|
||||
- "Options::ENABLE_WIKILINKS (pulldown-cmark 0.13.1, already locked)"
|
||||
patterns:
|
||||
- "PendingLink/PendingLinkRecord state machine for deferred line_index resolution"
|
||||
- "flush_line finalizes link records (Pitfall 1: line_index only known at flush time)"
|
||||
- "canonicalize + starts_with path traversal guard"
|
||||
- "chars().count() for Unicode-correct column offsets (Pitfall 4)"
|
||||
key_files:
|
||||
modified:
|
||||
- path: src/renderer.rs
|
||||
role: "Added LinkRecord, PendingLink, PendingLinkRecord; updated render_markdown to return (Vec<Line>, Vec<LinkRecord>); ENABLE_WIKILINKS; link event handling with broken wiki-link detection"
|
||||
- path: src/vault.rs
|
||||
role: "Added is_within_vault, resolve_wiki_link, resolve_standard_link"
|
||||
- path: src/app.rs
|
||||
role: "Updated handle_resize to destructure (lines, _link_records) from render_markdown"
|
||||
- path: src/main.rs
|
||||
role: "Updated initial render call to destructure (lines, _link_records) from render_markdown"
|
||||
decisions:
|
||||
- "vault_path: Option<&Path> added to render_markdown for render-time broken wiki-link detection"
|
||||
- "Broken wiki-links checked at render time (filesystem stat); standard links not checked at render time"
|
||||
- "link_span_start_count field tracks span index at Tag::Link Start to compute span_len at TagEnd::Link"
|
||||
- "app.rs and main.rs updated with TODO(03-02) comments at _link_records destructuring sites"
|
||||
- "ENABLE_WIKILINKS + TextMergeStream: no interaction issues observed (Pitfall 2 non-issue)"
|
||||
metrics:
|
||||
duration_seconds: 209
|
||||
completed_date: "2026-02-28"
|
||||
tasks_completed: 2
|
||||
tasks_total: 2
|
||||
files_modified: 4
|
||||
---
|
||||
|
||||
# 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`):
|
||||
```rust
|
||||
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:**
|
||||
|
||||
- `PendingLink` captures dest, is_wiki, link_style, and col_offset at `Tag::Link` Start
|
||||
- `PendingLinkRecord` is enqueued at `TagEnd::Link` with span_len computed
|
||||
- `flush_line()` finalizes pending records with the correct `line_index` (Pitfall 1 avoided — line_index is only known at flush time, not at TagEnd::Link)
|
||||
- `link_span_start_count` field records span array position at Start so span_len can be summed at End
|
||||
- `chars().count()` used for col_offset (Pitfall 4: multi-byte Unicode correctness)
|
||||
|
||||
**Link rendering:**
|
||||
- Wiki-links: `[[Target]]` → `[Target]` in `LightCyan` if resolved, `Red+CROSSED_OUT` if broken
|
||||
- Standard links: `[text](path.md)` → `[text]` in `LightCyan`
|
||||
- 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)` with `TODO(03-02)` comment
|
||||
- `main.rs`: `let (lines, _link_records) = render_markdown(&content, initial_width, None)` with `TODO(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 (`.md` stripped, `.to_lowercase()`)
|
||||
- Guards matched path with `is_within_vault()` + canonicalize before returning
|
||||
- Returns vault-relative path on success, `None` on no match or traversal detected
|
||||
|
||||
**`resolve_standard_link(vault_path, current_doc, dest) -> Option<PathBuf>`:**
|
||||
- Resolves `dest` relative to `current_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
|
||||
|
||||
1. `cargo build` succeeds — zero errors, 4 expected "unused" warnings (Link fields and vault functions will be consumed by Plan 02)
|
||||
2. `render_markdown` signature change handled: all call sites updated with `(lines, _link_records)` destructuring
|
||||
3. `ENABLE_WIKILINKS` enabled; `LinkType::WikiLink` matched in event handler
|
||||
4. `TextMergeStream` + `ENABLE_WIKILINKS` interaction: no issues (Pitfall 2 did not manifest)
|
||||
5. Path traversal guard: `is_within_vault` uses `canonicalize + starts_with` per 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`: contains `pub struct LinkRecord`, `ENABLE_WIKILINKS`, `pending_link_records`
|
||||
- `src/vault.rs`: contains `pub fn resolve_wiki_link`, `pub fn is_within_vault`, `pub fn resolve_standard_link`
|
||||
- `src/app.rs`: updated with `_link_records` destructuring and `TODO(03-02)`
|
||||
- `src/main.rs`: updated with `_link_records` destructuring and `TODO(03-02)`
|
||||
|
||||
Commits verified:
|
||||
- `a63f411`: feat(03-01): extend renderer with LinkRecord extraction and link styling
|
||||
- `f2604d6`: feat(03-01): add wiki-link resolution and path traversal guard to vault.rs
|
||||
Reference in New Issue
Block a user