1 | use std::path::Path; |
2 | use std::{cmp::Ordering, collections::BTreeMap}; |
3 | |
4 | use crate::utils; |
5 | use crate::utils::bracket_escape; |
6 | |
7 | use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError}; |
8 | |
9 | // Handlebars helper to construct TOC |
10 | #[derive (Clone, Copy)] |
11 | pub struct RenderToc { |
12 | pub no_section_label: bool, |
13 | } |
14 | |
15 | impl 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(¤t_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(¤t_path))?; |
133 | out.write(&tmp)?; |
134 | out.write(" \"" )?; |
135 | |
136 | if path == ¤t_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 | |
189 | fn 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 | |