feat(13.1): colorize + color_dic → ColorNode renderer
CI / cargo fmt --check (push) Successful in 26s
CI / cargo clippy (push) Successful in 1m24s
CI / cargo test (push) Successful in 1m24s
CI / editor keymaps (push) Successful in 1m45s

Closes the last pending §13.1 entries:

- **`nuwiki.colorize`** (client-side, Lua + VimL) — wraps the word
  at cursor (or the visual selection on Neovim) in an inline
  `<span style="color:%c">…</span>` template. Matches vimwiki's
  default `color_tag_template`. Bound to `<Leader>wc` in both
  normal and visual mode on both editor paths; the previous
  "deferred" stub is gone.

- **`color_dic` → renderer** — `HtmlConfig` gains a
  `color_dic: HashMap<String, String>` field reading the same key
  out of `initializationOptions` / `didChangeConfiguration`. The
  HTML export path threads it into `HtmlRenderer::with_colors`;
  `ColorNode` now picks `style="color:<value>"` when the colour
  name is in the dict, falling back to the existing
  `class="color-<name>"` when it isn't.

SPEC §13.1 is fully checked off — the table moved from "deferred
sketch" to a per-command status table marking all 11 commands done,
plus a note on the `color_dic` follow-up landing alongside. README's
"Phase 14 list & table edit commands" row now reads  without the
deferred-subset caveat, and the §13.1-blocked text-objects note in
the keymaps section is rephrased as "planned follow-ups".

Tests: 4 new in `phase17_colorize.rs` covering the renderer
fallback when the dict is empty, the inline-style emission for a
listed name, the fallback path for an unlisted name in a populated
dict, and the `initializationOptions` JSON shape. Total 414 Rust
tests pass; clippy clean; keymap harness still 20/20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 14:45:39 +00:00
parent 4245424d2f
commit de8b8fa30d
10 changed files with 226 additions and 47 deletions
+11
View File
@@ -63,6 +63,11 @@ pub struct HtmlConfig {
/// Glob patterns (matched against the page name relative to root)
/// to skip during `allToHtml*`.
pub exclude_files: Vec<String>,
/// `color_dic`: vimwiki colour-tag name → CSS value mapping used
/// by the HTML renderer (SPEC §12.11). Empty by default; the
/// renderer falls back to `class="color-<name>"` when a name
/// isn't in the dict.
pub color_dic: std::collections::HashMap<String, String>,
}
impl Default for HtmlConfig {
@@ -77,6 +82,7 @@ impl Default for HtmlConfig {
auto_export: false,
html_filename_parameterization: false,
exclude_files: Vec::new(),
color_dic: std::collections::HashMap::new(),
}
}
}
@@ -281,6 +287,8 @@ struct RawWiki {
html_filename_parameterization: Option<bool>,
#[serde(default)]
exclude_files: Option<Vec<String>>,
#[serde(default)]
color_dic: Option<std::collections::HashMap<String, String>>,
}
impl From<RawWiki> for WikiConfig {
@@ -318,6 +326,9 @@ impl From<RawWiki> for WikiConfig {
if let Some(v) = r.exclude_files {
html.exclude_files = v;
}
if let Some(v) = r.color_dic {
html.color_dic = v;
}
WikiConfig {
name: r.name.unwrap_or(derived_name),
root,
+3
View File
@@ -220,6 +220,9 @@ pub fn render_page_html(
r = r.with_template(t);
}
r = r.with_vars(vars);
if !cfg.color_dic.is_empty() {
r = r.with_colors(cfg.color_dic.clone());
}
r.render_to_string(doc)
}
@@ -0,0 +1,75 @@
//! §13.1 §13.3 — `color_dic` → `HtmlRenderer` integration.
//!
//! Drives the renderer directly so we don't have to spin up the full
//! export pipeline. The end-to-end path (export command → renderer
//! with `cfg.color_dic` plumbed through) is integration-tested via the
//! Phase 17 export tests.
use std::collections::HashMap;
use nuwiki_core::ast::{ColorNode, DocumentNode, InlineNode, ParagraphNode, Span, TextNode};
use nuwiki_core::render::{HtmlRenderer, Renderer};
use nuwiki_lsp::config::config_from_json;
fn doc_with_color(name: &str, text: &str) -> DocumentNode {
let span = Span::default();
DocumentNode {
span,
metadata: Default::default(),
children: vec![nuwiki_core::ast::BlockNode::Paragraph(ParagraphNode {
span,
children: vec![InlineNode::Color(ColorNode {
span,
color: name.into(),
children: vec![InlineNode::Text(TextNode {
span,
content: text.into(),
})],
})],
})],
}
}
#[test]
fn color_falls_back_to_class_when_dict_empty() {
let doc = doc_with_color("red", "hello");
let r = HtmlRenderer::new();
let out = r.render_to_string(&doc).unwrap();
assert!(out.contains(r#"<span class="color-red">"#), "got: {out}");
assert!(out.contains("hello</span>"));
}
#[test]
fn color_dic_emits_inline_style_when_name_listed() {
let mut dict = HashMap::new();
dict.insert("red".to_string(), "#ff0000".to_string());
let doc = doc_with_color("red", "hello");
let r = HtmlRenderer::new().with_colors(dict);
let out = r.render_to_string(&doc).unwrap();
assert!(
out.contains(r#"<span style="color:#ff0000">"#),
"got: {out}"
);
}
#[test]
fn unlisted_color_falls_back_even_with_dict_set() {
let mut dict = HashMap::new();
dict.insert("red".to_string(), "#ff0000".to_string());
let doc = doc_with_color("violet", "x");
let r = HtmlRenderer::new().with_colors(dict);
let out = r.render_to_string(&doc).unwrap();
assert!(out.contains(r#"<span class="color-violet">"#), "got: {out}");
}
#[test]
fn config_from_json_accepts_color_dic() {
let cfg = config_from_json(serde_json::json!({
"wikis": [
{ "root": "/tmp/c", "color_dic": { "red": "red", "blue": "blue" } },
],
}));
let d = &cfg.wikis[0].html.color_dic;
assert_eq!(d.get("red").map(String::as_str), Some("red"));
assert_eq!(d.get("blue").map(String::as_str), Some("blue"));
}