1use std::path::Path;
2use std::{cmp::Ordering, collections::BTreeMap};
3
4use crate::utils;
5use crate::utils::bracket_escape;
6
7use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
8
9// Handlebars helper to construct TOC
10#[derive(Clone, Copy)]
11pub struct RenderToc {
12 pub no_section_label: bool,
13}
14
15impl HelperDef for RenderToc {
16 fn call<'reg: 'rc, 'rc>(
17 &self,
18 _h: &Helper<'reg, 'rc>,
19 _r: &'reg Handlebars<'_>,
20 ctx: &'rc Context,
21 rc: &mut RenderContext<'reg, 'rc>,
22 out: &mut dyn Output,
23 ) -> Result<(), RenderError> {
24 // get value from context data
25 // rc.get_path() is current json parent path, you should always use it like this
26 // param is the key of value you want to display
27 let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
28 serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
29 .map_err(|_| RenderError::new("Could not decode the JSON data"))
30 })?;
31 let current_path = rc
32 .evaluate(ctx, "@root/path")?
33 .as_json()
34 .as_str()
35 .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
36 .replace('\"', "");
37
38 let current_section = rc
39 .evaluate(ctx, "@root/section")?
40 .as_json()
41 .as_str()
42 .map(str::to_owned)
43 .unwrap_or_default();
44
45 let fold_enable = rc
46 .evaluate(ctx, "@root/fold_enable")?
47 .as_json()
48 .as_bool()
49 .ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?;
50
51 let fold_level = rc
52 .evaluate(ctx, "@root/fold_level")?
53 .as_json()
54 .as_u64()
55 .ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?;
56
57 out.write("<ol class=\"chapter\">")?;
58
59 let mut current_level = 1;
60 // The "index" page, which has this attribute set, is supposed to alias the first chapter in
61 // the book, i.e. the first link. There seems to be no easy way to determine which chapter
62 // the "index" is aliasing from within the renderer, so this is used instead to force the
63 // first link to be active. See further below.
64 let mut is_first_chapter = ctx.data().get("is_index").is_some();
65
66 for item in chapters {
67 // Spacer
68 if item.get("spacer").is_some() {
69 out.write("<li class=\"spacer\"></li>")?;
70 continue;
71 }
72
73 let (section, level) = if let Some(s) = item.get("section") {
74 (s.as_str(), s.matches('.').count())
75 } else {
76 ("", 1)
77 };
78
79 let is_expanded =
80 if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
81 // Expand if folding is disabled, or if the section is an
82 // ancestor or the current section itself.
83 true
84 } else {
85 // Levels that are larger than this would be folded.
86 level - 1 < fold_level as usize
87 };
88
89 match level.cmp(&current_level) {
90 Ordering::Greater => {
91 while level > current_level {
92 out.write("<li>")?;
93 out.write("<ol class=\"section\">")?;
94 current_level += 1;
95 }
96 write_li_open_tag(out, is_expanded, false)?;
97 }
98 Ordering::Less => {
99 while level < current_level {
100 out.write("</ol>")?;
101 out.write("</li>")?;
102 current_level -= 1;
103 }
104 write_li_open_tag(out, is_expanded, false)?;
105 }
106 Ordering::Equal => {
107 write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
108 }
109 }
110
111 // Part title
112 if let Some(title) = item.get("part") {
113 out.write("<li class=\"part-title\">")?;
114 out.write(&bracket_escape(title))?;
115 out.write("</li>")?;
116 continue;
117 }
118
119 // Link
120 let path_exists: bool;
121 match item.get("path") {
122 Some(path) if !path.is_empty() => {
123 out.write("<a href=\"")?;
124 let tmp = Path::new(path)
125 .with_extension("html")
126 .to_str()
127 .unwrap()
128 // Hack for windows who tends to use `\` as separator instead of `/`
129 .replace('\\', "/");
130
131 // Add link
132 out.write(&utils::fs::path_to_root(&current_path))?;
133 out.write(&tmp)?;
134 out.write("\"")?;
135
136 if path == &current_path || is_first_chapter {
137 is_first_chapter = false;
138 out.write(" class=\"active\"")?;
139 }
140
141 out.write(">")?;
142 path_exists = true;
143 }
144 _ => {
145 out.write("<div>")?;
146 path_exists = false;
147 }
148 }
149
150 if !self.no_section_label {
151 // Section does not necessarily exist
152 if let Some(section) = item.get("section") {
153 out.write("<strong aria-hidden=\"true\">")?;
154 out.write(section)?;
155 out.write("</strong> ")?;
156 }
157 }
158
159 if let Some(name) = item.get("name") {
160 out.write(&bracket_escape(name))?
161 }
162
163 if path_exists {
164 out.write("</a>")?;
165 } else {
166 out.write("</div>")?;
167 }
168
169 // Render expand/collapse toggle
170 if let Some(flag) = item.get("has_sub_items") {
171 let has_sub_items = flag.parse::<bool>().unwrap_or_default();
172 if fold_enable && has_sub_items {
173 out.write("<a class=\"toggle\"><div>❱</div></a>")?;
174 }
175 }
176 out.write("</li>")?;
177 }
178 while current_level > 1 {
179 out.write("</ol>")?;
180 out.write("</li>")?;
181 current_level -= 1;
182 }
183
184 out.write("</ol>")?;
185 Ok(())
186 }
187}
188
189fn write_li_open_tag(
190 out: &mut dyn Output,
191 is_expanded: bool,
192 is_affix: bool,
193) -> Result<(), std::io::Error> {
194 let mut li: String = String::from("<li class=\"chapter-item ");
195 if is_expanded {
196 li.push_str(string:"expanded ");
197 }
198 if is_affix {
199 li.push_str(string:"affix ");
200 }
201 li.push_str(string:"\">");
202 out.write(&li)
203}
204