1use std::collections::HashSet;
6
7use crate::model::html::*;
8
9use config::*;
10
11#[derive(Debug, Clone)]
13pub struct TocMaker {
14 pub level: u8,
16 pub list_type: ListType,
19}
20
21pub mod config {
22 use crate::model::html::ElementTag;
28
29 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
31 pub enum ListType {
32 Unordered,
33 Ordered,
34 }
35
36 impl ListType {
37 pub fn to_tag(&self) -> ElementTag {
38 match self {
39 Self::Unordered => ElementTag::Ul,
40 Self::Ordered => ElementTag::Ol,
41 }
42 }
43 }
44}
45
46impl Default for TocMaker {
47 fn default() -> Self {
48 Self {
49 level: 3,
50 list_type: ListType::Unordered,
51 }
52 }
53}
54
55impl TocMaker {
56 pub fn level(mut self, level: u8) -> Self {
58 self.level = level;
59 self
60 }
61
62 pub fn list_type(mut self, list_type: ListType) -> Self {
64 self.list_type = list_type;
65 self
66 }
67}
68
69impl TocMaker {
70 pub fn make_toc<'a>(&self, input: &mut DocumentNode<'a>) -> DocumentNode<'a> {
72 let mut list = vec![];
73
74 let mut set = HashSet::new();
75
76 for node in input.root.iter_mut() {
77 let Node::Element(element) = node else { continue; };
78
79 use ElementTag::*;
80
81 let headline_level = match element.tag {
82 H1 | H2 | H3 | H4 | H5 | H6 => element.tag.get_headline_level().unwrap(),
83 _ => continue,
84 };
85
86 if headline_level > self.level {
87 continue;
88 }
89
90 let text = get_text(&element.children);
91
92 let (text, id) = if set.insert(text.clone()) {
93 (text.clone(), text)
94 } else {
95 let mut index = 1;
96
97 while !set.insert(text.clone() + &index.to_string()) {
98 index += 1;
99 }
100
101 (text.clone(), text + &index.to_string())
102 };
103
104 element.id.push(id.clone());
105
106 list.push((headline_level, text, id));
107 }
108
109 let output = self.nest(&list);
110
111 DocumentNode { root: vec![output] }
112 }
113
114 fn nest(&self, rest: &[(u8, String, String)]) -> Node<'static> {
115 let mut rest = rest;
116
117 let mut children = vec![];
118
119 while !rest.is_empty() {
120 let next = rest[1..]
121 .iter()
122 .position(|(level, _, _)| *level <= rest[0].0);
123
124 let a_tag = Node::Element(ElementNode {
125 tag: ElementTag::A,
126 href: Some(String::from("#") + &rest[0].2),
127 children: vec![Node::Text(TextNode {
128 text: rest[0].1.clone().into(),
129 })],
130 ..Default::default()
131 });
132
133 let mut element = ElementNode {
134 tag: ElementTag::Li,
135 children: vec![a_tag],
136 ..Default::default()
137 };
138
139 if let Some(index) = next {
140 let index = index + 1;
142
143 if index != 1 {
144 let output = self.nest(&rest[1..index]);
145 element.children.push(output);
146 }
147
148 children.push(Node::Element(element));
149
150 rest = &rest[index..];
151 } else {
152 if rest.len() >= 2 {
153 element.children.push(self.nest(&rest[1..]));
154 }
155
156 children.push(Node::Element(element));
157
158 rest = &[];
159 }
160 }
161
162 Node::Element(ElementNode {
163 tag: self.list_type.to_tag(),
164 children,
165 ..Default::default()
166 })
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::{layer::lexer::lex, Markdown};
174
175 #[test]
176 fn test_make_toc() {
177 let input =
178 "# H1AAAAAA\n\n# H1AAAAAA\n\n# H1BBBBBB\n\n## H2AAAAAA\n\n## H2BBBBBB\n\n# H1CCCCCC\n\n";
179
180 let markdown = Markdown::default();
181
182 let tokens = lex(input);
183 let tree = markdown.parser.parse(input, tokens);
184 let mut document = markdown.transformer.transform(tree);
185
186 let toc = TocMaker::default().make_toc(&mut document);
187
188 let output1 = markdown.stringifier.stringify(document);
189
190 assert_eq!(output1, "<h1 id=\"H1AAAAAA\">H1AAAAAA</h1><h1 id=\"H1AAAAAA1\">H1AAAAAA</h1><h1 id=\"H1BBBBBB\">H1BBBBBB</h1><h2 id=\"H2AAAAAA\">H2AAAAAA</h2><h2 id=\"H2BBBBBB\">H2BBBBBB</h2><h1 id=\"H1CCCCCC\">H1CCCCCC</h1>");
191
192 let output2 = markdown.stringifier.stringify(toc);
193
194 assert_eq!(output2, "<ul><li><a href=\"#H1AAAAAA\">H1AAAAAA</a></li><li><a href=\"#H1AAAAAA1\">H1AAAAAA</a></li><li><a href=\"#H1BBBBBB\">H1BBBBBB</a><ul><li><a href=\"#H2AAAAAA\">H2AAAAAA</a></li><li><a href=\"#H2BBBBBB\">H2BBBBBB</a></li></ul></li><li><a href=\"#H1CCCCCC\">H1CCCCCC</a></li></ul>")
195 }
196}