feat(config): configurable generated-section captions (P2)
CI / cargo fmt --check (push) Successful in 32s
CI / cargo clippy (push) Successful in 24s
CI / cargo test (push) Successful in 31s
CI / editor keymaps (push) Successful in 1m40s

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:
2026-05-31 18:44:35 +00:00
parent 2d3db458fb
commit 545367c3f6
11 changed files with 252 additions and 59 deletions
+28 -12
View File
@@ -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 =====