# nuwiki — Project Specification > Last updated: 2026-05-11 > Status: v1.0 (Phases 0–10) shipped; v1.1 (Phases 11–18) in design > ### Versioning convention > > - **v1.0** — the parsing + highlighting + navigation foundation. LSP server > speaks the spec's §6.9 method set; editor glue provides install + setup > + health check. > - **v1.1** — full vimwiki replacement. Adds tags, diary, the > text-manipulation command surface (toggle checkbox, change list symbol, > format table, rename page, …), multi-wiki, HTML export commands, > link-health diagnostics, and the keymap/text-object layer that matches > vimwiki's daily-authoring ergonomics. --- ## 1. Overview nuwiki is a Vim/Neovim plugin that provides full vimwiki syntax support, implemented as a Rust-based Language Server (LSP). It is a spiritual successor to vimwiki, architecturally distinct from it, designed for correctness, performance, and extensibility. **Goals:** - Full vimwiki syntax support (highlighting, navigation, link following, diagnostics) - First-class support for both Vim 9+ and Neovim 0.5+ - Installable via standard plugin managers (lazy.nvim, vim-plug, Dein) - Extensible to Markdown syntax without changes to core architecture - Independently testable core library with no editor dependencies **Non-goals (v1.0):** - MediaWiki syntax support - Multi-wiki configurations *(addressed in v1.1, see §12)* - Incremental / tree-sitter parsing - Browser-based or WASM build **v1.1 scope** *(see §12 for the full plan)*: - Vimwiki-parity command surface (`:Vimwiki*` compat aliases over LSP `executeCommand`) - Tags, diary, link health, HTML export commands, multi-wiki, folding --- ## 2. Repository | Property | Value | |---|---| | License | Dual MIT/Apache-2.0 | | MSRV | Rust 1.83 (stable-2) | |---|---| | URL | `https://code.gfran.co/gffranco/nuwiki` | | Version control | Gitea | | CI/CD | Gitea Actions | --- ## 3. Naming Conventions | Context | Name | |---|---| | GitHub/Gitea repo | `nuwiki` | | Cargo workspace | `nuwiki` | | Core library crate | `nuwiki-core` | | LSP bridge crate | `nuwiki-lsp` | | Binary crate | `nuwiki-ls` | | Vim plugin entry | `plugin/nuwiki.vim` | | Lua module | `require('nuwiki')` | | VimL autoload | `nuwiki#lsp#start()` | | Vim help file | `doc/nuwiki.txt` → `:h nuwiki` | | Release binary | `nuwiki-ls-{version}-{target}.tar.gz` | --- ## 4. Technology Stack | Concern | Choice | Rationale | |---|---|---| | Implementation language | Rust | Performance, type safety, single binary distribution, ideal for AST modelling | | Editor integration | LSP over stdio | Works in both Vim and Neovim without a shared scripting language | | LSP server library | `tower-lsp` | Async, tokio-based, higher-level than `lsp-server`; easier to start with | | Parser library | None — hand-rolled recursive descent | Tokens are span-bearing structs that fit awkwardly into `winnow`/`nom` combinator style; resilience and error recovery are explicit in the parser code. Originally specified as `winnow`; revisited in Phase 4. | | Cargo edition | 2021 | Current standard; required for resolver v2 | | Cargo resolver | v2 | Required for edition 2021 workspaces | | Vim glue layer | VimL (`plugin/nuwiki.vim`, `autoload/`) | Universal entry point for all plugin managers | | Neovim glue layer | Lua (`lua/nuwiki/`) | First-class Neovim API access via `vim.lsp.start()` | --- ## 5. Repository Layout ``` nuwiki/ │ ├── Cargo.toml # Rust workspace root ├── Cargo.lock │ ├── crates/ │ ├── nuwiki-ls/ # Binary crate — thin main.rs, starts stdio LSP server │ │ ├── Cargo.toml │ │ └── src/main.rs │ │ │ ├── nuwiki-core/ # Library crate — parser, AST, renderer (no editor deps) │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── lib.rs │ │ ├── syntax/ │ │ │ ├── mod.rs │ │ │ ├── registry.rs │ │ │ └── vimwiki/ │ │ │ ├── mod.rs │ │ │ ├── lexer.rs │ │ │ └── parser.rs │ │ ├── ast/ │ │ │ ├── mod.rs │ │ │ ├── block.rs │ │ │ ├── inline.rs │ │ │ └── link.rs │ │ └── render/ │ │ ├── mod.rs │ │ └── html.rs │ │ │ └── nuwiki-lsp/ # Library crate — LSP protocol bridge │ ├── Cargo.toml │ └── src/lib.rs │ ├── plugin/ # Universal Vim/Neovim entry point │ └── nuwiki.vim │ ├── lua/ # Neovim-specific Lua layer │ └── nuwiki/ │ ├── init.lua │ ├── config.lua │ ├── lsp.lua │ └── install.lua # Binary download / build-from-source logic │ ├── autoload/ # Lazy-loaded VimL (Vim compat layer) │ └── nuwiki/ │ └── lsp.vim │ ├── ftdetect/ # Filetype detection for .wiki files │ └── nuwiki.vim │ ├── ftplugin/ # Per-filetype buffer settings │ └── nuwiki.vim │ ├── syntax/ # Static fallback syntax highlighting (no LSP required) │ └── nuwiki.vim │ ├── doc/ # Vim help documentation │ └── nuwiki.txt │ ├── scripts/ │ └── download_bin.vim # VimL binary download (used by Dein/vim-plug build hooks) │ └── .gitea/ └── workflows/ ├── ci.yaml # Lint, test, fmt check on every push/PR └── release.yaml # Cross-compile + release on v* tag ``` --- ## 6. Architecture ### 6.1 Layer Model ``` ┌─────────────────────────────────────────────────────┐ │ Consumer Layer │ │ (editor highlight, HTML export, TOC, …) │ └────────────────────────┬────────────────────────────┘ │ operates on ▼ ┌─────────────────────────────────────────────────────┐ │ AST (nuwiki-core, shared) │ │ Document > Block nodes > Inline nodes │ └────────────────────────┬────────────────────────────┘ │ produced by ▼ ┌─────────────────────────────────────────────────────┐ │ Parser — syntax-specific (nuwiki-core) │ │ Registered per syntax; receives TokenStream │ └────────────────────────┬────────────────────────────┘ │ consumes ▼ ┌─────────────────────────────────────────────────────┐ │ Lexer — syntax-specific (nuwiki-core) │ │ Registered per syntax; receives raw text │ └────────────────────────┬────────────────────────────┘ │ reads ▼ Raw text input ``` ### 6.2 Crate Dependency Rules ``` nuwiki-ls → nuwiki-lsp → nuwiki-core ``` - `nuwiki-core` must never depend on `nuwiki-lsp` or `nuwiki-ls` - `nuwiki-lsp` must never depend on `nuwiki-ls` - The VimL and Lua editor layers must never contain logic — pure wiring only ### 6.3 Syntax Plugin Interface Every syntax (vimwiki, future markdown) implements: ``` SyntaxPlugin id: string -- "vimwiki" | "markdown" display_name: string file_extensions: string[] -- [".wiki"] | [".md"] lexer: fn(text) → TokenStream parser: fn(TokenStream) → DocumentNode ``` Registry: ``` SyntaxRegistry register(plugin: SyntaxPlugin) get(id: &str) → Option<&SyntaxPlugin> detect_from_extension(ext: &str) → Option<&SyntaxPlugin> ``` All plugins must be `Send + Sync` (held in the LSP server registry). ### 6.4 AST Node Types #### Document ``` DocumentNode children: Vec metadata: PageMetadata -- title, nohtml, template, date ``` #### Block Nodes ``` HeadingNode level: 1–6 | children: Vec | centered: bool ParagraphNode children: Vec HorizontalRuleNode BlockquoteNode children: Vec PreformattedNode content: String | language: Option MathBlockNode content: String | environment: Option ListNode ordered: bool | symbol: ListSymbol | items: Vec DefinitionListNode items: Vec TableNode rows: Vec | has_header: bool CommentNode content: String ErrorNode raw: String | message: String -- resilient parse failure ``` #### Inline Nodes ``` TextNode content: String BoldNode children: Vec ItalicNode children: Vec BoldItalicNode children: Vec StrikethroughNode children: Vec CodeNode content: String SuperscriptNode children: Vec SubscriptNode children: Vec MathInlineNode content: String KeywordNode keyword: Keyword -- TODO|DONE|STARTED|FIXME|FIXED|XXX ColorNode color: String | children: Vec WikiLinkNode target: LinkTarget | description: Option> ExternalLinkNode url: String | description: Option> TransclusionNode url: String | alt: Option | attrs: HashMap RawUrlNode url: String ``` #### Supporting Types ``` ListItemNode symbol: ListSymbol level: usize checkbox: Option children: Vec sublist: Option DefinitionItemNode term: Option> definitions: Vec> TableRowNode cells: Vec | is_header: bool TableCellNode children: Vec | col_span: bool | row_span: bool LinkTarget kind: LinkKind -- Wiki|Interwiki|Diary|File|Local|Raw|AnchorOnly path: Option wiki_index: Option wiki_name: Option anchor: Option is_absolute: bool is_directory: bool ListSymbol = Dash | Star | Hash | Numeric | NumericParen | AlphaParen | AlphaUpperParen | RomanParen | RomanUpperParen CheckboxState = Empty | Quarter | Half | ThreeQuarters | Done | Rejected Keyword = Todo | Done | Started | Fixme | Fixed | Xxx ``` ### 6.5 Span / Source Location Every AST node carries a `Span`: ```rust struct Span { start: Position, end: Position, } struct Position { line: u32, -- 0-indexed column: u32, -- 0-indexed, byte offset within line offset: usize, -- absolute byte offset from document start } ``` Spans are required for LSP diagnostics, semantic tokens, and go-to-definition. ### 6.6 Lexer Strategy - **Two-pass:** block-level pass first, then inline pass within each block - **Token types:** syntax-specific (`VimwikiToken`) — not shared across syntaxes - **TokenStream:** eager `Vec` wrapped in a `TokenStream` newtype (changeable to lazy later) - Operates on Rust `char`s for correctness; spans stored as byte offsets ### 6.7 Parser Strategy - Uses `winnow` parser combinator library - Resilient: on malformed input, emits `ErrorNode` and continues — never fails the whole document - Inline marker precedence rules documented explicitly in code and tests ### 6.8 Renderer - `Renderer` trait: writer-based (`fn render(&self, doc: &DocumentNode, w: &mut dyn Write)`) - `HtmlRenderer` is the first implementation - Link resolution injected as a callback at construction time - Template support: minimal `{{content}}` / `{{title}}` token substitution - CSS: linked external stylesheet (matches vimwiki convention) ### 6.9 LSP Features | Feature | LSP Method | |---|---| | Syntax highlighting | `textDocument/semanticTokens/full` + `/range` | | Diagnostics (broken links, parse errors) | `textDocument/publishDiagnostics` | | Follow link / go to definition | `textDocument/definition` | | Backlinks | `textDocument/references` | | TOC / outline | `textDocument/documentSymbol` | | Link preview | `textDocument/hover` | | Link autocomplete | `textDocument/completion` (trigger: `[[`) | | Page rename | `workspace/rename` | | Workspace search | `workspace/symbol` | ### 6.10 Document Store ``` DocumentStore: Arc> DocumentState text: String ast: DocumentNode version: i32 ``` Full re-parse on every `didOpen` / `didChange`. Incremental parsing deferred post-v1. ### 6.11 UTF-16 / Position Encoding LSP positions negotiated as UTF-8 (`positionEncoding = "utf-8"`) during `initialize` where the client supports LSP 3.17+. UTF-16 conversion fallback implemented in the LSP layer for older clients. `nuwiki-core` is always UTF-8 / byte-offset internally. --- ## 7. Editor Integration ### 7.1 Plugin Manager Installation **lazy.nvim:** ```lua { "gffranco/nuwiki", build = "lua require('nuwiki').install()", ft = { "vimwiki" }, opts = {}, } ``` **vim-plug:** ```vim Plug 'gffranco/nuwiki', { 'do': 'vim -e -s -c "source scripts/download_bin.vim" -c "q"' } ``` **Dein:** ```vim call dein#add('gffranco/nuwiki', { \ 'build': 'vim -e -s -c "source scripts/download_bin.vim" -c "q"' \ }) ``` ### 7.2 Binary Distribution - Pre-built binaries published as Gitea release assets - Named: `nuwiki-ls-{version}-{target}.tar.gz` - Downloaded at install time by `lua/nuwiki/install.lua` or `scripts/download_bin.vim` - Fallback: `cargo build --release` if download fails or `g:nuwiki_build_from_source = 1` - Binary installed to `{plugin_dir}/bin/nuwiki-ls[.exe]` ### 7.3 Editor Detection ```vim " plugin/nuwiki.vim if has('nvim') lua require('nuwiki').setup() else call nuwiki#lsp#start() endif ``` ### 7.4 Vim LSP Client Preference Order (Vim only) 1. `vim-lsp` (matoto/vim-lsp) 2. `coc.nvim` 3. Error message if neither found ### 7.5 User Config Schema ```lua require('nuwiki').setup({ wiki_root = "~/vimwiki", -- root directory of the wiki file_extension = ".wiki", -- file extension to associate syntax = "vimwiki", -- "vimwiki" | "markdown" (future) log_level = "warn", -- "error"|"warn"|"info"|"debug" }) ``` ### 7.6 Health Check `:checkhealth nuwiki` verifies: - Binary exists at `{plugin_dir}/bin/nuwiki-ls` - Binary is executable and responds to `--version` - LSP client is running - Filetype detection works for `.wiki` files --- ## 8. CI/CD ### 8.1 Runner Setup | Property | Value | |---|---| | System | Gitea Actions (`act_runner`) | | Mode | Docker (jobs run in containers) | | Docker access | Host socket mounted (`/var/run/docker.sock`) | | Internet access | Yes | ### 8.2 Workflow Files **`.gitea/workflows/ci.yaml`** — triggers on every push and PR: - `cargo fmt --check` - `cargo clippy -- -D warnings` - `cargo test --workspace` **`.gitea/workflows/release.yaml`** — triggers on `v*` tag push: - Cross-compile for all 4 Linux targets using `cross` - Package binaries as `.tar.gz` - Create Gitea release and upload assets via REST API using `RELEASE_TOKEN` secret - crates.io publish is deferred — workflow ships the Gitea release only. Re-enable by adding a `cargo publish -p nuwiki-core` job once the crate is ready for publication. ### 8.3 Build Targets | Target | Built by | |---|---| | `x86_64-unknown-linux-gnu` | CI (`cross`) | | `aarch64-unknown-linux-gnu` | CI (`cross`) | | `x86_64-unknown-linux-musl` | CI (`cross`) | | `aarch64-unknown-linux-musl` | CI (`cross`) | | `x86_64-apple-darwin` | Manual | | `aarch64-apple-darwin` | Manual | | `x86_64-pc-windows-msvc` | Manual | ### 8.4 Release Secrets | Secret | Purpose | |---|---| | `RELEASE_TOKEN` | Gitea personal access token for creating releases and uploading assets | | ~~`CARGO_REGISTRY_TOKEN`~~ | ~~crates.io API token for publishing `nuwiki-core`~~ — deferred (see §8.2) | ### 8.5 `actions/cache` Configuration Requires `act_runner`'s `config.yaml` to have the cache server host set to the runner host's LAN IP so job containers can reach it. ### 8.6 Breaking Change Policy A semver-breaking change (`v0.x` → `v0.x+1` or `v1.x` → `v2.0`) is defined as any of: - AST node type additions, removals, or field changes in `nuwiki-core` - `SyntaxPlugin` or `Renderer` trait signature changes - User config schema key removals or type changes - Removed LSP capabilities --- ## 9. Vimwiki Syntax Feature Checklist ### Typefaces - [ ] Bold: `*text*` - [ ] Italic: `_text_` - [ ] Bold italic: `_*text*_` / `*_text_*` - [ ] Strikethrough: `~~text~~` - [ ] Inline code: `` `text` `` - [ ] Superscript: `super^script^` - [ ] Subscript: `sub,,script,,` - [ ] Keywords: `TODO` `DONE` `STARTED` `FIXME` `FIXED` `XXX` ### Links - [ ] Plain wikilink: `[[Target]]` - [ ] Described wikilink: `[[Target|Description]]` - [ ] Subdirectory wikilink: `[[dir/Page]]` - [ ] Root-relative wikilink: `[[/Page]]` - [ ] Filesystem-absolute wikilink: `[[//path]]` - [ ] Subdirectory link: `[[dir/]]` - [ ] Interwiki numbered: `[[wiki1:Page]]` - [ ] Interwiki named: `[[wn.Name:Page]]` - [ ] Diary link: `[[diary:YYYY-MM-DD]]` - [ ] Anchor-only link: `[[#Anchor]]` - [ ] Wikilink with anchor: `[[Page#Anchor]]` - [ ] Raw URLs: `https://…` `mailto:…` `ftp://…` - [ ] External file link: `[[file:path]]` / `[[local:path]]` - [ ] Transclusion: `{{URL}}` with optional alt and attrs - [ ] Thumbnail link: `[[imgURL|{{thumbURL}}]]` ### Headers - [ ] Levels 1–6: `= H1 =` … `====== H6 ======` - [ ] Centered header (leading whitespace before `=`) ### Lists - [ ] Unordered: `-` and `*` - [ ] Ordered: `1.` `1)` `a)` `A)` `i)` `I)` `#` - [ ] Nested and mixed types - [ ] Multi-line items (indentation continuation) - [ ] Definition lists: `Term:: Definition` - [ ] Checkboxes: `[ ]` `[.]` `[o]` `[O]` `[X]` `[-]` ### Tables - [ ] Basic `|`-delimited table - [ ] Header row separator: `|---|` - [ ] Column span: `>` - [ ] Row span: `\/` - [ ] Inline formatting inside cells ### Preformatted Text - [ ] Fenced block: `{{{ … }}}` - [ ] Optional language/class on opening fence ### Mathematical Formulae - [ ] Inline math: `$ … $` - [ ] Block display math: `{{$ … }}$` - [ ] Block environment math: `{{$%env% … }}$` ### Blockquotes - [ ] 4-space indent blockquote - [ ] `>` prefix blockquote ### Comments - [ ] Single-line: `%% …` - [ ] Multi-line: `%%+ … +%%` ### Horizontal Rule - [ ] Four or more dashes: `----` ### Placeholders - [ ] `%title ` - [ ] `%nohtml` - [ ] `%template ` - [ ] `%date ` --- ## 10. Implementation Phases | Phase | Name | Key Output | |---|---|---| | 0 | Scaffolding | Compiling empty workspace + CI skeleton | | 1 | Core AST | All node types, spans, Visitor trait | | 2 | Syntax Plugin Interface | `Lexer`/`Parser`/`SyntaxPlugin` traits, `SyntaxRegistry` | | 3 | Vimwiki Lexer | `VimwikiToken`, two-pass lexer with spans | | 4 | Vimwiki Parser | Full AST from token stream, error recovery | | 5 | Renderer | `Renderer` trait + `HtmlRenderer` | | 6 | LSP Foundation | `didOpen`/`didChange`, diagnostics, document symbols | | 7 | Semantic Tokens | AST → LSP semantic token stream | | 8 | Navigation | Definition, references, hover, completion, workspace index | | 9 | Editor Glue | Installable plugin, health check, binary download | | 10 | CI/CD | Automated lint/test/release pipeline | | **— v1.1: full vimwiki replacement (see §12) —** | | | | 11 | Plumbing prerequisites | Server config receipt, `WorkspaceEditBuilder` + span helpers, closed-doc loader, diagnostic source chain, `Wiki` aggregate (single-entry to start) | | 12 | Tags | `:tag:` lex + parse, `TagNode`, index, tag-as-anchor resolution | | 13 | Workspace edits + executeCommand | `workspace/rename` with cross-doc link rewrite, `DeleteFile` ops, `executeCommand` infrastructure | | 14 | List & table edit commands | Server commands returning `WorkspaceEdit` for checkbox toggle, list-symbol/level change, renumber, table align, column move, table insert | | 15 | Link health + link/TOC generation | Broken-link diagnostics, `nuwiki.toc.generate`, `nuwiki.links.generate`, `nuwiki.workspace.checkLinks` | | 16 | Diary | Diary subpath, today/yesterday/tomorrow open commands, diary index generation, calendar hook API | | 17 | HTML export commands | `nuwiki.export.*` commands, `%template` resolution, CSS file management, auto-export option | | 18 | Multi-wiki | `wikis = [...]` config shape, multi-entry `Wiki` aggregate (data structures already landed in Phase 11), URI → wiki resolution, wiki-picker commands | | 19 | Editor glue v2 | Full `:Vimwiki*` command compat layer, default keymaps, text objects (`ah`/`aH`/`a\\`/`ac`/`al`), folding via LSP `foldingRange` + `foldexpr` fallback | --- ## 11. Pending Decisions These decisions are required before or during the phase indicated. | # | Decision | Needed by | Options | Notes | |---|---|---|---|---| | ~~P1~~ | ~~**License**~~ | ~~Phase 0~~ | ✅ **Dual MIT/Apache-2.0** | | | ~~P2~~ | ~~**MSRV**~~ | ~~Phase 0~~ | ✅ **stable-2 (Rust 1.83)** | | | ~~P3~~ | ~~**String representation in AST**~~ | ~~Phase 1~~ | ✅ **`String` (owned)** | | | ~~P4~~ | ~~**Visitor pattern**~~ | ~~Phase 1~~ | ✅ **Defined in Phase 1** | Open-recursion `Visitor` trait + `walk_*` helpers | | ~~P5~~ | ~~**Minimum Neovim version**~~ | ~~Phase 9~~ | ✅ **Neovim 0.11+** | Server registered via `vim.lsp.config{}` + `vim.lsp.enable`; a `vim.lsp.start` autocmd fallback is wired up but only fires when the declarative API is unavailable. | | ~~P6~~ | ~~**Minimum Vim version**~~ | ~~Phase 9~~ | ✅ **Vim 9.1+** | autoload glue assumes 9.1 idioms; uses `vim-lsp` first, then falls back to `coc.nvim`. | | ~~P7~~ | ~~**Semantic token type mapping**~~ | ~~Phase 7~~ | ✅ **Custom vimwiki-specific token types** | ~20 types (`vimwikiHeading`, `vimwikiBold`, …) + `level1`..`level6` + `centered` modifiers. Phase 9 ships default highlight groups in `syntax/nuwiki.vim` and the Lua glue. | | ~~P8~~ | ~~**Workspace indexing strategy**~~ | ~~Phase 8~~ | ✅ **Lazy + background with progress** | Initial scan runs as a background tokio task; nav features answer with partial data until complete; progress reported via `window/workDoneProgress`. | | ~~P9~~ | ~~**macOS runner**~~ | ~~Phase 10~~ | ✅ **Stay manual** | Release workflow cross-compiles the 4 Linux targets via `cross`; macOS + Windows binaries are produced ad hoc by maintainers and uploaded to the same Gitea release. Revisit when a Mac runner is available. | | **v1.1 decisions** | | | | | | P10 | **Command namespace** | Phase 13 | `:Vimwiki*` only · `:Nuwiki*` only · both | Both → easy migration *and* discoverable native commands; one canonical name + alias. | | P11 | **Tag syntax** | Phase 12 | Strict vimwiki `:tag:` only · Add markdown `#tag` as a second flavour | Strict keeps parity; adding `#tag` collides with hash list markers and ordered-list `#`. | | P12 | **List-edit transport** | Phase 14 | Server returns `WorkspaceEdit` from `executeCommand` · Editor-side text manipulation in VimL/Lua | `WorkspaceEdit` keeps logic in Rust + testable, but adds latency per keystroke for ``. | | P13 | **Multi-wiki config shape** | Phase 18 | Lua list (Neovim-native) · JSON config file (cross-editor) | Lua is ergonomic for Neovim; JSON works for Vim + arbitrary clients. | | ~~P14~~ | ~~**Folding mechanism**~~ | ~~Phase 19~~ | ✅ **LSP `textDocument/foldingRange` (primary) + `foldexpr` fallback** | Server already has the AST + heading boundaries — duplicating that knowledge as regex in VimL would be lossy. `foldexpr` ships in `ftplugin` as a fallback for when the server hasn't attached. | | P15 | **Diary path scheme** | Phase 16 | Fixed `/diary/` · Per-wiki configurable `diary_rel_path` | Vimwiki has `diary_rel_path`; matching keeps migrations clean. | | P16 | **Backwards-compat for v1.0 config** | Phase 18 | Preserve old single-`wiki_root` shape · Force migration to `wikis = [...]` | Single-wiki should keep working without re-config; `wiki_root` becomes sugar for `wikis = [{ root = wiki_root }]`. | | P17 | **Markdown syntax in v1.1** | Phase 12+ | Include · Defer to v1.2 | Architecture already supports it (`SyntaxPlugin`); cost is parser work + per-syntax differences in commands. | | P18 | **Server-config transport** | Phase 11 | `initializationOptions` only · `workspace/didChangeConfiguration` only · both | Both → simple boot path *and* live reload. Initialise at startup, accept change notifications afterwards. | --- ## 12. v1.1 — Full vimwiki Replacement v1.0 shipped the parsing + highlighting + navigation foundation. A vimwiki user transitioning to nuwiki today gets accurate syntax recognition and modern LSP nav, but loses the dense interactive layer that makes vimwiki ergonomic for daily authoring: ≈40 `:Vimwiki*` commands, ≈60 keymaps, the diary, the tag system, link health, HTML export commands, and multi-wiki configuration. v1.1 closes that gap. The architecture is unchanged — every new editing operation lands as an LSP `executeCommand` returning a `WorkspaceEdit`, so the `nuwiki-core` AST stays the single source of truth and the editor glue stays pure wiring. ### 12.1 Stability commitment v1.1 is additive. The following stay backward-compatible: - `nuwiki-core` public AST + Visitor (additions only; no field renames) - LSP capabilities advertised in v1.0 - `Renderer` trait + `HtmlRenderer` default output shape - v1.0 user-config keys (`wiki_root`, `file_extension`, `syntax`, `log_level`) New AST nodes (e.g. `TagNode`) and new LSP capabilities are opt-in by inspection. The Visitor trait gains default-bodied `visit_*` methods for new node kinds so existing Visitor implementations don't break. ### 12.2 Plumbing prerequisites (Phase 11) Cross-cutting infrastructure that every later v1.1 phase depends on. Landing it as Phase 11 — before any user-visible feature — keeps Phases 12–19 mechanical instead of full of "oh we also need to refactor X". #### 12.2.1 Server config receipt (P18) Per P18: both `initializationOptions` and `workspace/didChangeConfiguration` land in Phase 11. - `initialize.initializationOptions` carries the full v1.1 config (§12.11 schema) verbatim. Parsed into a server-side `Config` struct. - `workspace/didChangeConfiguration` re-applies the config for live reload. - Every handler reads from a single `Arc>` field on `Backend`. ```rust pub struct Config { pub log_level: LogLevel, pub diagnostic: DiagnosticConfig, pub wikis: Vec, // always ≥ 1; v1.0-shape desugars // … } impl Config { pub fn from_init_options(value: serde_json::Value) -> Self; pub fn from_v1_0_compat(legacy: LegacyConfig) -> Self; // P16 path } ``` #### 12.2.2 `WorkspaceEditBuilder` + span helpers New module `crates/nuwiki-lsp/src/edits.rs`: ```rust pub fn text_edit_replace(span: Span, replacement: String, text: &str, utf8: bool) -> TextEdit; pub fn text_edit_insert(pos: Position, content: String) -> TextEdit; pub fn text_edit_delete(span: Span, text: &str, utf8: bool) -> TextEdit; pub fn op_rename(old: Url, new: Url) -> DocumentChangeOperation; pub fn op_delete(uri: Url) -> DocumentChangeOperation; pub struct WorkspaceEditBuilder { changes: HashMap>, file_ops: Vec, } impl WorkspaceEditBuilder { pub fn edit(&mut self, uri: Url, edit: TextEdit) -> &mut Self; pub fn file_op(&mut self, op: DocumentChangeOperation) -> &mut Self; pub fn build(self) -> WorkspaceEdit; } ``` All v1.1 commands that produce `WorkspaceEdit` (Phases 13–17) go through the builder. Standardises UTF-8/UTF-16 conversion and document-change ordering. #### 12.2.3 Closed-doc loader ```rust pub struct DocSnapshot { pub text: String, pub ast: DocumentNode, pub in_memory: bool, // held in `documents` DashMap } impl Backend { pub async fn read_or_open(&self, uri: &Url) -> io::Result; } ``` If the URI is in `documents`, return live state. Otherwise read the file from disk and re-parse so cross-document operations (Phase 13 `workspace/rename`) can compute accurate spans + UTF-16 conversions instead of trusting stale `WorkspaceIndex` data. #### 12.2.4 Diagnostic source chain Today's `ast_diagnostics(&ast, &text, utf8)` is replaced by a composable collector: ```rust pub fn collect_diagnostics( ast: &DocumentNode, text: &str, index: Option<&WorkspaceIndex>, page_name: Option<&str>, cfg: &DiagnosticConfig, utf8: bool, ) -> Vec; ``` Composes: 1. Parse errors (`ErrorNode` walk; already shipped) 2. Broken-link warnings (Phase 15) 3. (future) other sources Each gated by `cfg`. Per-source severity is config-driven. #### 12.2.5 `Wiki` aggregate Single-wiki state becomes a `Wiki` aggregate, even though Phase 11 only ever builds one entry. Phase 18 then changes config shape, not data structures. ```rust pub struct WikiId(u32); pub struct WikiConfig { pub name: String, pub root: PathBuf, pub file_extension: String, pub syntax: String, pub diary_rel_path: String, pub html_path: PathBuf, pub template_path: PathBuf, // … all per-wiki options } pub struct Wiki { pub id: WikiId, pub config: WikiConfig, pub index: Arc>, } struct Backend { // … wikis: Arc>>, } impl Backend { fn resolve_uri_to_wiki(&self, uri: &Url) -> Option; // longest-prefix root match fn wiki(&self, id: WikiId) -> Option>; fn default_wiki(&self) -> Option>; } ``` `WorkspaceIndex` itself doesn't gain any new responsibilities in this phase — it just lives inside a `Wiki`. All v1.0 handlers that today read the single `index` field get refactored to call `default_wiki()` (or `resolve_uri_to_wiki()` where the URI is known). #### 12.2.6 What does *not* change Explicitly out of scope for Phase 11: - AST shape (no new node kinds — Tags wait for Phase 12) - LSP capability set (no new providers — capabilities expand from Phase 13) - HtmlRenderer surface (Phase 17 extends the template substitution set) - Anything user-visible (this phase is invisible to clients) #### 12.2.7 Tests Phase 11 is fully covered by unit tests on the new helpers: - `WorkspaceEditBuilder`: multi-uri accumulation, file-op ordering, UTF-16 conversion for non-ASCII spans. - `read_or_open`: live doc returns `in_memory: true`; missing path returns `Err`; on-disk path returns `in_memory: false` with parsed AST. - `collect_diagnostics`: parse errors emitted; link-health stub returns empty until Phase 15 wires it. - `Config::from_init_options`: legacy single-wiki shape desugars to one-entry `wikis = [...]`. - `Backend::resolve_uri_to_wiki`: longest-prefix match wins. ### 12.3 Tags (Phase 12) #### Syntax Vimwiki tags are colon-delimited sequences of non-space characters: ``` :tag-one:tag-two:other-tag: ``` Placement rules (matching vimwiki): - On line 1 or 2 of a file → file-level tag - Within 2 lines after a `= Heading =` → header-level tag - Otherwise → standalone anchor at the source line #### AST additions ``` TagNode (block) span: Span tags: Vec scope: TagScope -- File | Heading(idx) | Standalone PageMetadata (extend) tags: Vec -- file-level tags, accumulated ``` `TagNode` slots into `BlockNode` as a new variant. The Visitor gains `visit_tag` (default-bodied). #### Indexing `WorkspaceIndex` extends `IndexedPage` with: ``` IndexedPage (extend) tags: Vec TagInfo name: String scope: TagScope span: Span ``` Plus a `tags_by_name: HashMap>` reverse map. The existing backlink machinery in §6 stays as-is. #### LSP behaviour - `tags-as-anchors` extends `textDocument/definition`: `[[Page#some-tag]]` resolves to the tag's location in the target page (in addition to headings). - `workspace/symbol` results include tags (with `SymbolKind::PROPERTY` or a custom kind). - New `executeCommand` operations: `nuwiki.tags.search`, `nuwiki.tags.generateLinks(tag, ...)`, `nuwiki.tags.rebuild` (force re-index). ### 12.4 Workspace edits + `executeCommand` (Phase 13) > **Note on closed documents.** When `workspace/rename` rewrites incoming > links across pages that are *not* open in the editor, the server uses the > Phase 11 closed-doc loader (`read_or_open`) to re-parse them on demand. > Stored `WorkspaceIndex` spans are byte-accurate but may have drifted if > the on-disk file changed outside the editor; the re-parse guarantees we > emit `TextEdit`s against the current source text and convert to LSP > positions correctly (esp. for non-ASCII content under UTF-16 encoding). #### Capability ``` ServerCapabilities { execute_command_provider: Some(ExecuteCommandOptions { commands: vec!["nuwiki.*", …], }), rename_provider: Some(true), workspace: Some(WorkspaceServerCapabilities { file_operations: Some(FileOperationOptions { will_rename: Some(...), did_rename: Some(...), will_delete: Some(...), did_delete: Some(...), }), }), } ``` #### `workspace/rename` semantics When a user renames `Page A` → `Page B`: 1. Server emits a `WorkspaceEdit` containing a `RenameFile` op (`A.wiki` → `B.wiki`). 2. For every page in the index that links to `A`, the edit also rewrites `[[A]]` → `[[B]]` (preserving description, anchor, kind). 3. Anchors don't trigger renames — `[[A#anchor]]` follows. #### Custom commands router A single `executeCommand` handler dispatches by command name. Every command takes either: ``` { uri: Url, position?: Position, range?: Range, ...args } ``` and returns a `WorkspaceEdit` (which the client applies via `workspace/applyEdit`). ### 12.5 List & table editing commands (Phase 14) | Command | Operates on | Result | |---|---|---| | `nuwiki.list.toggleCheckbox` | list item under cursor | `[ ]` ↔ `[X]`, propagates parent state | | `nuwiki.list.cycleCheckbox` | list item | `[ ]` → `[.]` → `[o]` → `[O]` → `[X]` cycle | | `nuwiki.list.rejectCheckbox` | list item | toggle `[-]` rejected state | | `nuwiki.list.changeSymbol` | list item, args: `symbol: ListSymbol`, `whole_list: bool` | rewrite marker, renumber if numeric | | `nuwiki.list.changeLevel` | list item, args: `delta: i32`, `whole_subtree: bool` | re-indent + adjust child levels | | `nuwiki.list.renumber` | list, args: `whole_file: bool` | re-sequence numeric markers | | `nuwiki.list.removeDone` | range or current list | remove every `[X]` / `[-]` item and its checked children | | `nuwiki.list.nextTask` | document | navigate to next unfinished task (returns `Location`, not edit) | | `nuwiki.table.align` | table under cursor | reformat columns to max width | | `nuwiki.table.moveColumn` | cell, args: `dir: "left"\|"right"` | swap column with neighbour | | `nuwiki.table.insert` | cursor, args: `cols, rows` | insert blank table at cursor | | `nuwiki.heading.addLevel` | heading | promote (`==` → `===`) | | `nuwiki.heading.removeLevel` | heading | demote | | `nuwiki.link.normalize` | word/selection | convert to `[[wikilink]]`, add description if missing | | `nuwiki.link.pasteWikilink` | cursor | paste current page name as an absolute wikilink | | `nuwiki.link.pasteUrl` | cursor | paste the corresponding HTML output URL | | `nuwiki.colorize` | range, args: `color: String` | wrap in colour tag per `color_tag_template` | All operations resolve to a textual diff over the existing source so the AST stays the source of truth. None of them rely on editor-side state. ### 12.6 Link health + TOC/index generation (Phase 15) #### Broken-link diagnostics A new diagnostic source `nuwiki.link` runs after each parse: - Wiki target → not in `WorkspaceIndex` → severity from config (default `Warning`) - Anchor target → page exists but anchor doesn't → `Warning` - `file:` / `local:` → file exists on disk → `Warning` if missing - Raw URL / external → never diagnosed (we don't fetch) - Configurable filtering via `diagnostic.link.severity` (`off|hint|warn|error`) #### Commands | Command | Behaviour | |---|---| | `nuwiki.toc.generate` | inserts a nested list of headings in the current page; replaces existing `%toc` placeholder if present | | `nuwiki.links.generate` | inserts a flat list of all wiki pages (vimwiki's `:VimwikiGenerateLinks`) | | `nuwiki.workspace.checkLinks` | returns a `Vec` for the client to render in a quickfix-style view; also emits diagnostics | | `nuwiki.workspace.findOrphans` | returns pages with no incoming links | ### 12.7 Diary (Phase 16) > **Date parsing.** Diary entries use `YYYY-MM-DD` (and `YYYY-Www`, > `YYYY-MM`, `YYYY` for non-daily frequencies). v1.1 ships a small > hand-rolled parser/formatter in `nuwiki-core` for these formats — > no `chrono` / `time` dependency. The parsable surface is small enough > that the dependency cost outweighs the convenience. #### Path conventions ``` // # default: "diary" diary.wiki # configurable: diary_index 2026-05-11.wiki # daily: YYYY-MM-DD 2026-W19.wiki # weekly: YYYY-Www (configurable) 2026-05.wiki # monthly 2026.wiki # yearly ``` #### Custom commands | Command | Behaviour | |---|---| | `nuwiki.diary.openToday` | open / create today's diary page | | `nuwiki.diary.openYesterday` | analogous | | `nuwiki.diary.openTomorrow` | analogous | | `nuwiki.diary.openIndex` | open the diary index page | | `nuwiki.diary.generateIndex` | rebuild the diary index page with current entries grouped by year/month | | `nuwiki.diary.next` | navigate to the chronologically next diary entry from the current one | | `nuwiki.diary.prev` | analogous | #### Calendar hook External plugins (Calendar.vim, neorg-style calendars) can query: - `nuwiki.diary.listEntries(year, month)` → list of dates with entries - `nuwiki.diary.openForDate(date)` → opens that date's entry These are `executeCommand` operations, not LSP standard methods. ### 12.8 HTML export commands (Phase 17) The `HtmlRenderer` from Phase 5 stays as the engine. v1.1 wraps it in user-facing commands and the per-wiki HTML lifecycle. #### Per-wiki HTML options | Key | Default | Description | |---|---|---| | `html_path` | `/_html` | output directory | | `template_path` | `/_templates` | template lookup root | | `template_default` | `default` | base template name | | `template_ext` | `.tpl` | template extension | | `template_date_format` | `%Y-%m-%d` | format passed to `{{date}}` | | `css_name` | `style.css` | CSS file copied/created at export | | `auto_export` | `false` | export on save | | `html_filename_parameterization` | `false` | URL-safe slugs in output filenames | | `exclude_files` | `[]` | globs to skip | #### Commands | Command | Behaviour | |---|---| | `nuwiki.export.currentToHtml` | render current page; honours `%template`, copies CSS if missing | | `nuwiki.export.allToHtml` | export every page in the wiki | | `nuwiki.export.allToHtmlForce` | `:VimwikiAll2HTML!` equivalent | | `nuwiki.export.browse` | export current + open default browser to result | | `nuwiki.export.rss` | emit `rss.xml` of recent diary entries (when diary enabled) | #### Template variables The `HtmlRenderer` template substitution gains: - `{{title}}` (already shipped) - `{{content}}` (already shipped) - `{{date}}` (formatted via `template_date_format`) - `{{root_path}}` (relative path from output to `html_path` — for stylesheet hrefs in nested pages) - `{{toc}}` (rendered TOC for current page) ### 12.9 Multi-wiki (Phase 18) > **Data-structure pre-work.** The `Wiki` aggregate, `WorkspaceIndex` > per-wiki ownership, and `Backend::resolve_uri_to_wiki` already landed > in Phase 11 with a single-entry `wikis` list. Phase 18 is therefore > a *config-shape* change (accept `wikis = [...]` from > `initializationOptions`) and *resolution-policy* change (interwiki > links resolve through the matching wiki's index) — not a refactor of > the data flow. #### Config shape ```lua require('nuwiki').setup({ wikis = { { name = 'personal', root = '~/vimwiki', file_extension = '.wiki', syntax = 'vimwiki', diary_rel_path = 'diary', html_path = '~/vimwiki/_html', -- … any per-wiki option }, { name = 'work', root = '~/work-notes', syntax = 'vimwiki', }, }, log_level = 'warn', }) ``` The v1.0 single-wiki shape continues to work; internally it desugars to `wikis = [{ root = wiki_root, file_extension = file_extension, syntax = syntax }]`. #### Per-wiki context `WorkspaceIndex` becomes one per wiki. Backend holds: ``` indexes: Arc>>> ``` URI → wiki resolution walks the URI's filesystem path upward, matching against each registered root. Cross-wiki links (interwiki) resolve through the matching wiki's index. #### Custom commands | Command | Behaviour | |---|---| | `nuwiki.wiki.select` | client-side picker over `wikis`; resolves to a wiki id | | `nuwiki.wiki.openIndex` | open the selected wiki's `index.wiki` | | `nuwiki.wiki.tabOpenIndex` | open in new tab (client-side) | | `nuwiki.wiki.listAll` | returns the registered wikis for editor UIs | ### 12.10 Editor glue v2 (Phase 19) The thickest user-facing layer. Per SPEC §6.3 it still contains no logic — every command body is a wrapper that issues `workspace/executeCommand`. #### `:Vimwiki*` command compatibility `ftplugin/nuwiki.vim` defines every command listed in §4 of vimwiki's help, mapped to the corresponding `executeCommand`: | Vimwiki command | nuwiki implementation | |---|---| | `:VimwikiIndex` `[count]` | `nuwiki.wiki.openIndex(count)` | | `:VimwikiTabIndex` | tab-open variant | | `:VimwikiUISelect` | `nuwiki.wiki.select` | | `:VimwikiDiaryIndex` | `nuwiki.diary.openIndex` | | `:VimwikiMakeDiaryNote` | `nuwiki.diary.openToday` | | `:VimwikiMakeYesterdayDiaryNote` | `nuwiki.diary.openYesterday` | | `:VimwikiMakeTomorrowDiaryNote` | `nuwiki.diary.openTomorrow` | | `:VimwikiFollowLink` | client `vim.lsp.buf.definition()` | | `:VimwikiGoBackLink` | client `` (jumplist) | | `:VimwikiSplitLink` / `:VimwikiVSplitLink` | client-side `:split`/`:vsplit` + `definition` | | `:VimwikiTabnewLink` / `:VimwikiTabDropLink` | client-side `:tabnew` + `definition` | | `:VimwikiNextLink` / `:VimwikiPrevLink` | `nuwiki.cursor.nextLink` / `prevLink` | | `:VimwikiGoto {name}` | `nuwiki.wiki.gotoPage(name)` | | `:VimwikiDeleteFile` | server-side delete via `executeCommand` | | `:VimwikiRenameFile` | client prompt → `workspace/rename` | | `:VimwikiRemoveDone` | `nuwiki.list.removeDone` | | `:VimwikiNextTask` | `nuwiki.list.nextTask` | | `:Vimwiki2HTML` | `nuwiki.export.currentToHtml` | | `:Vimwiki2HTMLBrowse` | `nuwiki.export.browse` | | `:VimwikiAll2HTML[!]` | `nuwiki.export.allToHtml[Force]` | | `:VimwikiRss` | `nuwiki.export.rss` | | `:VimwikiToggleListItem` | `nuwiki.list.toggleCheckbox` | | `:VimwikiToggleRejectedListItem` | `nuwiki.list.rejectCheckbox` | | `:VimwikiListChangeLvl CMD` | `nuwiki.list.changeLevel` / `changeSymbol` | | `:VimwikiSearch` / `:VWS` | client `:lvimgrep` wrapper over wiki root | | `:VimwikiBacklinks` / `:VWB` | client `vim.lsp.buf.references()` | | `:VimwikiTable [cols] [rows]` | `nuwiki.table.insert` | | `:VimwikiTableMoveColumnLeft/Right` | `nuwiki.table.moveColumn` | | `:VimwikiGenerateLinks` | `nuwiki.links.generate` | | `:VimwikiDiaryGenerateLinks` | `nuwiki.diary.generateIndex` | | `:VimwikiDiaryNextDay` / `:VimwikiDiaryPrevDay` | `nuwiki.diary.next/prev` | | `:VimwikiTOC` | `nuwiki.toc.generate` | | `:VimwikiCheckLinks` | `nuwiki.workspace.checkLinks` | | `:VimwikiRebuildTags` | `nuwiki.tags.rebuild` | | `:VimwikiSearchTags {tag}` | `nuwiki.tags.search` | | `:VimwikiGenerateTagLinks` | `nuwiki.tags.generateLinks` | | `:VimwikiColorize {color}` | `nuwiki.colorize` | | `:VimwikiPasteLink` | `nuwiki.link.pasteWikilink` | | `:VimwikiPasteUrl` | `nuwiki.link.pasteUrl` | Each `:Vimwiki*` form ships with a matching `:Nuwiki*` alias (P10 resolution). #### Default keymaps `g:nuwiki_key_mappings` (Vim) / `mappings = { … }` (Neovim) controls opt-in keymap registration. Default: enabled; group flags let users disable the table/list/heading/diary subsets independently. Buffer-local maps mirror vimwiki: - `ww` / `wt` / `ws` / `wi` / `ww` etc. — wiki navigation - `` / `` / `` / `` / `` — link follow variants - `` / `` / `` — link history + per-page link navigation - `=` / `-` / `[[` / `]]` / `[=` / `]=` / `]u` / `[u` — header manipulation + navigation (pure VimL/Lua, regex over current buffer — no LSP round-trip) - `+` — `nuwiki.link.normalize` - `` — `nuwiki.list.toggleCheckbox` - `gnt` — `nuwiki.list.nextTask` - `gl` / `gL` — checkbox remove (single / list) - `gln` / `glp` — checkbox cycle up/down - `gll` / `gLl` / `glh` / `gLh` — list level change (single / subtree) - `glr` / `gLr` — list renumber - `gl*` / `gl#` / `gl-` / `gl+` / `gl1` / `gla` / `glA` / `gli` / `glI` — list symbol change (single / subtree variants `gL`*) - `glx` — `:VimwikiToggleRejectedListItem` - `gqq` / `gww` / `gq1` / `gw1` — table align - `` / `` — table move column - `` / `` — diary prev/next day - `wh` / `whh` — HTML export current / browse - `wc` — colorize (asks for colour) - `wn` — goto/create new page - `wd` / `wr` — delete / rename current page - Mouse: `<2-LeftMouse>`, ``, ``, `` (opt-in via `g:nuwiki_mouse_mappings`) #### Insert-mode behaviour - Table cells: ``/``/`` navigate cells, create rows on overflow; implemented in Lua/VimL via a small autocmd - Lists: `` inserts the next bullet/number; `` continues without a new bullet; ``/`` indent/dedent; ``/`` cycle the list symbol; `` toggles a list item on/off - All gated by `b:did_ftplugin` and remappable #### Text objects `omap`/`xmap` defining: | Object | Selects | |---|---| | `ah` / `ih` | a header (with/without trailing blank lines + header line itself) | | `aH` / `iH` | a header + all sub-headers ([count] climbs parent levels) | | `a\` / `i\` | a table cell | | `ac` / `ic` | a table column | | `al` / `il` | a list item (with/without children) | Pure VimL/Lua — no LSP round-trip. Reuses our AST-shaped regexes. #### Folding Per P14 resolution; the spec'd default (subject to that decision): - `setlocal foldmethod=expr` + `foldexpr=NuwikiFold(v:lnum)` (or LSP `foldingRange` driving it on Neovim 0.11+) - Folds at heading boundaries; nested headings nest folds - List-item folds when configured via `g:nuwiki_folding = 'list'` - Custom variant via `g:nuwiki_folding = 'custom'` #### Health check additions `:checkhealth nuwiki` v1.1 reports: - LSP commands registered (per command name) - Index loaded for each registered wiki - Tags table built - HTML output path writable - Default keymaps installed (or disabled by config) ### 12.11 Updated user-config schema ```lua require('nuwiki').setup({ -- single-wiki shorthand (v1.0 compat) wiki_root = '~/vimwiki', file_extension = '.wiki', syntax = 'vimwiki', log_level = 'warn', -- or the v1.1 multi-wiki form wikis = { { name = 'personal', root = '~/vimwiki', file_extension = '.wiki', syntax = 'vimwiki', diary_rel_path = 'diary', diary_frequency = 'daily', -- daily | weekly | monthly | yearly diary_start_week_day = 'monday', template_path = '~/vimwiki/_templates', template_default = 'default', template_ext = '.tpl', css_name = 'style.css', html_path = '~/vimwiki/_html', auto_export = false, auto_toc = false, auto_tags = false, exclude_files = {}, bullet_types = { '-', '*', '#' }, listsyms = ' .oOX', listsym_rejected = '-', listsyms_propagate = true, color_dic = { red = 'red', green = 'green' }, }, }, -- workspace-wide behaviour diagnostic = { link_severity = 'warn', -- off | hint | warn | error }, mappings = { enabled = true, table_editing = true, list_editing = true, header_nav = true, diary = true, html_export = true, mouse = false, }, folding = 'syntax', -- off | syntax | list | custom }) ``` ### 12.12 Updated §9 syntax checklist (additions) The following items extend the v1.0 checklist: #### Tags - [ ] Inline tag sequence: `:tag-one:tag-two:` - [ ] File-level tag (line 1–2 of a wiki page) - [ ] Header-level tag (within 2 lines after a heading) - [ ] Tag-as-anchor target (`[[Page#some-tag]]`) - [ ] Hex-colour code rendering in code blocks (`` `#ffe119` ``) #### Tables (additions) - [ ] Column-alignment markers in header separator: `|:--|` / `|:--:|` / `|--:|` #### Lists (parser extensions) - [ ] Multi-line list item (indented continuation lines) - [ ] Mixed-symbol lists at the same level ### 12.13 Updated CI/CD No new workflow files. The existing `ci.yaml` continues to gate `fmt`/`clippy`/`test`. New phases land behind feature additions in the existing crates; the test suite grows accordingly.