Writer-based `Renderer` trait per SPEC §6.8: object-safe
`fn render(&self, doc, w: &mut dyn Write) -> io::Result<()>` plus
a `render_to_string` convenience. Object safety preserves the option
of `Box<dyn Renderer>` in the LSP layer.
`HtmlRenderer` builder accepts a link-resolution callback (via
`with_link_resolver`) and an optional enclosing template (via
`with_template`). The default resolver mirrors §9: Wiki paths become
`path.html`, interwiki links land in sibling directories
(`../wiki<N>/` / `../wn-<Name>/`), diary entries under `diary/`,
file/local schemes use literal paths, anchor-only collapses to `#`.
Renderer covers every AST node:
- block: heading (centered class), paragraph, hr, blockquote,
preformatted (`language-<lang>` class), math block (with
`\begin{env}` when an environment is set), lists (ordered/unordered
+ every checkbox state, plus recursive sublists), definition list
(dt/dd), table (thead/tbody driven by `is_header`, col/row span as
CSS class hooks), comment (as HTML comment with `--` neutralised),
error node (as `.parse-error` span)
- inline: strong/em + bold-italic, `<del>`, code (escaped),
`<sup>`/`<sub>`, math-inline wrapped in `\(...\)`, keyword spans
with per-keyword class, color spans, wikilink/external/raw URL
anchors, transclusion as `<img>` with sorted attrs
HTML escaping is byte-level (`<>&"'`). Template substitution replaces
`{{content}}` first so a body containing the literal `{{title}}`
isn't double-substituted.
Tests (31 new) cover every block + inline construct, every LinkKind
through the default resolver, HTML escaping, custom resolver
override, template with/without title metadata, and an end-to-end
smoke test on a multi-construct document.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the Lexer/Parser/SyntaxPlugin traits and the SyntaxRegistry per
SPEC.md §6.3.
- `Lexer<Token>` and `Parser<Token>` are typed building blocks: each
syntax owns its own token alphabet (vimwiki today, markdown later).
- `TokenStream<T>` is a `Vec<T>` newtype so the eager-vs-lazy choice
(§6.6) can flip later without breaking callers.
- `SyntaxPlugin` is type-erased: it exposes id / display_name /
file_extensions / parse(text) so the registry can hold heterogeneous
plugins behind a single trait object. Implementations chain their
Lexer + Parser inside parse().
- `SyntaxPlugin: Send + Sync` so the LSP server can stash plugins in
an Arc and share them across handler tasks.
- `SyntaxRegistry` stores `Arc<dyn SyntaxPlugin>`, supports lookup by
id and by file extension, iteration in registration order.
Tests cover TokenStream round-trip, full lex→parse chain through a
mock plugin, registry lookup hits and misses, and a compile-time
assert_send_sync::<SyntaxRegistry>().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Define every node type in SPEC.md §6.4 — Document, 11 block variants,
15 inline variants (including the 4 link-related ones), plus
ListItem / DefinitionItem / TableRow / TableCell, ListSymbol,
CheckboxState, Keyword, LinkTarget, LinkKind. Every node carries a
`Span` (line + column + byte offset, 0-indexed) per §6.5.
Visitor: open-recursion trait with default `visit_*` bodies that
delegate to `walk_*` free functions. Override at any granularity;
call `walk_*` from your override to keep descending. P4 resolved by
defining the trait now so Phase 5 (renderer) and Phase 7 (semantic
tokens) can plug in without refactoring traversal.
P3 resolved: AST string fields are owned `String`. Cow / Arc<str> can
be revisited if the parser grows a zero-copy mode.
Tests: visitor smoke test builds a heading + paragraph + list + table
by hand and asserts exact visit counts (4 blocks, 12 inlines, 9 text
leaves, 1 bold, 1 external link, 1 transclusion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>