epub_builder/
toc.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with
3// this file, You can obtain one at https://mozilla.org/MPL/2.0/.
4use crate::common;
5
6/// An element of the [Table of contents](struct.Toc.html)
7///
8/// # Example
9///
10/// ```
11/// use epub_builder::TocElement;
12/// TocElement::new("chapter_1.xhtml", "Chapter 1")
13///     .child(TocElement::new("chapter_1.xhtml#1", "Chapter 1, section 1")
14///               .child(TocElement::new("chapter_1.xhtml#1-1", "Chapter 1, section 1, subsection 1")));
15/// ```
16#[derive(Debug, Clone)]
17pub struct TocElement {
18    /// The level. 0: part, 1: chapter, 2: section, ...
19    pub level: i32,
20    /// The link
21    pub url: String,
22    /// Title of this entry
23    pub title: String,
24    /// Title of this entry without HTML tags (if None, defaults to the main one)
25    pub raw_title: Option<String>,
26    /// Inner elements
27    pub children: Vec<TocElement>,
28}
29
30impl TocElement {
31    /// Creates a new element of the toc
32    ///
33    /// By default, the element's level is `1` and it has no children.
34    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    /// Adds an alternate version of the title without HTML tags.
45    ///
46    /// Useful only if you disable escaping of HTML fields.
47    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    /// Sets the level of a TocElement
53    pub fn level(mut self, level: i32) -> Self {
54        self.level = level;
55        self
56    }
57
58    /// Change level, recursively, so the structure keeps having some sense
59    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    /// Add a child to this element.
69    ///
70    /// This adjust the level of the child to be the level of its parents, plus 1;
71    /// this means that there is no point in manually setting the level to elements
72    /// added with this method.
73    ///
74    /// # Example
75    ///
76    /// ```
77    /// use epub_builder::TocElement;
78    /// let elem = TocElement::new("foo.xhtml", "Foo")
79    ///     .child(TocElement::new("bar.xhtml", "Bar")
80    ///          .level(42));
81    ///
82    /// // `Bar`'s level wiss still be `2`.
83    /// ```
84    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    /// Add element to self or to children, according to its level
93    ///
94    /// This will adds `element` directly to `self` if its level is equal or less
95    /// to the last children element; else it will insert it to the last child.
96    ///
97    /// See the `add` method of [`Toc](struct.toc.html).
98    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    /// Render element for Epub's toc.ncx format
112    #[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        // Try to use the raw title of all HTML elements; if it doesn't exist, insert escaped title
128        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, // Not escaped: XML content
146            ),
147        )
148    }
149
150    /// Render element as a list element
151    #[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" }, // Not escaped: Static string
170                children = common::indent(output.join("\n"), 1), // Not escaped: XML content
171            );
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), // Not escaped: XML content
181            )
182        }
183    }
184}
185
186/// A Table Of Contents
187///
188/// It basically contains a list of [`TocElement`](struct.TocElement.html)s.
189///
190/// # Example
191///
192/// Creates a Toc, fills it, and render it to HTML:
193///
194/// ```
195/// use epub_builder::{Toc, TocElement};
196/// Toc::new()
197///    // add a level-1 element
198///    .add(TocElement::new("intro.xhtml", "Introduction"))
199///    // add a level-1 element with children
200///    .add(TocElement::new("chapter_1.xhtml", "Chapter 1")
201///            .child(TocElement::new("chapter_1.xhtml#section1", "1.1: Some section"))
202///            .child(TocElement::new("chapter_1.xhtml#section2", "1.2: another section")))
203///    // add a level-2 element, which will thus get "attached" to previous level-1 element
204///    .add(TocElement::new("chapter_1.xhtml#section3", "1.3: yet another section")
205///            .level(2))
206///    // render the toc (non-numbered list, escape html) and returns a string
207///    .render(false, true);
208/// ```
209#[derive(Debug, Default)]
210pub struct Toc {
211    /// The elements composing the TOC
212    pub elements: Vec<TocElement>,
213}
214
215impl Toc {
216    /// Creates a new, empty, Toc
217    pub fn new() -> Toc {
218        Toc { elements: vec![] }
219    }
220
221    /// Returns `true` if the toc is empty, `false` else.
222    ///
223    /// Note that `empty` here means that the the toc has zero *or one*
224    /// element, since it's still not worth displaying it in this case.
225    pub fn is_empty(&self) -> bool {
226        self.elements.len() <= 1
227    }
228
229    /// Adds a [`TocElement`](struct.TocElement.html) to the Toc.
230    ///
231    /// This will look at the element's level and will insert it as a child of the last
232    /// element of the Toc that has an inferior level.
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// # use epub_builder::{Toc, TocElement};
238    /// let mut toc = Toc::new();
239    /// // Insert an element at default level (1)
240    /// toc.add(TocElement::new("chapter_1.xhtml", "Chapter 1"));
241    ///
242    /// // Insert an element at level 2
243    /// toc.add(TocElement::new("section_1.xhtml", "Section 1")
244    ///           .level(2));
245    /// // "Section 1" is now a child of "Chapter 1"
246    /// ```
247    ///
248    /// There are some cases where this behaviour might not be what you want; however,
249    /// it makes sure that the TOC can still be renderer correctly for HTML and EPUB.
250    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    /// Render the Toc in a toc.ncx compatible way, for EPUB.
266    ///
267    /// * `escape_html`: whether titles should be HTML-encoded or not (only applies to titles)
268    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    /// Render the Toc in either <ul> or <ol> form (according to numbered)
280    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), // Not escaped: XML content
290                oul = if numbered { "ol" } else { "ul" }       // Not escaped: Static string
291            ),
292            2,
293        )
294    }
295}
296
297/////////////////////////////////////////////////////////////////////////////////
298///                                  TESTS                                     //
299/////////////////////////////////////////////////////////////////////////////////
300
301#[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&amp;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&amp;D<em>").raw_title("D&amp;D"));
426    let actual = toc.render_epub(false);
427    let expected = "    <navPoint playOrder=\"1\" id=\"navPoint-1\">
428      <navLabel>
429       <text>D&amp;D</text>
430      </navLabel>
431      <content src=\"#1\"/>
432    </navPoint>";
433    assert_eq!(&actual, expected);
434}