markdown_it_table_of_contents/
lib.rs1use markdown_it::{
2 MarkdownIt, Node, NodeValue, Renderer,
3 plugins::cmark::block::{
4 heading::ATXHeading,
5 lheading::SetextHeader,
6 },
7 parser::{
8 core::CoreRule,
9 extset::MarkdownItExt
10 }
11};
12use std::vec::Vec;
13use unbox_box::BoxExt;
14
15#[derive(Debug)]
16pub struct TableOfContentsItem {
17 pub slug: String,
18 pub title: String,
19 pub level: u8,
20 pub children: Box<Vec<TableOfContentsItem>>
21}
22
23#[derive(Debug)]
24pub struct TOCOptions {
25 pub allow_titles_in_toc: bool, pub treat_title_as_h2: bool, pub toc_class: String,
28 pub wrap_in_nav: bool, pub toc_heading: Option<( u8, String )> }
33
34impl MarkdownItExt for TOCOptions {}
35impl Default for TOCOptions {
36 fn default() -> Self {
37 TOCOptions {
38 allow_titles_in_toc: false,
39 treat_title_as_h2: false,
40 toc_class: "table_of_contents".to_string(),
41 wrap_in_nav: true,
42 toc_heading: Some((2, "Contents".to_string()))
43 }
44 }
45}
46
47impl TableOfContentsItem {
48 fn push(&mut self, item: TableOfContentsItem, depth_limit: u8) {
49 let child_count = self.children.unbox_ref().len();
50 if depth_limit > self.level+1 && child_count != 0 {
51 self.children.unbox_mut()[child_count - 1].push(item, depth_limit-1);
52 return;
53 }
54 self.children.unbox_mut().push(item);
55 }
56}
57
58#[derive(Debug)]
59pub struct TOC {
60 contents: Vec<TableOfContentsItem>,
61 min_level: u8,
62 wrap_in_nav: bool,
63 toc_heading: Option<( u8, String )>
64}
65
66impl NodeValue for TOC {
67 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
68
69 fn render_item(item: &TableOfContentsItem, fmt: &mut dyn Renderer) {
70 fmt.cr();
71 fmt.open("li", &[]);
72
73 let mut link_href = String::from("#");
74 link_href.push_str(&item.slug);
75 fmt.cr();
76 fmt.open("a", &[("href", link_href)]);
77 fmt.text(&item.title);
78 fmt.close("a");
79 if item.children.len() != 0 {
80 fmt.cr();
81 fmt.open("ol", &[]);
82 item.children.unbox_ref().iter().for_each(|child| {
83 render_item(&(child), fmt);
84 });
85 fmt.cr();
86 fmt.close("ol");
87 }
88 fmt.cr();
89 fmt.close("li");
90 }
91 let attrs = node.attrs.clone();
92
93 fmt.cr();
94 if self.wrap_in_nav {
95 fmt.open("nav", &attrs);
96 fmt.cr();
97 }
98 if let Some(heading) = &self.toc_heading {
99 let heading_tag = String::from("h") + &heading.0.to_string();
100 fmt.open(&heading_tag, &[]);
101 fmt.text(&heading.1);
102 fmt.close(&heading_tag);
103 fmt.cr();
104 }
105 fmt.open(
106 "ol",
107 if !self.wrap_in_nav { &attrs } else { &[] }
108 );
109 self.contents.iter().for_each(|item| {
110 render_item(item, fmt);
111 });
112 fmt.cr();
113 fmt.close("ol");
114 if self.wrap_in_nav {
115 fmt.cr();
116 fmt.close("nav");
117 }
118 fmt.cr();
119 }
120}
121
122impl TOC {
123 fn push(&mut self, item: TableOfContentsItem, depth_limit: u8) {
124 println!("{}", depth_limit);
125 let child_count = self.contents.len();
126 if depth_limit > self.min_level && child_count != 0 {
127 self.contents[child_count - 1].push(item, depth_limit - 1);
128 return;
129 }
130 println!("---");
131 self.min_level = item.level;
132 self.contents.push(item);
133 }
134}
135
136
137fn sluggify(name: &str) -> String {
138 name.to_string().replace(|c| !char::is_alphanumeric(c) && c != ' ', "").replace(" ", "-").to_lowercase()
139}
140
141struct TableOfContentsDetect;
142
143impl CoreRule for TableOfContentsDetect {
144 fn run(root: &mut Node, md: &MarkdownIt) {
145 struct Heading {
146 level: u8,
147 title: String,
148 slug: String
149 }
150
151 let mut disorganized_headings: Vec<Heading> = Vec::new();
152 let mut index = 0;
153 let mut head_count = 0;
154 let mut first_heading: Option<u16> = None;
155 root.walk_post_mut(|node, _| {
156 fn get_level(node: &Node) -> Option<u8> {
157 match node.cast::<ATXHeading>() {
158 Some(item) => return Some(item.level),
159 None => ()
160 };
161 match node.cast::<SetextHeader>() {
162 Some(item) => return Some(item.level),
163 None => ()
164 };
165 return None
166 }
167
168 index += 1;
169
170 let level = match get_level(node) {
171 None => return (),
172 Some(l) => l
173 };
174
175 if first_heading == None {
176 first_heading = Some((index - 1) >> 1);
177 }
179 head_count = head_count + 1;
180
181 let title = node.collect_text();
182 let slug = match node.attrs.as_slice() {
183 [("id", id)] => String::from(id),
184 _ => {
186 let slug = sluggify(&title);
187 node.attrs.push(("id", slug.clone()));
188 slug
189 }
190 };
191
192 let header_tag = Heading {
193 title: title,
194 slug: String::from(slug),
195 level: level
196 };
197 disorganized_headings.push(header_tag);
198 });
199
200 let default_opts = TOCOptions::default();
201 let opts = md.ext.get::<TOCOptions>().unwrap_or(&default_opts);
202
203 let mut organized_headings = TOC {
204 contents: Vec::new(),
205 min_level: 6,
206 wrap_in_nav: opts.wrap_in_nav,
207 toc_heading: opts.toc_heading.clone()
208 };
209 for heading in disorganized_headings {
210 let head = TableOfContentsItem {
211 title: heading.title,
212 slug: heading.slug,
213 level: heading.level,
214 children: Box::new(Vec::new())
215 };
216 if heading.level == 1 && !opts.allow_titles_in_toc { continue; }
217 organized_headings.push(
218 head,
219 if heading.level == 1 && opts.treat_title_as_h2 { 2 } else { heading.level }
220 );
221 }
222
223 if head_count < 2 {
224 return ();
225 }
226
227 let mut table_of_contents = Node::new(organized_headings);
228 table_of_contents.attrs.push(("class", opts.toc_class.clone()));
229 match first_heading {
230 Some(i) => root.children.insert(i.into(), table_of_contents),
231 None => ()
232 };
233 }
234}
235
236pub fn add(md: &mut MarkdownIt) {
237 md.add_rule::<TableOfContentsDetect>();
239}