]>
Commit | Line | Data |
---|---|---|
9fa01778 | 1 | use std::path::Path; |
064997fb | 2 | use std::{cmp::Ordering, collections::BTreeMap}; |
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"))? |
064997fb | 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; | |
064997fb FG |
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(); | |
cc61c64b | 65 | |
ea8adc8c | 66 | for item in chapters { |
cc61c64b XL |
67 | // Spacer |
68 | if item.get("spacer").is_some() { | |
9fa01778 | 69 | out.write("<li class=\"spacer\"></li>")?; |
cc61c64b XL |
70 | continue; |
71 | } | |
72 | ||
e74abb32 XL |
73 | let (section, level) = if let Some(s) = item.get("section") { |
74 | (s.as_str(), s.matches('.').count()) | |
cc61c64b | 75 | } else { |
e74abb32 XL |
76 | ("", 1) |
77 | }; | |
78 | ||
f035d41b XL |
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. | |
e74abb32 XL |
83 | true |
84 | } else { | |
85 | // Levels that are larger than this would be folded. | |
86 | level - 1 < fold_level as usize | |
f035d41b | 87 | }; |
cc61c64b | 88 | |
064997fb FG |
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)?; | |
cc61c64b | 97 | } |
064997fb FG |
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())?; | |
cc61c64b | 108 | } |
cc61c64b | 109 | } |
f035d41b XL |
110 | |
111 | // Part title | |
112 | if let Some(title) = item.get("part") { | |
3dfed10e | 113 | out.write("<li class=\"part-title\">")?; |
04454e1e | 114 | out.write(&bracket_escape(title))?; |
f035d41b XL |
115 | out.write("</li>")?; |
116 | continue; | |
117 | } | |
cc61c64b XL |
118 | |
119 | // Link | |
1b1a35ee XL |
120 | let path_exists = if let Some(path) = |
121 | item.get("path") | |
122 | .and_then(|p| if p.is_empty() { None } else { Some(p) }) | |
123 | { | |
124 | out.write("<a href=\"")?; | |
125 | ||
126 | let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)")) | |
127 | .with_extension("html") | |
128 | .to_str() | |
129 | .unwrap() | |
130 | // Hack for windows who tends to use `\` as separator instead of `/` | |
064997fb | 131 | .replace('\\', "/"); |
1b1a35ee XL |
132 | |
133 | // Add link | |
134 | out.write(&utils::fs::path_to_root(¤t_path))?; | |
135 | out.write(&tmp)?; | |
136 | out.write("\"")?; | |
137 | ||
064997fb FG |
138 | if path == ¤t_path || is_first_chapter { |
139 | is_first_chapter = false; | |
1b1a35ee | 140 | out.write(" class=\"active\"")?; |
cc61c64b | 141 | } |
1b1a35ee XL |
142 | |
143 | out.write(">")?; | |
144 | true | |
cc61c64b | 145 | } else { |
1b1a35ee | 146 | out.write("<div>")?; |
cc61c64b XL |
147 | false |
148 | }; | |
149 | ||
2c00a5a8 XL |
150 | if !self.no_section_label { |
151 | // Section does not necessarily exist | |
152 | if let Some(section) = item.get("section") { | |
9fa01778 | 153 | out.write("<strong aria-hidden=\"true\">")?; |
a2a8927a | 154 | out.write(section)?; |
9fa01778 | 155 | out.write("</strong> ")?; |
2c00a5a8 | 156 | } |
cc61c64b XL |
157 | } |
158 | ||
159 | if let Some(name) = item.get("name") { | |
04454e1e | 160 | out.write(&bracket_escape(name))? |
cc61c64b XL |
161 | } |
162 | ||
163 | if path_exists { | |
9fa01778 | 164 | out.write("</a>")?; |
1b1a35ee XL |
165 | } else { |
166 | out.write("</div>")?; | |
cc61c64b XL |
167 | } |
168 | ||
e74abb32 XL |
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 | } | |
9fa01778 | 176 | out.write("</li>")?; |
cc61c64b XL |
177 | } |
178 | while current_level > 1 { | |
9fa01778 XL |
179 | out.write("</ol>")?; |
180 | out.write("</li>")?; | |
cc61c64b XL |
181 | current_level -= 1; |
182 | } | |
183 | ||
9fa01778 | 184 | out.write("</ol>")?; |
cc61c64b XL |
185 | Ok(()) |
186 | } | |
187 | } | |
e74abb32 XL |
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> { | |
f9f354fc | 194 | let mut li = String::from("<li class=\"chapter-item "); |
e74abb32 XL |
195 | if is_expanded { |
196 | li.push_str("expanded "); | |
197 | } | |
198 | if is_affix { | |
199 | li.push_str("affix "); | |
200 | } | |
201 | li.push_str("\">"); | |
202 | out.write(&li) | |
203 | } |