feat(html): html_header_numbering section numbering for HTML export
CI / cargo fmt --check (push) Successful in 51s
CI / cargo clippy (push) Successful in 27s
CI / cargo test (push) Successful in 49s
CI / editor keymaps (push) Successful in 2m52s

Mirror vimwiki's html_header_numbering / html_header_numbering_sym:
top-level headings at or below the start level get a dotted section
number (1, 1.1, 1.2, ...) with a configurable trailing symbol. Off by
default (level 0). Nested headings in lists/quotes stay unnumbered,
matching upstream's document-level-only scan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 00:33:01 +00:00
parent 88114a65a4
commit 79b502fa5c
5 changed files with 184 additions and 3 deletions
+69
View File
@@ -41,6 +41,8 @@ fn html_config_defaults_match_vimwiki() {
assert!(!c.auto_export);
assert!(!c.html_filename_parameterization);
assert!(c.exclude_files.is_empty());
assert_eq!(c.html_header_numbering, 0); // upstream default: numbering off
assert!(c.html_header_numbering_sym.is_empty());
}
#[test]
@@ -270,6 +272,73 @@ fn render_page_html_uses_metadata_date_when_set() {
assert!(html.contains("DATE=2025-01-02"), "got: {html}");
}
#[test]
fn render_page_html_numbers_headers_when_enabled() {
let ast = parse("= Intro =\n== Background ==\n== Methods ==\n= Results =\n");
let mut c = cfg("/tmp/x");
c.html.html_header_numbering = 1; // number from h1
c.html.html_header_numbering_sym = ".".into();
let html = export::render_page_html(
&ast,
None,
"Home",
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
&c.html,
None,
-1,
HashMap::new(),
)
.unwrap();
assert!(html.contains("<h1>1. Intro</h1>"), "got: {html}");
assert!(html.contains("<h2>1.1. Background</h2>"), "got: {html}");
assert!(html.contains("<h2>1.2. Methods</h2>"), "got: {html}");
assert!(html.contains("<h1>2. Results</h1>"), "got: {html}");
}
#[test]
fn render_page_html_numbering_skips_headers_above_start_level() {
let ast = parse("= Title =\n== Section ==\n=== Sub ===\n");
let mut c = cfg("/tmp/x");
c.html.html_header_numbering = 2; // start at h2; h1 stays unnumbered
// empty sym → number followed by a bare space
let html = export::render_page_html(
&ast,
None,
"Home",
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
&c.html,
None,
-1,
HashMap::new(),
)
.unwrap();
assert!(
html.contains("<h1>Title</h1>"),
"h1 unnumbered, got: {html}"
);
assert!(html.contains("<h2>1 Section</h2>"), "got: {html}");
assert!(html.contains("<h3>1.1 Sub</h3>"), "got: {html}");
}
#[test]
fn render_page_html_no_header_numbering_by_default() {
let ast = parse("= Intro =\n== Sub ==\n");
let c = cfg("/tmp/x");
let html = export::render_page_html(
&ast,
None,
"Home",
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
&c.html,
None,
-1,
HashMap::new(),
)
.unwrap();
assert!(html.contains("<h1>Intro</h1>"), "got: {html}");
assert!(html.contains("<h2>Sub</h2>"), "got: {html}");
}
#[test]
fn render_page_html_root_path_for_nested_pages() {
let ast = parse("= D =\n");