libhanzzok/core/plugin/heading/
toc.rs

1use std::{collections::VecDeque, iter::once};
2
3use serde_hzdata::HzdataValue;
4
5use crate::{
6    api::BlockConstructorRule,
7    codegen::{Context, HtmlNode},
8    core::{
9        ast::{BlockConstructorForm, InlineObjectNode},
10        plugin::heading::heading_meta::{Heading, HeadingList},
11    },
12};
13
14pub struct TocBlockConstructorRule;
15
16impl BlockConstructorRule for TocBlockConstructorRule {
17    fn name(&self) -> String {
18        "table-of-contents".to_owned()
19    }
20
21    fn form(&self) -> crate::core::ast::BlockConstructorForm {
22        BlockConstructorForm::Basic
23    }
24
25    fn apply(
26        &self,
27        _context: &mut Context,
28        _main_text: Vec<InlineObjectNode>,
29        _param: Option<HzdataValue>,
30        _: Vec<Vec<InlineObjectNode>>,
31    ) -> HtmlNode {
32        HtmlNode::create_lazy(|context| {
33            let meta: HeadingList = context.load_meta_or_default("heading", "list");
34
35            let mut stack = VecDeque::new();
36            stack.push_back((
37                Heading {
38                    name: Vec::new(),
39                    depth: 0,
40                },
41                HtmlNode::create_tag_builder("ol"),
42            ));
43
44            for heading in meta.values {
45                if heading.depth > 3 {
46                    continue;
47                }
48
49                let depth = stack.back().unwrap().0.depth;
50
51                if depth >= heading.depth {
52                    loop {
53                        let (pop_heading, mut pop) = stack.pop_back().unwrap();
54                        let (back_heading, back) = stack.back_mut().unwrap();
55
56                        back.append(HtmlNode::create_tag(
57                            "li",
58                            &pop_heading
59                                .name
60                                .into_iter()
61                                .map(|node_ref| node_ref.load_from(context).clone())
62                                .chain(once(pop.build()))
63                                .collect::<Vec<_>>(),
64                        ));
65
66                        if back_heading.depth < heading.depth {
67                            break;
68                        }
69                    }
70                }
71
72                let depth = stack.back().unwrap().0.depth;
73
74                for depth in depth..(heading.depth - 1) {
75                    stack.push_back((
76                        Heading {
77                            name: Vec::new(),
78                            depth,
79                        },
80                        HtmlNode::create_tag_builder("ol"),
81                    ));
82                }
83
84                stack.push_back((heading, HtmlNode::create_tag_builder("ol")));
85            }
86
87            loop {
88                let (pop_heading, mut pop) = stack.pop_back().unwrap();
89                let (back_heading, back) = stack.back_mut().unwrap();
90
91                back.append(HtmlNode::create_tag(
92                    "li",
93                    &pop_heading
94                        .name
95                        .into_iter()
96                        .map(|node_ref| node_ref.load_from(context).clone())
97                        .chain(once(pop.build()))
98                        .collect::<Vec<_>>(),
99                ));
100
101                if back_heading.depth <= 0 {
102                    break;
103                }
104            }
105
106            HtmlNode::create_tag_builder("div")
107                .append_all(stack.pop_front().map(|(_, mut builder)| builder.build()))
108                .set_attr("class", "table-of-contents")
109                .build()
110        })
111    }
112}