summaryrefslogtreecommitdiff
path: root/src/directive/toc.rs
blob: c4ffc9e6e2c27d289dac0f0ae2b5b70ca3b4553b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use crate::directive::{DirectiveError, DirectiveImplementation, Processed};
use crate::page::PageMeta;
use crate::site::Site;
use crate::wikitext::ParsedDirective;
use html_page::{Element, Tag, Visitor};
use std::cmp::Ordering;

#[derive(Debug, Default)]
struct Headings {
    headings: Vec<(usize, Element)>,
}

impl Visitor for Headings {
    fn visit_text(&mut self, _: &str) {}
    fn visit_element(&mut self, e: &Element) {
        match e.tag() {
            Tag::H1 => self.headings.push((1, e.clone())),
            Tag::H2 => self.headings.push((2, e.clone())),
            Tag::H3 => self.headings.push((3, e.clone())),
            Tag::H4 => self.headings.push((4, e.clone())),
            Tag::H5 => self.headings.push((5, e.clone())),
            Tag::H6 => self.headings.push((6, e.clone())),
            _ => (),
        }
    }
}

#[derive(Debug, Default)]
struct Text {
    text: String,
}

impl Visitor for Text {
    fn visit_text(&mut self, s: &str) {
        self.text.push_str(s);
    }
    fn visit_element(&mut self, _: &Element) {}
}

#[derive(Debug, Default, Eq, PartialEq)]
pub struct Toc {
    levels: String,
}

impl DirectiveImplementation for Toc {
    const REQUIRED: &'static [&'static str] = &[];
    const ALLOWED: &'static [&'static str] = &["levels"];
    const ALLOW_ANY_UNNAMED: bool = true;

    fn from_parsed(p: &ParsedDirective) -> Self {
        let args = p.args();
        let levels = args.get("levels").unwrap_or(&"9");
        Self::new(levels.to_string())
    }

    fn process(&self, _site: &Site, _meta: &mut PageMeta) -> Result<Processed, DirectiveError> {
        let levels: usize = self
            .levels
            .parse()
            .map_err(|e| DirectiveError::LevelsParse(self.levels.clone(), e))?;
        Ok(Processed::Toc(levels))
    }
}

impl Toc {
    fn new(levels: String) -> Self {
        Self { levels }
    }

    pub fn post_process(html: &Element, levels: usize) -> String {
        let mut headings = Headings::default();
        headings.visit(html);

        let mut stack = vec![];
        let mut prev_level: usize = 0;
        for (level, heading) in headings
            .headings
            .iter()
            .filter(|(level, _)| *level < levels)
        {
            match level.cmp(&prev_level) {
                Ordering::Greater => {
                    let list = Element::new(Tag::Ul);
                    stack.push(list);
                }
                Ordering::Less => {
                    assert!(!stack.is_empty());
                    let ending = stack.pop().unwrap();

                    assert!(!stack.is_empty());
                    let parent = stack.last_mut().unwrap();
                    parent.push_child(&ending);
                }
                Ordering::Equal => (),
            }
            if let Some(ol) = stack.last_mut() {
                let mut text = Text::default();
                text.visit(heading);
                let li = Element::new(Tag::Li).with_text(&text.text);
                ol.push_child(&li);
            }
            prev_level = *level;
        }

        while stack.len() > 1 {
            assert!(!stack.is_empty());
            let ending = stack.pop().unwrap();

            assert!(!stack.is_empty());
            let parent = stack.last_mut().unwrap();
            parent.push_child(&ending);
        }

        let mut toc =
            Element::new(Tag::Div).with_child(Element::new(Tag::H1).with_text("Contents"));
        if let Some(e) = stack.get(0) {
            toc.push_child(e);
        }
        format!("{}", toc)
    }
}