note_mark/layer/
toc.rs

1//! Table of contents.
2//!
3//! This module contains some configuration options for the table of contents.
4
5use std::collections::HashSet;
6
7use crate::model::html::*;
8
9use config::*;
10
11/// The struct to make a table of contents.
12#[derive(Debug, Clone)]
13pub struct TocMaker {
14    /// The maximum level of the table of contents. Default is 3.
15    pub level: u8,
16    /// The type of the list. Default is
17    /// [`ListType::Unordered`](config::ListType).
18    pub list_type: ListType,
19}
20
21pub mod config {
22    //! Configuration options for the table of contents.
23    //!
24    //! This module contains some configuration options for the table of
25    //! contents.
26
27    use crate::model::html::ElementTag;
28
29    /// The type of the list.
30    #[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    /// Set the maximum level of the table of contents.
57    pub fn level(mut self, level: u8) -> Self {
58        self.level = level;
59        self
60    }
61
62    /// Set the type of the list.
63    pub fn list_type(mut self, list_type: ListType) -> Self {
64        self.list_type = list_type;
65        self
66    }
67}
68
69impl TocMaker {
70    /// Make a table of contents.
71    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                // `index` is the index of the next element with the same or higher level
141                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}