base_markdown/
headings.rs1use crate::{stream::EventKind, NodeId};
5use alloc::{collections::BTreeMap, string::String};
6
7pub(crate) fn semantic_text<'a, const HEADING: bool>(
9 kind: &'a EventKind<'_>,
10 source: &'a str,
11) -> Option<&'a str> {
12 let value = match kind {
13 EventKind::Text(value)
14 | EventKind::SoftBreak(value)
15 | EventKind::Math(value)
16 | EventKind::Code { value, .. }
17 | EventKind::Html { value, .. } => value,
18 EventKind::CodeBlock { value, .. } if !HEADING => value,
19 EventKind::HardBreak => return Some("\n"),
20 _ => return None,
21 };
22 Some(
23 value
24 .resolve(source)
25 .expect("semantic text values are validated"),
26 )
27}
28
29pub(crate) fn append_text<const HEADING: bool>(
30 kind: &EventKind<'_>,
31 source: &str,
32 text: &mut String,
33) {
34 if let Some(value) = semantic_text::<HEADING>(kind, source) {
35 text.push_str(value);
36 }
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct HeadingOptions {
42 pub min_level: u8,
44 pub max_level: u8,
46}
47impl Default for HeadingOptions {
48 fn default() -> Self {
49 Self {
50 min_level: 1,
51 max_level: 6,
52 }
53 }
54}
55#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct HeadingEntry {
58 pub node: NodeId,
60 pub level: u8,
62 pub text: String,
64 pub anchor: String,
66}
67
68#[derive(Default)]
69pub(crate) struct AnchorGenerator {
70 next_suffix: BTreeMap<String, usize>,
71}
72
73impl AnchorGenerator {
74 pub(crate) fn anchor_for(&mut self, text: &str) -> String {
75 let base = slug(text);
76 let Some(mut suffix) = self.next_suffix.get(&base).copied() else {
77 self.next_suffix.insert(base.clone(), 2);
78 return base;
79 };
80
81 let anchor = loop {
82 let candidate = alloc::format!("{base}-{suffix}");
83 suffix += 1;
84 if !self.next_suffix.contains_key(&candidate) {
85 break candidate;
86 }
87 };
88
89 self.next_suffix.insert(base, suffix);
90 self.next_suffix.insert(anchor.clone(), 2);
91 anchor
92 }
93}
94
95fn slug(text: &str) -> String {
96 let mut result = String::new();
97 let mut previous_was_whitespace = false;
98 let lowercase = text.to_lowercase();
99
100 for character in lowercase.chars() {
101 if character.is_whitespace() {
102 if !previous_was_whitespace {
103 result.push('-');
104 }
105 previous_was_whitespace = true;
106 } else {
107 result.push(character);
108 previous_was_whitespace = false;
109 }
110 }
111
112 result
113}