]>
Commit | Line | Data |
---|---|---|
ea8adc8c | 1 | use std::collections::BTreeMap; |
9fa01778 | 2 | use std::path::Path; |
cc61c64b | 3 | |
dc9dc135 | 4 | use crate::utils; |
04454e1e | 5 | use crate::utils::bracket_escape; |
9fa01778 XL |
6 | |
7 | use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError}; | |
cc61c64b XL |
8 | |
9 | // Handlebars helper to construct TOC | |
10 | #[derive(Clone, Copy)] | |
2c00a5a8 | 11 | pub struct RenderToc { |
83c7162d | 12 | pub no_section_label: bool, |
2c00a5a8 | 13 | } |
cc61c64b XL |
14 | |
15 | impl HelperDef for RenderToc { | |
9fa01778 XL |
16 | fn call<'reg: 'rc, 'rc>( |
17 | &self, | |
dc9dc135 | 18 | _h: &Helper<'reg, 'rc>, |
f9f354fc | 19 | _r: &'reg Handlebars<'_>, |
dc9dc135 | 20 | ctx: &'rc Context, |
f9f354fc | 21 | rc: &mut RenderContext<'reg, 'rc>, |
dc9dc135 | 22 | out: &mut dyn Output, |
9fa01778 | 23 | ) -> Result<(), RenderError> { |
cc61c64b XL |
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 | |
416331ca XL |
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()) | |
2c00a5a8 XL |
29 | .map_err(|_| RenderError::new("Could not decode the JSON data")) |
30 | })?; | |
e74abb32 | 31 | let current_path = rc |
416331ca XL |
32 | .evaluate(ctx, "@root/path")? |
33 | .as_json() | |
83c7162d | 34 | .as_str() |
f035d41b | 35 | .ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? |
83c7162d | 36 | .replace("\"", ""); |
cc61c64b | 37 | |
e74abb32 XL |
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() | |
f035d41b | 49 | .ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?; |
e74abb32 XL |
50 | |
51 | let fold_level = rc | |
52 | .evaluate(ctx, "@root/fold_level")? | |
53 | .as_json() | |
54 | .as_u64() | |
f035d41b | 55 | .ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?; |
e74abb32 | 56 | |
9fa01778 | 57 | out.write("<ol class=\"chapter\">")?; |
cc61c64b XL |
58 | |
59 | let mut current_level = 1; | |
60 | ||
ea8adc8c | 61 | for item in chapters { |
cc61c64b XL |
62 | // Spacer |
63 | if item.get("spacer").is_some() { | |
9fa01778 | 64 | out.write("<li class=\"spacer\"></li>")?; |
cc61c64b XL |
65 | continue; |
66 | } | |
67 | ||
e74abb32 XL |
68 | let (section, level) = if let Some(s) = item.get("section") { |
69 | (s.as_str(), s.matches('.').count()) | |
cc61c64b | 70 | } else { |
e74abb32 XL |
71 | ("", 1) |
72 | }; | |
73 | ||
f035d41b XL |
74 | let is_expanded = |
75 | if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) { | |
76 | // Expand if folding is disabled, or if the section is an | |
77 | // ancestor or the current section itself. | |
e74abb32 XL |
78 | true |
79 | } else { | |
80 | // Levels that are larger than this would be folded. | |
81 | level - 1 < fold_level as usize | |
f035d41b | 82 | }; |
cc61c64b XL |
83 | |
84 | if level > current_level { | |
85 | while level > current_level { | |
9fa01778 XL |
86 | out.write("<li>")?; |
87 | out.write("<ol class=\"section\">")?; | |
cc61c64b XL |
88 | current_level += 1; |
89 | } | |
e74abb32 | 90 | write_li_open_tag(out, is_expanded, false)?; |
cc61c64b XL |
91 | } else if level < current_level { |
92 | while level < current_level { | |
9fa01778 XL |
93 | out.write("</ol>")?; |
94 | out.write("</li>")?; | |
cc61c64b XL |
95 | current_level -= 1; |
96 | } | |
e74abb32 | 97 | write_li_open_tag(out, is_expanded, false)?; |
cc61c64b | 98 | } else { |
e74abb32 | 99 | write_li_open_tag(out, is_expanded, item.get("section").is_none())?; |
cc61c64b | 100 | } |
f035d41b XL |
101 | |
102 | // Part title | |
103 | if let Some(title) = item.get("part") { | |
3dfed10e | 104 | out.write("<li class=\"part-title\">")?; |
04454e1e | 105 | out.write(&bracket_escape(title))?; |
f035d41b XL |
106 | out.write("</li>")?; |
107 | continue; | |
108 | } | |
cc61c64b XL |
109 | |
110 | // Link | |
1b1a35ee XL |
111 | let path_exists = if let Some(path) = |
112 | item.get("path") | |
113 | .and_then(|p| if p.is_empty() { None } else { Some(p) }) | |
114 | { | |
115 | out.write("<a href=\"")?; | |
116 | ||
117 | let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)")) | |
118 | .with_extension("html") | |
119 | .to_str() | |
120 | .unwrap() | |
121 | // Hack for windows who tends to use `\` as separator instead of `/` | |
122 | .replace("\\", "/"); | |
123 | ||
124 | // Add link | |
125 | out.write(&utils::fs::path_to_root(¤t_path))?; | |
126 | out.write(&tmp)?; | |
127 | out.write("\"")?; | |
128 | ||
129 | if path == ¤t_path { | |
130 | out.write(" class=\"active\"")?; | |
cc61c64b | 131 | } |
1b1a35ee XL |
132 | |
133 | out.write(">")?; | |
134 | true | |
cc61c64b | 135 | } else { |
1b1a35ee | 136 | out.write("<div>")?; |
cc61c64b XL |
137 | false |
138 | }; | |
139 | ||
2c00a5a8 XL |
140 | if !self.no_section_label { |
141 | // Section does not necessarily exist | |
142 | if let Some(section) = item.get("section") { | |
9fa01778 | 143 | out.write("<strong aria-hidden=\"true\">")?; |
a2a8927a | 144 | out.write(section)?; |
9fa01778 | 145 | out.write("</strong> ")?; |
2c00a5a8 | 146 | } |
cc61c64b XL |
147 | } |
148 | ||
149 | if let Some(name) = item.get("name") { | |
04454e1e | 150 | out.write(&bracket_escape(name))? |
cc61c64b XL |
151 | } |
152 | ||
153 | if path_exists { | |
9fa01778 | 154 | out.write("</a>")?; |
1b1a35ee XL |
155 | } else { |
156 | out.write("</div>")?; | |
cc61c64b XL |
157 | } |
158 | ||
e74abb32 XL |
159 | // Render expand/collapse toggle |
160 | if let Some(flag) = item.get("has_sub_items") { | |
161 | let has_sub_items = flag.parse::<bool>().unwrap_or_default(); | |
162 | if fold_enable && has_sub_items { | |
163 | out.write("<a class=\"toggle\"><div>❱</div></a>")?; | |
164 | } | |
165 | } | |
9fa01778 | 166 | out.write("</li>")?; |
cc61c64b XL |
167 | } |
168 | while current_level > 1 { | |
9fa01778 XL |
169 | out.write("</ol>")?; |
170 | out.write("</li>")?; | |
cc61c64b XL |
171 | current_level -= 1; |
172 | } | |
173 | ||
9fa01778 | 174 | out.write("</ol>")?; |
cc61c64b XL |
175 | Ok(()) |
176 | } | |
177 | } | |
e74abb32 XL |
178 | |
179 | fn write_li_open_tag( | |
180 | out: &mut dyn Output, | |
181 | is_expanded: bool, | |
182 | is_affix: bool, | |
183 | ) -> Result<(), std::io::Error> { | |
f9f354fc | 184 | let mut li = String::from("<li class=\"chapter-item "); |
e74abb32 XL |
185 | if is_expanded { |
186 | li.push_str("expanded "); | |
187 | } | |
188 | if is_affix { | |
189 | li.push_str("affix "); | |
190 | } | |
191 | li.push_str("\">"); | |
192 | out.write(&li) | |
193 | } |