feat(config): configurable generated-section captions (P2)
TOC / links / tags section headings (and their levels) were hardcoded. Added per-wiki toc_header/toc_header_level, links_header/links_header_level, tags_header/tags_header_level — defaults Contents / Generated Links / Generated Tags at level 1, matching upstream (also fixes the tags index heading: Tags -> Generated Tags). A caption_line(name, level) helper emits the `=`-markers; the configured text + level thread through toc_edit / links_edit / tag_links_edit and the auto_toc save hook. find_section_range matches the configured text, so regeneration stays idempotent at any level. Tests: captions_honour_custom_header_and_level (link_health) + config round-trip + default assertions (index_and_config); existing tag/toc/links tests updated for the new arity and the Generated Tags default. fmt/clippy clean (8-arg ops get allow(too_many_arguments), matching the codebase precedent); 0 Rust test failures; config-parity green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -298,7 +298,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
||||
let src = "= Top =\n== Sub ==\ntext\n";
|
||||
let doc = parse(src);
|
||||
let uri = Url::from_file_path("/wiki/Page.wiki").unwrap();
|
||||
let edit = ops::toc_edit(src, &doc, &uri, true).expect("toc edit produced");
|
||||
let edit = ops::toc_edit(src, &doc, &uri, true, "Contents", 1).expect("toc edit produced");
|
||||
assert!(edit.changes.is_some() || edit.document_changes.is_some());
|
||||
|
||||
let toc = ops::build_toc_text(
|
||||
@@ -307,6 +307,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
||||
(2, "Sub".into(), "sub".into()),
|
||||
],
|
||||
"Contents",
|
||||
1,
|
||||
);
|
||||
assert!(toc.contains("= Contents ="));
|
||||
assert!(toc.contains("- [[#top|Top]]"));
|
||||
@@ -317,7 +318,7 @@ fn nuwiki_toc_generates_nested_contents() {
|
||||
#[test]
|
||||
fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
||||
let pages = vec!["About".to_string(), "Home".to_string(), "Notes".to_string()];
|
||||
let text = ops::build_links_text(&pages, "Links", Some("Home"));
|
||||
let text = ops::build_links_text(&pages, "Links", 1, Some("Home"));
|
||||
assert!(text.contains("= Links ="));
|
||||
assert!(text.contains("- [[About]]"));
|
||||
assert!(text.contains("- [[Notes]]"));
|
||||
@@ -326,7 +327,7 @@ fn nuwiki_generate_links_lists_all_pages_excluding_current() {
|
||||
let src = "= Existing =\n";
|
||||
let doc = parse(src);
|
||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
assert!(ops::links_edit(src, &doc, &uri, "Home", &pages, true).is_some());
|
||||
assert!(ops::links_edit(src, &doc, &uri, "Home", &pages, true, "Generated Links", 1).is_some());
|
||||
}
|
||||
|
||||
// :NuwikiCheckLinks — surface broken links across the workspace.
|
||||
@@ -383,21 +384,31 @@ fn nuwiki_generate_tag_links_builds_section() {
|
||||
);
|
||||
let by_tag = ops::tag_pages_snapshot(&idx);
|
||||
|
||||
let single = ops::build_tag_links_text(&by_tag, Some("alpha")).unwrap();
|
||||
let single = ops::build_tag_links_text(&by_tag, Some("alpha"), "Generated Tags", 1).unwrap();
|
||||
assert!(single.starts_with("= Tag: alpha ="));
|
||||
assert!(single.contains("- [[Home]]"));
|
||||
assert!(single.contains("- [[Work]]"));
|
||||
|
||||
// No-arg variant emits a section per tag.
|
||||
let all = ops::build_tag_links_text(&by_tag, None).unwrap();
|
||||
assert!(all.starts_with("= Tags ="));
|
||||
let all = ops::build_tag_links_text(&by_tag, None, "Generated Tags", 1).unwrap();
|
||||
assert!(all.starts_with("= Generated Tags ="));
|
||||
assert!(all.contains("== alpha =="));
|
||||
assert!(all.contains("== beta =="));
|
||||
|
||||
let src = "= Home =\n";
|
||||
let doc = parse(src);
|
||||
let uri = Url::from_file_path("/wiki/Home.wiki").unwrap();
|
||||
assert!(ops::tag_links_edit(src, &doc, &uri, Some("alpha"), &by_tag, true).is_some());
|
||||
assert!(ops::tag_links_edit(
|
||||
src,
|
||||
&doc,
|
||||
&uri,
|
||||
Some("alpha"),
|
||||
&by_tag,
|
||||
true,
|
||||
"Generated Tags",
|
||||
1
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
// :NuwikiRebuildTags — force a full workspace re-index. A rebuild must
|
||||
|
||||
@@ -203,7 +203,7 @@ fn tag_pages_snapshot_sorts_pages_alphabetically() {
|
||||
fn build_tag_links_for_single_tag() {
|
||||
let mut snap = BTreeMap::new();
|
||||
snap.insert("release".to_string(), vec!["Alpha".into(), "Beta".into()]);
|
||||
let out = ops::build_tag_links_text(&snap, Some("release")).unwrap();
|
||||
let out = ops::build_tag_links_text(&snap, Some("release"), "Generated Tags", 1).unwrap();
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "= Tag: release =");
|
||||
assert_eq!(lines[1], "- [[Alpha]]");
|
||||
@@ -213,7 +213,7 @@ fn build_tag_links_for_single_tag() {
|
||||
#[test]
|
||||
fn build_tag_links_for_missing_tag_returns_none() {
|
||||
let snap = BTreeMap::new();
|
||||
assert!(ops::build_tag_links_text(&snap, Some("ghost")).is_none());
|
||||
assert!(ops::build_tag_links_text(&snap, Some("ghost"), "Generated Tags", 1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -221,8 +221,8 @@ fn build_tag_links_full_index_groups_by_tag() {
|
||||
let mut snap = BTreeMap::new();
|
||||
snap.insert("a".to_string(), vec!["P1".into()]);
|
||||
snap.insert("b".to_string(), vec!["P2".into(), "P3".into()]);
|
||||
let out = ops::build_tag_links_text(&snap, None).unwrap();
|
||||
assert!(out.starts_with("= Tags =\n"));
|
||||
let out = ops::build_tag_links_text(&snap, None, "Generated Tags", 1).unwrap();
|
||||
assert!(out.starts_with("= Generated Tags =\n"));
|
||||
assert!(out.contains("== a =="));
|
||||
assert!(out.contains("== b =="));
|
||||
assert!(out.contains("- [[P1]]"));
|
||||
@@ -233,7 +233,7 @@ fn build_tag_links_full_index_groups_by_tag() {
|
||||
#[test]
|
||||
fn build_tag_links_full_index_returns_none_when_no_tags() {
|
||||
let snap = BTreeMap::new();
|
||||
assert!(ops::build_tag_links_text(&snap, None).is_none());
|
||||
assert!(ops::build_tag_links_text(&snap, None, "Generated Tags", 1).is_none());
|
||||
}
|
||||
|
||||
// ===== `nuwiki.tags.generateLinks` — tag_links_edit (in-buffer rewrite) =====
|
||||
@@ -245,8 +245,17 @@ fn tag_links_edit_inserts_when_section_missing() {
|
||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||
let mut snap = BTreeMap::new();
|
||||
snap.insert("release".to_string(), vec!["Alpha".into()]);
|
||||
let edit =
|
||||
ops::tag_links_edit(src, &ast, &uri, Some("release"), &snap, true).expect("got an edit");
|
||||
let edit = ops::tag_links_edit(
|
||||
src,
|
||||
&ast,
|
||||
&uri,
|
||||
Some("release"),
|
||||
&snap,
|
||||
true,
|
||||
"Generated Tags",
|
||||
1,
|
||||
)
|
||||
.expect("got an edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
// Insertion at start.
|
||||
assert_eq!(te.range.start, te.range.end);
|
||||
@@ -261,7 +270,17 @@ fn tag_links_edit_replaces_existing_section() {
|
||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||
let mut snap = BTreeMap::new();
|
||||
snap.insert("release".to_string(), vec!["Fresh".into()]);
|
||||
let edit = ops::tag_links_edit(src, &ast, &uri, Some("release"), &snap, true).expect("edit");
|
||||
let edit = ops::tag_links_edit(
|
||||
src,
|
||||
&ast,
|
||||
&uri,
|
||||
Some("release"),
|
||||
&snap,
|
||||
true,
|
||||
"Generated Tags",
|
||||
1,
|
||||
)
|
||||
.expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert_eq!(te.range.start.line, 0);
|
||||
assert!(te.new_text.contains("[[Fresh]]"));
|
||||
@@ -270,12 +289,13 @@ fn tag_links_edit_replaces_existing_section() {
|
||||
|
||||
#[test]
|
||||
fn tag_links_edit_full_index_replaces_existing_tags_section() {
|
||||
let src = "= Tags =\n- [[old]]\n";
|
||||
let src = "= Generated Tags =\n- [[old]]\n";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||
let mut snap = BTreeMap::new();
|
||||
snap.insert("alpha".to_string(), vec!["P".into()]);
|
||||
let edit = ops::tag_links_edit(src, &ast, &uri, None, &snap, true).expect("edit");
|
||||
let edit =
|
||||
ops::tag_links_edit(src, &ast, &uri, None, &snap, true, "Generated Tags", 1).expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("== alpha =="));
|
||||
assert!(!te.new_text.contains("[[old]]"));
|
||||
@@ -287,7 +307,17 @@ fn tag_links_edit_returns_none_for_unknown_tag() {
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/p.wiki").unwrap();
|
||||
let snap = BTreeMap::new();
|
||||
assert!(ops::tag_links_edit(src, &ast, &uri, Some("ghost"), &snap, true).is_none());
|
||||
assert!(ops::tag_links_edit(
|
||||
src,
|
||||
&ast,
|
||||
&uri,
|
||||
Some("ghost"),
|
||||
&snap,
|
||||
true,
|
||||
"Generated Tags",
|
||||
1
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// ===== smoke: COMMANDS list advertises tag commands =====
|
||||
|
||||
@@ -275,6 +275,12 @@ fn defaults_carry_vimwiki_per_wiki_keys() {
|
||||
assert_eq!(cfg.list_margin, -1);
|
||||
assert_eq!(cfg.links_space_char, " ");
|
||||
assert!(!cfg.auto_toc);
|
||||
// Generated-section captions match upstream defaults.
|
||||
assert_eq!(cfg.toc_header, "Contents");
|
||||
assert_eq!(cfg.toc_header_level, 1);
|
||||
assert_eq!(cfg.links_header, "Generated Links");
|
||||
assert_eq!(cfg.tags_header, "Generated Tags");
|
||||
assert_eq!(cfg.listsym_rejected, "-");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -295,6 +301,11 @@ fn raw_wiki_parses_every_new_key() {
|
||||
"list_margin": 2,
|
||||
"links_space_char": "_",
|
||||
"auto_toc": true,
|
||||
"toc_header": "Table of Contents",
|
||||
"toc_header_level": 2,
|
||||
"links_header": "All Pages",
|
||||
"tags_header": "Tag Index",
|
||||
"tags_header_level": 3,
|
||||
}],
|
||||
}));
|
||||
let w = &cfg.wikis[0];
|
||||
@@ -312,6 +323,12 @@ fn raw_wiki_parses_every_new_key() {
|
||||
assert_eq!(w.list_margin, 2);
|
||||
assert_eq!(w.links_space_char, "_");
|
||||
assert!(w.auto_toc);
|
||||
assert_eq!(w.toc_header, "Table of Contents");
|
||||
assert_eq!(w.toc_header_level, 2);
|
||||
assert_eq!(w.links_header, "All Pages");
|
||||
assert_eq!(w.links_header_level, 1); // unspecified → default
|
||||
assert_eq!(w.tags_header, "Tag Index");
|
||||
assert_eq!(w.tags_header_level, 3);
|
||||
|
||||
// The parsed keys drive a date-mode, Sunday-start diary calendar:
|
||||
// a weekly note is the week-start date (YYYY-MM-DD), stepping ±7 days.
|
||||
|
||||
@@ -290,7 +290,7 @@ fn build_toc_text_nests_by_level() {
|
||||
(2u8, "Sub".into(), "sub".into()),
|
||||
(1u8, "Other".into(), "other".into()),
|
||||
];
|
||||
let out = ops::build_toc_text(&items, "Contents");
|
||||
let out = ops::build_toc_text(&items, "Contents", 1);
|
||||
assert!(out.starts_with("= Contents =\n"));
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "= Contents =");
|
||||
@@ -301,16 +301,30 @@ fn build_toc_text_nests_by_level() {
|
||||
|
||||
#[test]
|
||||
fn build_toc_text_with_no_headings_is_just_the_heading() {
|
||||
let out = ops::build_toc_text(&[], "Contents");
|
||||
let out = ops::build_toc_text(&[], "Contents", 1);
|
||||
assert_eq!(out, "= Contents =\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captions_honour_custom_header_and_level() {
|
||||
// toc_header="Table of Contents", level 2 → `== Table of Contents ==`.
|
||||
let out = ops::build_toc_text(&[], "Table of Contents", 2);
|
||||
assert_eq!(out, "== Table of Contents ==\n");
|
||||
// links_header at level 3.
|
||||
let pages = vec!["A".to_string(), "B".to_string()];
|
||||
let links = ops::build_links_text(&pages, "All Pages", 3, None);
|
||||
assert!(links.starts_with("=== All Pages ===\n"));
|
||||
// level clamps to 1..=6.
|
||||
assert!(ops::build_toc_text(&[], "X", 9).starts_with("====== X ======"));
|
||||
assert!(ops::build_toc_text(&[], "X", 0).starts_with("= X ="));
|
||||
}
|
||||
|
||||
// ===== Links text generation =====
|
||||
|
||||
#[test]
|
||||
fn build_links_text_excludes_current_page() {
|
||||
let pages = vec!["A".to_string(), "Home".to_string(), "B".to_string()];
|
||||
let out = ops::build_links_text(&pages, "Generated Links", Some("Home"));
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, Some("Home"));
|
||||
let lines: Vec<&str> = out.lines().collect();
|
||||
assert_eq!(lines[0], "= Generated Links =");
|
||||
assert!(lines.contains(&"- [[A]]"));
|
||||
@@ -321,7 +335,7 @@ fn build_links_text_excludes_current_page() {
|
||||
#[test]
|
||||
fn build_links_text_no_excludes_includes_all() {
|
||||
let pages = vec!["A".to_string(), "B".to_string()];
|
||||
let out = ops::build_links_text(&pages, "Generated Links", None);
|
||||
let out = ops::build_links_text(&pages, "Generated Links", 1, None);
|
||||
assert!(out.contains("[[A]]"));
|
||||
assert!(out.contains("[[B]]"));
|
||||
}
|
||||
@@ -359,7 +373,7 @@ fn toc_edit_inserts_at_top_when_no_existing_toc() {
|
||||
let src = "= One =\n== Two ==\n";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true).expect("got an edit");
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1).expect("got an edit");
|
||||
let changes = edit.changes.expect("changes map");
|
||||
let edits = &changes[&uri];
|
||||
assert_eq!(edits.len(), 1);
|
||||
@@ -375,7 +389,7 @@ fn toc_edit_replaces_existing_toc() {
|
||||
let src = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true).expect("got an edit");
|
||||
let edit = ops::toc_edit(src, &ast, &uri, true, "Contents", 1).expect("got an edit");
|
||||
let changes = edit.changes.expect("changes map");
|
||||
let edits = &changes[&uri];
|
||||
let te = &edits[0];
|
||||
@@ -390,7 +404,7 @@ fn toc_edit_returns_none_for_empty_doc() {
|
||||
let src = "";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/empty.wiki").unwrap();
|
||||
assert!(ops::toc_edit(src, &ast, &uri, true).is_none());
|
||||
assert!(ops::toc_edit(src, &ast, &uri, true, "Contents", 1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -401,14 +415,14 @@ fn toc_rebuild_edit_only_acts_when_toc_already_present() {
|
||||
let without = "= One =\n== Two ==\n";
|
||||
let ast = parse(without);
|
||||
assert!(
|
||||
ops::toc_rebuild_edit(without, &ast, &uri, true).is_none(),
|
||||
ops::toc_rebuild_edit(without, &ast, &uri, true, "Contents", 1).is_none(),
|
||||
"auto_toc should not insert a TOC where none existed"
|
||||
);
|
||||
|
||||
// Existing TOC → rebuild refreshes it.
|
||||
let with = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||
let ast = parse(with);
|
||||
let edit = ops::toc_rebuild_edit(with, &ast, &uri, true).expect("rebuild edit");
|
||||
let edit = ops::toc_rebuild_edit(with, &ast, &uri, true, "Contents", 1).expect("rebuild edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("[[#real|Real]]"));
|
||||
assert!(!te.new_text.contains("Stale"));
|
||||
@@ -422,7 +436,8 @@ fn links_edit_inserts_when_section_absent() {
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
let pages = vec!["A".into(), "B".into(), "Home".into()];
|
||||
let edit = ops::links_edit(src, &ast, &uri, "Home", &pages, true).expect("edit");
|
||||
let edit =
|
||||
ops::links_edit(src, &ast, &uri, "Home", &pages, true, "Generated Links", 1).expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("[[A]]"));
|
||||
assert!(te.new_text.contains("[[B]]"));
|
||||
@@ -435,7 +450,8 @@ fn links_edit_replaces_when_section_present() {
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
let pages = vec!["Fresh".into()];
|
||||
let edit = ops::links_edit(src, &ast, &uri, "Home", &pages, true).expect("edit");
|
||||
let edit =
|
||||
ops::links_edit(src, &ast, &uri, "Home", &pages, true, "Generated Links", 1).expect("edit");
|
||||
let te = &edit.changes.unwrap()[&uri][0];
|
||||
assert!(te.new_text.contains("[[Fresh]]"));
|
||||
assert!(!te.new_text.contains("Stale"));
|
||||
@@ -447,7 +463,7 @@ fn links_edit_returns_none_for_empty_page_list() {
|
||||
let src = "Hi\n";
|
||||
let ast = parse(src);
|
||||
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||
assert!(ops::links_edit(src, &ast, &uri, "Home", &[], true).is_none());
|
||||
assert!(ops::links_edit(src, &ast, &uri, "Home", &[], true, "Generated Links", 1).is_none());
|
||||
}
|
||||
|
||||
// ===== find_orphans =====
|
||||
|
||||
Reference in New Issue
Block a user