wowlab_docgen_cli/infra/
toc.rs1use wowlab_common::markdown::{List, anchor_link};
2
3const TOC_MARKER: &str = "<!-- block:toc -->";
4
5#[must_use]
7pub fn inject(output: &str) -> String {
8 if !output.contains(TOC_MARKER) {
9 return output.to_string();
10 }
11
12 let toc = build(output);
13
14 output.replace(TOC_MARKER, &toc)
15}
16
17fn build(rendered: &str) -> String {
18 let items = rendered.lines().filter_map(|line| {
19 let heading = line.trim().strip_prefix("## ")?;
20
21 Some(anchor_link(heading, to_anchor(heading)))
22 });
23
24 List::new().items(items).build()
25}
26
27fn to_anchor(heading: &str) -> String {
28 heading
29 .to_lowercase()
30 .chars()
31 .filter_map(|c| match c {
32 ' ' | '-' => Some('-'),
33 ch if ch.is_alphanumeric() => Some(ch),
34 _ => None,
35 })
36 .collect()
37}
38
39#[cfg(test)]
40mod tests {
41 use googletest::prelude::*;
42
43 use super::*;
44
45 #[gtest]
46 fn anchor_lowercases_and_replaces_spaces() -> Result<()> {
47 verify_eq!(to_anchor("HTTP API"), "http-api")?;
48 verify_eq!(to_anchor("How scheduling works"), "how-scheduling-works")?;
49
50 verify_eq!(to_anchor("Cargo features"), "cargo-features")
51 }
52
53 #[gtest]
54 fn anchor_strips_punctuation() -> Result<()> {
55 verify_eq!(to_anchor("What's new?"), "whats-new")?;
56
57 verify_eq!(to_anchor("C/C++ support"), "cc-support")
58 }
59
60 #[gtest]
61 fn build_extracts_h2_only() {
62 let input = "# Title\n## Section A\n### Sub\n## Section B\n";
63 let rendered = build(input);
64
65 insta::assert_snapshot!(rendered);
66 }
67
68 #[gtest]
69 fn inject_replaces_marker() {
70 let input = "<!-- block:toc -->\n\n## Usage\n\n## Tests\n";
71 let rendered = inject(input);
72
73 insta::assert_snapshot!(rendered);
74 }
75
76 #[gtest]
77 fn inject_noop_without_marker() -> Result<()> {
78 let input = "## Usage\n## Tests\n";
79
80 verify_eq!(inject(input), input)
81 }
82
83 #[gtest]
84 fn build_empty_when_no_headings() -> Result<()> {
85 verify_eq!(build("# Title\nSome text.\n"), "")
86 }
87}