Skip to main content

wowlab_docgen_cli/infra/
toc.rs

1use wowlab_common::markdown::{List, anchor_link};
2
3const TOC_MARKER: &str = "<!-- block:toc -->";
4
5/// Replace the TOC marker in `output` with a list built from its `## ` headings.
6#[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}