1use crate::common;
5
6#[derive(Debug, Clone)]
17pub struct TocElement {
18 pub level: i32,
20 pub url: String,
22 pub title: String,
24 pub raw_title: Option<String>,
26 pub children: Vec<TocElement>,
28}
29
30impl TocElement {
31 pub fn new<S1: Into<String>, S2: Into<String>>(url: S1, title: S2) -> TocElement {
35 TocElement {
36 level: 1,
37 url: url.into(),
38 title: title.into(),
39 raw_title: None,
40 children: vec![],
41 }
42 }
43
44 pub fn raw_title<S: Into<String>>(mut self, title: S) -> TocElement {
48 self.raw_title = Option::Some(title.into());
49 self
50 }
51
52 pub fn level(mut self, level: i32) -> Self {
54 self.level = level;
55 self
56 }
57
58 fn level_up(&mut self, level: i32) {
60 self.level = level;
61 for child in &mut self.children {
62 if child.level <= self.level {
63 child.level_up(level + 1);
64 }
65 }
66 }
67
68 pub fn child(mut self, mut child: TocElement) -> Self {
85 if child.level <= self.level {
86 child.level_up(self.level + 1);
87 }
88 self.children.push(child);
89 self
90 }
91
92 pub fn add(&mut self, element: TocElement) {
99 let mut inserted = false;
100 if let Some(ref mut last_elem) = self.children.last_mut() {
101 if element.level > last_elem.level {
102 last_elem.add(element.clone());
103 inserted = true;
104 }
105 }
106 if !inserted {
107 self.children.push(element);
108 }
109 }
110
111 #[doc(hidden)]
113 pub fn render_epub(&self, mut offset: u32, escape_html: bool) -> (u32, String) {
114 offset += 1;
115 let id = offset;
116 let children = if self.children.is_empty() {
117 String::new()
118 } else {
119 let mut output: Vec<String> = Vec::new();
120 for child in &self.children {
121 let (n, s) = child.render_epub(offset, escape_html);
122 offset = n;
123 output.push(s);
124 }
125 format!("\n{}", common::indent(output.join("\n"), 1))
126 };
127 let mut title = html_escape::encode_text(&self.title);
129 if let Some(ref raw_title) = &self.raw_title {
130 title = std::borrow::Cow::Borrowed(raw_title);
131 }
132 (
133 offset,
134 format!(
135 "\
136<navPoint playOrder=\"{id}\" id=\"navPoint-{id}\">
137 <navLabel>
138 <text>{title}</text>
139 </navLabel>
140 <content src=\"{url}\"/>{children}
141</navPoint>",
142 id = html_escape::encode_double_quoted_attribute(&id.to_string()),
143 title = title.trim(),
144 url = html_escape::encode_double_quoted_attribute(&self.url),
145 children = children, ),
147 )
148 }
149
150 #[doc(hidden)]
152 pub fn render(&self, numbered: bool, escape_html: bool) -> String {
153 if self.title.is_empty() {
154 return String::new();
155 }
156 if self.children.is_empty() {
157 format!(
158 "<li><a href=\"{link}\">{title}</a></li>",
159 link = html_escape::encode_double_quoted_attribute(&self.url),
160 title = common::encode_html(&self.title, escape_html),
161 )
162 } else {
163 let mut output: Vec<String> = Vec::new();
164 for child in &self.children {
165 output.push(child.render(numbered, escape_html));
166 }
167 let children = format!(
168 "<{oul}>\n{children}\n</{oul}>",
169 oul = if numbered { "ol" } else { "ul" }, children = common::indent(output.join("\n"), 1), );
172 format!(
173 "\
174<li>
175 <a href=\"{link}\">{title}</a>
176{children}
177</li>",
178 link = html_escape::encode_double_quoted_attribute(&self.url),
179 title = common::encode_html(&self.title, escape_html),
180 children = common::indent(children, 1), )
182 }
183 }
184}
185
186#[derive(Debug, Default)]
210pub struct Toc {
211 pub elements: Vec<TocElement>,
213}
214
215impl Toc {
216 pub fn new() -> Toc {
218 Toc { elements: vec![] }
219 }
220
221 pub fn is_empty(&self) -> bool {
226 self.elements.len() <= 1
227 }
228
229 pub fn add(&mut self, element: TocElement) -> &mut Self {
251 let mut inserted = false;
252 if let Some(ref mut last_elem) = self.elements.last_mut() {
253 if element.level > last_elem.level {
254 last_elem.add(element.clone());
255 inserted = true;
256 }
257 }
258 if !inserted {
259 self.elements.push(element);
260 }
261
262 self
263 }
264
265 pub fn render_epub(&mut self, escape_html: bool) -> String {
269 let mut output: Vec<String> = Vec::new();
270 let mut offset = 0;
271 for elem in &self.elements {
272 let (n, s) = elem.render_epub(offset, escape_html);
273 offset = n;
274 output.push(s);
275 }
276 common::indent(output.join("\n"), 2)
277 }
278
279 pub fn render(&mut self, numbered: bool, escape_html: bool) -> String {
281 let mut output: Vec<String> = Vec::new();
282 for elem in &self.elements {
283 log::debug!("rendered elem: {:?}", &elem.render(numbered, escape_html));
284 output.push(elem.render(numbered, escape_html));
285 }
286 common::indent(
287 format!(
288 "<{oul}>\n{output}\n</{oul}>",
289 output = common::indent(output.join("\n"), 1), oul = if numbered { "ol" } else { "ul" } ),
292 2,
293 )
294 }
295}
296
297#[test]
302fn toc_simple() {
303 let mut toc = Toc::new();
304 toc.add(TocElement::new("#1", "0.0.1").level(3));
305 toc.add(TocElement::new("#2", "1").level(1));
306 toc.add(TocElement::new("#3", "1.0.1").level(3));
307 toc.add(TocElement::new("#4", "1.1").level(2));
308 toc.add(TocElement::new("#5", "2"));
309 let actual = toc.render(false, true);
310 let expected = " <ul>
311 <li><a href=\"#1\">0.0.1</a></li>
312 <li>
313 <a href=\"#2\">1</a>
314 <ul>
315 <li><a href=\"#3\">1.0.1</a></li>
316 <li><a href=\"#4\">1.1</a></li>
317 </ul>
318 </li>
319 <li><a href=\"#5\">2</a></li>
320 </ul>";
321 assert_eq!(&actual, expected);
322}
323
324#[test]
325fn toc_epub_simple() {
326 let mut toc = Toc::new();
327 toc.add(TocElement::new("#1", "1"));
328 toc.add(TocElement::new("#2", "2"));
329 let actual = toc.render_epub(true);
330 let expected = " <navPoint playOrder=\"1\" id=\"navPoint-1\">
331 <navLabel>
332 <text>1</text>
333 </navLabel>
334 <content src=\"#1\"/>
335 </navPoint>
336 <navPoint playOrder=\"2\" id=\"navPoint-2\">
337 <navLabel>
338 <text>2</text>
339 </navLabel>
340 <content src=\"#2\"/>
341 </navPoint>";
342 assert_eq!(&actual, expected);
343}
344
345#[test]
346fn toc_epub_simple_sublevels() {
347 let mut toc = Toc::new();
348 toc.add(TocElement::new("#1", "1"));
349 toc.add(TocElement::new("#1.1", "1.1").level(2));
350 toc.add(TocElement::new("#2", "2"));
351 toc.add(TocElement::new("#2.1", "2.1").level(2));
352 let actual = toc.render_epub(true);
353 let expected = " <navPoint playOrder=\"1\" id=\"navPoint-1\">
354 <navLabel>
355 <text>1</text>
356 </navLabel>
357 <content src=\"#1\"/>
358 <navPoint playOrder=\"2\" id=\"navPoint-2\">
359 <navLabel>
360 <text>1.1</text>
361 </navLabel>
362 <content src=\"#1.1\"/>
363 </navPoint>
364 </navPoint>
365 <navPoint playOrder=\"3\" id=\"navPoint-3\">
366 <navLabel>
367 <text>2</text>
368 </navLabel>
369 <content src=\"#2\"/>
370 <navPoint playOrder=\"4\" id=\"navPoint-4\">
371 <navLabel>
372 <text>2.1</text>
373 </navLabel>
374 <content src=\"#2.1\"/>
375 </navPoint>
376 </navPoint>";
377 assert_eq!(&actual, expected);
378}
379
380#[test]
381fn toc_epub_broken_sublevels() {
382 let mut toc = Toc::new();
383 toc.add(TocElement::new("#1.1", "1.1").level(2));
384 toc.add(TocElement::new("#2", "2"));
385 toc.add(TocElement::new("#2.1", "2.1").level(2));
386 let actual = toc.render_epub(true);
387 let expected = " <navPoint playOrder=\"1\" id=\"navPoint-1\">
388 <navLabel>
389 <text>1.1</text>
390 </navLabel>
391 <content src=\"#1.1\"/>
392 </navPoint>
393 <navPoint playOrder=\"2\" id=\"navPoint-2\">
394 <navLabel>
395 <text>2</text>
396 </navLabel>
397 <content src=\"#2\"/>
398 <navPoint playOrder=\"3\" id=\"navPoint-3\">
399 <navLabel>
400 <text>2.1</text>
401 </navLabel>
402 <content src=\"#2.1\"/>
403 </navPoint>
404 </navPoint>";
405 assert_eq!(&actual, expected);
406}
407
408#[test]
409fn toc_epub_title_escaped() {
410 let mut toc = Toc::new();
411 toc.add(TocElement::new("#1", "D&D"));
412 let actual = toc.render_epub(true);
413 let expected = " <navPoint playOrder=\"1\" id=\"navPoint-1\">
414 <navLabel>
415 <text>D&D</text>
416 </navLabel>
417 <content src=\"#1\"/>
418 </navPoint>";
419 assert_eq!(&actual, expected);
420}
421
422#[test]
423fn toc_epub_title_not_escaped() {
424 let mut toc = Toc::new();
425 toc.add(TocElement::new("#1", "<em>D&D<em>").raw_title("D&D"));
426 let actual = toc.render_epub(false);
427 let expected = " <navPoint playOrder=\"1\" id=\"navPoint-1\">
428 <navLabel>
429 <text>D&D</text>
430 </navLabel>
431 <content src=\"#1\"/>
432 </navPoint>";
433 assert_eq!(&actual, expected);
434}