feat(config): P3 config batch — group 4 (valid_html_tags, emoji, color_tag_template)

- valid_html_tags: HTML export now passes the allowlisted inline tags through
  verbatim (b/i/sub/sup/kbd/div/center/strong/em/...) and escapes the rest,
  via a render-time tag-aware escaper (write_escaped_allowing/match_allowed_tag)
  — mirroring upstream's s:safe_html_line regex rather than a parser change.
  `span` is always allowed so colour spans render.
- emoji_enable: render-time `:alias:` -> glyph substitution (curated common
  set), default on; with_emoji builder + substitute_emoji/emoji_for.
- color_tag_template: :VimwikiColorize now wraps via a configurable template
  (g:nuwiki_color_tag_template / setup color_tag_template), splitting on
  __CONTENT__ with __COLORFG__ -> colour; both clients. Colour spans render in
  export via the valid_html_tags span passthrough. Default template reproduces
  the prior `<span style="color:NAME">` output (harness colorize tests green).

Renderer gets with_valid_html_tags + with_emoji; export.rs wires both from
HtmlConfig. Tests: valid_html_tags passthrough + escape, emoji on/off
(html_export.rs). Full rust suite + both harnesses green (Neovim 307,
Vim 301/18/21); clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 12:30:38 +00:00
parent dcb5d89c0c
commit 5d1ac09245
5 changed files with 256 additions and 5 deletions
+4
View File
@@ -257,6 +257,10 @@ pub fn render_page_html(
cfg.html_header_numbering_sym.clone(),
);
}
if !cfg.valid_html_tags.is_empty() {
r = r.with_valid_html_tags(cfg.valid_html_tags.clone());
}
r = r.with_emoji(cfg.emoji_enable);
r.render_to_string(doc)
}
+58
View File
@@ -232,6 +232,64 @@ fn template_for_falls_back_when_no_directive() {
// ===== render_page_html =====
#[test]
fn render_page_html_passes_through_valid_html_tags() {
// `<sub>`/`<kbd>` are in the default valid_html_tags → verbatim; `<script>`
// is not → escaped.
let ast = parse("H<sub>2</sub>O press <kbd>Ctrl</kbd> <script>x</script>\n");
let c = cfg("/tmp/x");
let html = export::render_page_html(
&ast,
Some("{{content}}".into()),
"Home",
DiaryDate::today_utc(),
&c.html,
None,
HashMap::new(),
)
.unwrap();
assert!(html.contains("H<sub>2</sub>O"), "sub verbatim: {html}");
assert!(html.contains("<kbd>Ctrl</kbd>"), "kbd verbatim: {html}");
assert!(html.contains("&lt;script&gt;"), "script escaped: {html}");
}
#[test]
fn render_page_html_substitutes_emoji_when_enabled() {
let ast = parse("ship it :rocket: and :tada:\n");
let c = cfg("/tmp/x"); // emoji_enable defaults true
let html = export::render_page_html(
&ast,
Some("{{content}}".into()),
"Home",
DiaryDate::today_utc(),
&c.html,
None,
HashMap::new(),
)
.unwrap();
assert!(html.contains("🚀"), "rocket: {html}");
assert!(html.contains("🎉"), "tada: {html}");
assert!(!html.contains(":rocket:"), "shortcode replaced: {html}");
}
#[test]
fn render_page_html_emoji_off_leaves_shortcodes() {
let ast = parse("plain :rocket: here\n");
let mut c = cfg("/tmp/x");
c.html.emoji_enable = false;
let html = export::render_page_html(
&ast,
Some("{{content}}".into()),
"Home",
DiaryDate::today_utc(),
&c.html,
None,
HashMap::new(),
)
.unwrap();
assert!(html.contains(":rocket:"), "left as-is: {html}");
}
#[test]
fn render_page_html_substitutes_title_content_and_extras() {
let ast = parse("%title My Page\n= One =\nbody text\n");