epub_builder/
epub.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/.
4
5use crate::templates;
6use crate::toc::{Toc, TocElement};
7use crate::zip::Zip;
8use crate::ReferenceType;
9use crate::Result;
10use crate::{common, EpubContent};
11
12use core::fmt::Debug;
13use std::io;
14use std::io::Read;
15use std::path::Path;
16use std::str::FromStr;
17use upon::Engine;
18
19/// Represents the EPUB version.
20///
21/// Currently, this library supports EPUB 2.0.1 and 3.0.1.
22#[non_exhaustive]
23#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Eq)]
24pub enum EpubVersion {
25    /// EPUB 2.0.1 format
26    V20,
27    /// EPUB 3.0.1 format
28    V30,
29    /// EPUB 3.3.0 format
30    V33,
31}
32
33pub trait MetadataRenderer: Send + Sync {
34    fn render_opf(&self, escape_html: bool) -> String;
35}
36
37impl Debug for dyn MetadataRenderer {
38    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39        write!(f, "MetadataRenderer{{{}}}", self.render_opf(true))
40    }
41}
42
43/// Represents the EPUB `<meta>` content inside `content.opf` file.
44///
45/// <meta dir="" id="" property="" refines="" scheme="" xml:lang="">content</meta>
46/// https://www.w3.org/TR/epub-33/#sec-meta-elem
47#[derive(Debug)]
48pub struct MetadataOpfV3 {
49    /// The property attribute takes a property data type value that defines the statement
50    /// made in the expression, and the text content of the element represents the assertion. 
51    /// https://www.w3.org/TR/epub-33/#attrdef-meta-property
52    pub property: String,
53
54    /// The content of the metadata tag very much based on what you put in property
55    pub content: String,
56
57    /// Specifies the base direction [bidi] of the textual content and attribute values
58    /// of the carrying element and its descendants.
59    /// https://www.w3.org/TR/epub-33/#attrdef-dir
60    pub dir: Option<String>,
61
62    /// The ID xml of the element, which MUST be unique within the document scope.
63    /// https://www.w3.org/TR/epub-33/#attrdef-id
64    pub id: Option<String>,
65
66    /// Establishes an association between the current expression and the element or resource
67    /// identified by its value. 
68    /// https://www.w3.org/TR/epub-33/#attrdef-refines
69    pub refines: Option<String>,
70
71    /// The scheme attribute identifies the system or scheme the EPUB creator obtained the
72    /// element's value from.
73    /// https://www.w3.org/TR/epub-33/#attrdef-scheme
74    pub scheme: Option<String>,
75
76    /// Specifies the language of the textual content and attribute values of the
77    /// carrying element and its descendants.
78    /// https://www.w3.org/TR/epub-33/#attrdef-xml-lang
79    pub xml_lang: Option<String>,
80}
81
82impl MetadataOpfV3 {
83    /// Create instance of MetadataOpfV3
84    ///
85    pub fn new(property: String, content: String) -> MetadataOpfV3 {
86        MetadataOpfV3{
87            property: property,
88            content: content,
89            dir: None,
90            id: None,
91            refines: None,
92            scheme: None,
93            xml_lang: None,
94        }
95    }
96
97    /// Add reading direction metadata
98    pub fn add_direction(&mut self, direction: String) -> &mut Self {
99        self.dir = Some(direction);
100        self
101    }
102
103    /// Add id metadata
104    pub fn add_id(&mut self, id: String) -> &mut Self {
105        self.id = Some(id);
106        self
107    }
108
109    /// Add refines metadata
110    pub fn add_refines(&mut self, refines: String) -> &mut Self {
111        self.id = Some(refines);
112        self
113    }
114
115    /// Add scheme metadata
116    pub fn add_scheme(&mut self, scheme: String) -> &mut Self {
117        self.scheme = Some(scheme);
118        self
119    }
120
121    /// Add xml_lang metadata
122    pub fn add_xml_lang(&mut self, xml_lang: String) -> &mut Self {
123        self.xml_lang = Some(xml_lang);
124        self
125    }
126}
127
128impl MetadataRenderer for MetadataOpfV3 {
129    /// Create instance of MetadataOpfV3
130    fn render_opf(&self, escape_html: bool) -> String {
131        let mut meta_tag = String::from("<meta ");
132
133        if let Some(dir) = &self.dir {
134            meta_tag.push_str(&format!(
135                    "dir=\"{}\" ", common::encode_html(dir, escape_html),
136            ));
137        }
138
139        if let Some(id) = &self.id {
140            meta_tag.push_str(&format!(
141                    "id=\"{}\" ", common::encode_html(id, escape_html),
142            ));
143        }
144
145        if let Some(refines) = &self.refines {
146            meta_tag.push_str(&format!(
147                    "refines=\"{}\" ", common::encode_html(refines, escape_html)
148            ));
149        }
150
151        if let Some(scheme) = &self.scheme {
152            meta_tag.push_str(&format!(
153                    "scheme=\"{}\" ", common::encode_html(scheme, escape_html),
154            ));
155        }
156
157        if let Some(xml_lang) = &self.xml_lang {
158            meta_tag.push_str(&format!(
159                    "xml:lang=\"{}\" ", common::encode_html(xml_lang, escape_html) 
160            ));
161        }
162
163        meta_tag.push_str(&format!(
164                "property=\"{}\">{}</meta>",
165                common::encode_html(&self.property, escape_html),
166                &self.content,
167        ));
168
169        meta_tag
170    }
171}
172
173/// Represents the EPUB `<meta>` content inside `content.opf` file.
174///
175/// <meta name="" content="">
176/// 
177#[derive(Debug)]
178pub struct MetadataOpf {
179    /// Name of the `<meta>` tag
180    pub name: String,
181    /// Content of the `<meta>` tag
182    pub content: String
183}
184
185impl MetadataOpf {
186    /// Create new instance
187    /// 
188    /// 
189    pub fn new(&self, meta_name: String, meta_content: String) -> Self {
190        Self { name: meta_name, content: meta_content }
191    }
192}
193
194impl MetadataRenderer for MetadataOpf {
195    fn render_opf(&self, escape_html: bool) -> String {
196        format!(
197            "<meta name=\"{}\" content=\"{}\"/>", 
198            common::encode_html(&self.name, escape_html),
199            common::encode_html(&self.content, escape_html),
200        )
201    }
202}
203
204/// The page-progression-direction attribute of spine is a global attribute and
205/// therefore defines the pagination flow of the book as a whole.
206#[derive(Debug, Copy, Clone, Default)]
207pub enum PageDirection {
208    /// Left to right
209    #[default]
210    Ltr,
211    /// Right to left
212    Rtl,
213}
214
215impl ToString for PageDirection {
216    fn to_string(&self) -> String {
217        match &self {
218            PageDirection::Rtl => "rtl".into(),
219            PageDirection::Ltr => "ltr".into(),
220        }
221    }
222}
223
224impl FromStr for PageDirection {
225    type Err = crate::Error;
226
227    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
228        let s = s.to_lowercase();
229        match s.as_ref() {
230            "rtl" => Ok(PageDirection::Rtl),
231            "ltr" => Ok(PageDirection::Ltr),
232            _ => Err(crate::Error::PageDirectionError(s)),
233        }
234    }
235}
236
237/// EPUB Metadata
238#[derive(Debug)]
239pub struct Metadata {
240    pub title: String,
241    pub author: Vec<String>,
242    pub lang: String,
243    pub direction: PageDirection,
244    pub generator: String,
245    pub toc_name: String,
246    pub description: Vec<String>,
247    pub subject: Vec<String>,
248    pub license: Option<String>,
249    pub date_published: Option<chrono::DateTime<chrono::Utc>>,
250    pub date_modified: Option<chrono::DateTime<chrono::Utc>>,
251    pub uuid: Option<uuid::Uuid>,
252}
253
254impl Default for Metadata {
255    fn default() -> Self {
256        Self {
257            title: String::new(),
258            author: vec![],
259            lang: String::from("en"),
260            direction: PageDirection::default(),
261            generator: String::from("Rust EPUB library"),
262            toc_name: String::from("Table Of Contents"),
263            description: vec![],
264            subject: vec![],
265            license: None,
266            date_published: None,
267            date_modified: None,
268            uuid: None,
269        }
270    }
271}
272
273/// A file added in the EPUB
274#[derive(Debug)]
275struct Content {
276    pub file: String,
277    pub mime: String,
278    pub itemref: bool,
279    pub cover: bool,
280    pub reftype: Option<ReferenceType>,
281    pub title: String,
282}
283
284impl Content {
285    /// Create a new content file
286    pub fn new<S1, S2>(file: S1, mime: S2) -> Content
287    where
288        S1: Into<String>,
289        S2: Into<String>,
290    {
291        Content {
292            file: file.into(),
293            mime: mime.into(),
294            itemref: false,
295            cover: false,
296            reftype: None,
297            title: String::new(),
298        }
299    }
300}
301
302/// Epub Builder
303///
304/// The main struct you'll need to use in this library. It is first created using
305/// a wrapper to zip files; then you add content to it, and finally you generate
306/// the EPUB file by calling the `generate` method.
307///
308/// ```
309/// use epub_builder::EpubBuilder;
310/// use epub_builder::ZipCommand;
311/// use std::io;
312///
313/// // "Empty" EPUB file
314/// let mut builder = EpubBuilder::new(ZipCommand::new().unwrap()).unwrap();
315/// builder.metadata("title", "Empty EPUB").unwrap();
316/// builder.metadata("author", "Ann 'Onymous").unwrap();
317/// builder.generate(&mut io::stdout()).unwrap();
318/// ```
319#[derive(Debug)]
320pub struct EpubBuilder<Z: Zip> {
321    version: EpubVersion,
322    direction: PageDirection,    
323    zip: Z,
324    files: Vec<Content>,
325    metadata: Metadata,
326    toc: Toc,
327    stylesheet: bool,
328    inline_toc: bool,
329    escape_html: bool,
330    meta_opf: Vec<Box<dyn MetadataRenderer>>,
331}
332
333impl<Z: Zip> EpubBuilder<Z> {
334    /// Create a new default EPUB Builder
335    pub fn new(zip: Z) -> Result<EpubBuilder<Z>> {
336        let mut epub = EpubBuilder {
337            version: EpubVersion::V20,
338            direction: PageDirection::Ltr,
339            zip,
340            files: vec![],
341            metadata: Metadata::default(),
342            toc: Toc::new(),
343            stylesheet: false,
344            inline_toc: false,
345            escape_html: true,
346            meta_opf: vec![],
347        };
348
349        epub.zip
350            .write_file("META-INF/container.xml", templates::CONTAINER)?;
351        epub.zip.write_file(
352            "META-INF/com.apple.ibooks.display-options.xml",
353            templates::IBOOKS,
354        )?;
355
356        Ok(epub)
357    }
358
359    /// Set EPUB version (default: V20)
360    ///
361    /// Supported versions are:
362    ///
363    /// * `V20`: EPUB 2.0.1
364    /// * 'V30`: EPUB 3.0.1
365    /// * 'V33`: EPUB 3.3
366    pub fn epub_version(&mut self, version: EpubVersion) -> &mut Self {
367        self.version = version;
368        self
369    }
370    
371    /// Set EPUB Direction (default: Ltr)
372    ///
373    /// * `Ltr`: Left-To-Right 
374    /// * `Rtl`: Right-To-Left 
375    /// 
376    /// 
377    pub fn epub_direction(&mut self, direction: PageDirection) -> &mut Self {
378        self.direction = direction;
379        self
380    }
381    
382    /// Add custom <meta> to `content.opf`
383    /// Syntax: `self.add_metadata_opf(name, content)`
384    /// 
385    /// ### Example
386    /// If you wanna add `<meta name="primary-writing-mode" content="vertical-rl"/>` into `content.opf`
387    /// 
388    /// ```rust
389    /// use epub_builder::EpubBuilder;
390    /// use epub_builder::ZipCommand;
391    /// use epub_builder::MetadataOpf;
392    /// use epub_builder::MetadataOpfV3;
393    /// let mut builder = EpubBuilder::new(ZipCommand::new().unwrap()).unwrap();
394    ///
395    /// builder.add_metadata_opf(Box::new(MetadataOpf{
396    ///         name: String::from("dcterms:modified"),
397    ///         content: String::from("2016-02-29T12:34:56Z")
398    ///     }
399    /// ));
400    ///
401    /// builder.add_metadata_opf(Box::new(MetadataOpfV3::new(
402    ///         String::from("dcterms:modified"),
403    ///         String::from("2016-02-29T12:34:56Z")
404    /// )));
405    ///
406    /// ```
407    /// 
408    pub fn add_metadata_opf(&mut self, item: Box<dyn MetadataRenderer>) -> &mut Self {
409        self.meta_opf.push(item);
410        self
411    }
412
413    /// Set some EPUB metadata
414    ///
415    /// For most metadata, this function will replace the existing metadata, but for subject, cteator and identifier who
416    /// can have multiple values, it will add data to the existing data, unless the empty string "" is passed, in which case
417    /// it will delete existing data for this key.
418    ///
419    /// # Valid keys used by the EPUB builder
420    ///
421    /// * `author`: author(s) of the book;
422    /// * `title`: title of the book;
423    /// * `lang`: the language of the book, quite important as EPUB renderers rely on it
424    ///   for e.g. hyphenating words.
425    /// * `generator`: generator of the book (should be your program name);
426    /// * `toc_name`: the name to use for table of contents (by default, "Table of Contents");
427    /// * `subject`;
428    /// * `description`;
429    /// * `license`.
430
431    pub fn metadata<S1, S2>(&mut self, key: S1, value: S2) -> Result<&mut Self>
432    where
433        S1: AsRef<str>,
434        S2: Into<String>,
435    {
436        match key.as_ref() {
437            "author" => {
438                let value = value.into();
439                if value.is_empty() {
440                    self.metadata.author = vec![];
441                } else {
442                    self.metadata.author.push(value);
443                }
444            }
445            "title" => self.metadata.title = value.into(),
446            "lang" => self.metadata.lang = value.into(),
447            "direction" => self.metadata.direction = PageDirection::from_str(&value.into())?,
448            "generator" => self.metadata.generator = value.into(),
449            "description" => {
450                let value = value.into();
451                if value.is_empty() {
452                    self.metadata.description = vec![];
453                } else {
454                    self.metadata.description.push(value);
455                }
456            }
457            "subject" => {
458                let value = value.into();
459                if value.is_empty() {
460                    self.metadata.subject = vec![];
461                } else {
462                    self.metadata.subject.push(value);
463                }
464            }
465            "license" => self.metadata.license = Some(value.into()),
466            "toc_name" => self.metadata.toc_name = value.into(),
467            s => Err(crate::Error::InvalidMetadataError(s.to_string()))?,
468        }
469        Ok(self)
470    }
471
472    /// Sets the authors of the EPUB
473    pub fn set_authors(&mut self, value: Vec<String>) {
474        self.metadata.author = value;
475    }
476
477    /// Add an author to the EPUB
478    pub fn add_author<S: Into<String>>(&mut self, value: S) {
479        self.metadata.author.push(value.into());
480    }
481
482    /// Remove all authors from EPUB
483    pub fn clear_authors<S: Into<String>>(&mut self) {
484        self.metadata.author.clear()
485    }
486
487    /// Sets the title of the EPUB
488    pub fn set_title<S: Into<String>>(&mut self, value: S) {
489        self.metadata.title = value.into();
490    }
491
492    /// Tells whether fields should be HTML-escaped.
493    ///
494    /// * `true`: fields such as titles, description, and so on will be HTML-escaped everywhere (default)
495    /// * `false`: fields will be left as is (letting you in charge of making
496    /// sure they do not contain anything illegal, e.g. < and > characters)
497    pub fn escape_html(&mut self, val: bool) {
498        self.escape_html = val;
499    }
500
501    /// Sets the language of the EPUB
502    ///
503    /// This is quite important as EPUB renderers rely on it
504    /// for e.g. hyphenating words.
505    pub fn set_lang<S: Into<String>>(&mut self, value: S) {
506        self.metadata.lang = value.into();
507    }
508
509    /// Sets the generator of the book (should be your program name)
510    pub fn set_generator<S: Into<String>>(&mut self, value: S) {
511        self.metadata.generator = value.into();
512    }
513
514    /// Sets the name to use for table of contents. This is by default, "Table of Contents"
515    pub fn set_toc_name<S: Into<String>>(&mut self, value: S) {
516        self.metadata.toc_name = value.into();
517    }
518
519    /// Sets and replaces the description of the EPUB
520    pub fn set_description(&mut self, value: Vec<String>) {
521        self.metadata.description = value;
522    }
523
524    /// Adds a line to the EPUB description
525    pub fn add_description<S: Into<String>>(&mut self, value: S) {
526        self.metadata.description.push(value.into());
527    }
528
529    /// Remove all description paragraphs from EPUB
530    pub fn clear_description(&mut self) {
531        self.metadata.description.clear();
532    }
533
534    /// Sets and replaces the subjects of the EPUB
535    pub fn set_subjects(&mut self, value: Vec<String>) {
536        self.metadata.subject = value;
537    }
538
539    /// Adds a value to the subjects
540    pub fn add_subject<S: Into<String>>(&mut self, value: S) {
541        self.metadata.subject.push(value.into());
542    }
543
544    /// Remove all the subjects from EPUB
545    pub fn clear_subjects(&mut self) {
546        self.metadata.subject.clear();
547    }
548
549    /// Sets the license under which this EPUB is distributed
550    pub fn set_license<S: Into<String>>(&mut self, value: S) {
551        self.metadata.license = Some(value.into());
552    }
553
554    /// Sets the publication date of the EPUB
555    pub fn set_publication_date(&mut self, date_published: chrono::DateTime<chrono::Utc>) {
556        self.metadata.date_published = Some(date_published);
557    }
558    /// Sets the date on which the EPUB was last modified.
559    ///
560    /// This value is part of the metadata. If this function is not called, the time at the
561    /// moment of generation will be used instead.
562    pub fn set_modified_date(&mut self, date_modified: chrono::DateTime<chrono::Utc>) {
563        self.metadata.date_modified = Some(date_modified);
564    }
565    /// Sets the uuid used for the EPUB.
566    ///
567    /// This is useful for reproducibly generating epubs.
568    pub fn set_uuid(&mut self, uuid: uuid::Uuid) {
569        self.metadata.uuid = Some(uuid);
570    }
571
572    /// Sets stylesheet of the EPUB.
573    ///
574    /// This content will be written in a `stylesheet.css` file; it is used by
575    /// some pages (such as nav.xhtml), you don't have use it in your documents though it
576    /// makes sense to also do so.
577    pub fn stylesheet<R: Read>(&mut self, content: R) -> Result<&mut Self> {
578        self.add_resource("stylesheet.css", content, "text/css")?;
579        self.stylesheet = true;
580        Ok(self)
581    }
582
583    /// Adds an inline toc in the document.
584    ///
585    /// If this method is called it adds a page that contains the table of contents
586    /// that appears in the document.
587    ///
588    /// The position where this table of contents will be inserted depends on when
589    /// you call this method: if you call it before adding any content, it will be
590    /// at the beginning, if you call it after, it will be at the end.
591    pub fn inline_toc(&mut self) -> &mut Self {
592        self.inline_toc = true;
593        self.toc.add(TocElement::new(
594            "toc.xhtml",
595            self.metadata.toc_name.as_str(),
596        ));
597        let mut file = Content::new("toc.xhtml", "application/xhtml+xml");
598        file.reftype = Some(ReferenceType::Toc);
599        file.title = self.metadata.toc_name.clone();
600        file.itemref = true;
601        self.files.push(file);
602        self
603    }
604
605    /// Add a resource to the EPUB file
606    ///
607    /// This resource can be a picture, a font, some CSS file, .... Unlike
608    /// `add_content`, files added this way won't appear in the linear
609    /// document.
610    ///
611    /// Note that these files will automatically be inserted into an `OEBPS` directory,
612    /// so you don't need (and shouldn't) prefix your path with `OEBPS/`.
613    ///
614    /// # Arguments
615    ///
616    /// * `path`: the path where this file will be written in the EPUB OEBPS structure,
617    ///   e.g. `data/image_0.png`
618    /// * `content`: the resource to include
619    /// * `mime_type`: the mime type of this file, e.g. "image/png".
620    pub fn add_resource<R, P, S>(&mut self, path: P, content: R, mime_type: S) -> Result<&mut Self>
621    where
622        R: Read,
623        P: AsRef<Path>,
624        S: Into<String>,
625    {
626        self.zip
627            .write_file(Path::new("OEBPS").join(path.as_ref()), content)?;
628        log::debug!("Add resource: {:?}", path.as_ref().display());
629        self.files.push(Content::new(
630            format!("{}", path.as_ref().display()),
631            mime_type,
632        ));
633        Ok(self)
634    }
635
636    /// Add a cover image to the EPUB.
637    ///
638    /// This works similarly to adding the image as a resource with the `add_resource`
639    /// method, except, it signals it in the Manifest section so it is displayed as the
640    /// cover by Ereaders
641    pub fn add_cover_image<R, P, S>(
642        &mut self,
643        path: P,
644        content: R,
645        mime_type: S,
646    ) -> Result<&mut Self>
647    where
648        R: Read,
649        P: AsRef<Path>,
650        S: Into<String>,
651    {
652        self.zip
653            .write_file(Path::new("OEBPS").join(path.as_ref()), content)?;
654        let mut file = Content::new(format!("{}", path.as_ref().display()), mime_type);
655        file.cover = true;
656        self.files.push(file);
657        Ok(self)
658    }
659
660    /// Add a XHTML content file that will be added to the EPUB.
661    ///
662    /// # Examples
663    ///
664    /// ```
665    /// # use epub_builder::{EpubBuilder, ZipLibrary, EpubContent};
666    /// let content = "Some content";
667    /// let mut builder = EpubBuilder::new(ZipLibrary::new().unwrap()).unwrap();
668    /// // Add a chapter that won't be added to the Table of Contents
669    /// builder.add_content(EpubContent::new("intro.xhtml", content.as_bytes())).unwrap();
670    /// ```
671    ///
672    /// ```
673    /// # use epub_builder::{EpubBuilder, ZipLibrary, EpubContent, TocElement};
674    /// # let mut builder = EpubBuilder::new(ZipLibrary::new().unwrap()).unwrap();
675    /// # let content = "Some content";
676    /// // Sets the title of a chapter so it is added to the Table of contents
677    /// // Also add information about its structure
678    /// builder.add_content(EpubContent::new("chapter_1.xhtml", content.as_bytes())
679    ///                      .title("Chapter 1")
680    ///                      .child(TocElement::new("chapter_1.xhtml#1", "1.1"))).unwrap();
681    /// ```
682    ///
683    /// ```
684    /// # use epub_builder::{EpubBuilder, ZipLibrary, EpubContent};
685    /// # let mut builder = EpubBuilder::new(ZipLibrary::new().unwrap()).unwrap();
686    /// # let content = "Some content";
687    /// // Add a section, by setting the level to 2 (instead of the default value 1)
688    /// builder.add_content(EpubContent::new("section.xhtml", content.as_bytes())
689    ///                      .title("Section 1")
690    ///                      .level(2)).unwrap();
691    /// ```
692    ///
693    /// Note that these files will automatically be inserted into an `OEBPS` directory,
694    /// so you don't need (and shouldn't) prefix your path with `OEBPS/`.
695    ///
696    /// # See also
697    ///
698    /// * [`EpubContent`](struct.EpubContent.html)
699    /// * the `add_resource` method, to add other resources in the EPUB file.
700    pub fn add_content<R: Read>(&mut self, content: EpubContent<R>) -> Result<&mut Self> {
701        self.zip.write_file(
702            Path::new("OEBPS").join(content.toc.url.as_str()),
703            content.content,
704        )?;
705        let mut file = Content::new(content.toc.url.as_str(), "application/xhtml+xml");
706        file.itemref = true;
707        file.reftype = content.reftype;
708        if file.reftype.is_some() {
709            file.title = content.toc.title.clone();
710        }
711        self.files.push(file);
712        if !content.toc.title.is_empty() {
713            self.toc.add(content.toc);
714        }
715        Ok(self)
716    }
717
718    /// Generate the EPUB file and write it to the writer
719    ///
720    /// # Example
721    ///
722    /// ```
723    /// # use epub_builder::{EpubBuilder, ZipLibrary};
724    /// let mut builder = EpubBuilder::new(ZipLibrary::new().unwrap()).unwrap();
725    /// // Write the EPUB file into a Vec<u8>
726    /// let mut epub: Vec<u8> = vec!();
727    /// builder.generate(&mut epub).unwrap();
728    /// ```
729    pub fn generate<W: io::Write>(mut self, to: W) -> Result<()> {
730        // If no styleesheet was provided, generate a dummy one
731        if !self.stylesheet {
732            self.stylesheet(b"".as_ref())?;
733        }
734        // Render content.opf
735        let bytes = self.render_opf()?;
736        self.zip.write_file("OEBPS/content.opf", &*bytes)?;
737        // Render toc.ncx
738        let bytes = self.render_toc()?;
739        self.zip.write_file("OEBPS/toc.ncx", &*bytes)?;
740        // Render nav.xhtml
741        let bytes = self.render_nav(true)?;
742        self.zip.write_file("OEBPS/nav.xhtml", &*bytes)?;
743        // Write inline toc if it needs to
744        if self.inline_toc {
745            let bytes = self.render_nav(false)?;
746            self.zip.write_file("OEBPS/toc.xhtml", &*bytes)?;
747        }
748
749        self.zip.generate(to)?;
750        Ok(())
751    }
752
753    /// Render content.opf file
754    fn render_opf(&mut self) -> Result<Vec<u8>> {
755        log::debug!("render_opf...");
756        let mut optional: Vec<String> = Vec::new();
757        for desc in &self.metadata.description {
758            optional.push(format!(
759                "<dc:description>{}</dc:description>",
760                common::encode_html(desc, self.escape_html),
761            ));
762        }
763        for subject in &self.metadata.subject {
764            optional.push(format!(
765                "<dc:subject>{}</dc:subject>",
766                common::encode_html(subject, self.escape_html),
767            ));
768        }
769        if let Some(ref rights) = self.metadata.license {
770            optional.push(format!(
771                "<dc:rights>{}</dc:rights>",
772                common::encode_html(rights, self.escape_html),
773            ));
774        }
775
776        for meta in &self.meta_opf {
777            optional.push(meta.render_opf(self.escape_html))
778        }
779
780        let date_modified = self
781            .metadata
782            .date_modified
783            .unwrap_or_else(chrono::Utc::now)
784            .format("%Y-%m-%dT%H:%M:%SZ");
785        let date_published = self
786            .metadata
787            .date_published
788            .map(|date| date.format("%Y-%m-%dT%H:%M:%SZ"));
789        let uuid = uuid::fmt::Urn::from_uuid(self.metadata.uuid.unwrap_or_else(uuid::Uuid::new_v4))
790            .to_string();
791
792        let mut items: Vec<String> = Vec::new();
793        let mut itemrefs: Vec<String> = Vec::new();
794        let mut guide: Vec<String> = Vec::new();
795
796        for content in &self.files {
797            let id = if content.cover {
798                String::from("cover-image")
799            } else {
800                to_id(&content.file)
801            };
802            let properties = match (self.version, content.cover) {
803                (EpubVersion::V30, true) => "properties=\"cover-image\" ",
804                (EpubVersion::V33, true) => "properties=\"cover-image\" ",
805                _ => "",
806            };
807            if content.cover {
808                optional.push("<meta name=\"cover\" content=\"cover-image\"/>".to_string());
809            }
810            log::debug!("id={:?}, mime={:?}", id, content.mime);
811            items.push(format!(
812                "<item media-type=\"{mime}\" {properties}\
813                        id=\"{id}\" href=\"{href}\"/>",
814                properties = properties, // Not escaped: XML attributes above
815                mime = html_escape::encode_double_quoted_attribute(&content.mime),
816                id = html_escape::encode_double_quoted_attribute(&id),
817                // in the zip the path is always with forward slashes, on windows it is with backslashes
818                href =
819                    html_escape::encode_double_quoted_attribute(&content.file.replace('\\', "/")),
820            ));
821            if content.itemref {
822                itemrefs.push(format!(
823                    "<itemref idref=\"{id}\"/>",
824                    id = html_escape::encode_double_quoted_attribute(&id),
825                ));
826            }
827            if let Some(reftype) = content.reftype {
828                use crate::ReferenceType::*;
829                let reftype = match reftype {
830                    Cover => "cover",
831                    TitlePage => "title-page",
832                    Toc => "toc",
833                    Index => "index",
834                    Glossary => "glossary",
835                    Acknowledgements => "acknowledgements",
836                    Bibliography => "bibliography",
837                    Colophon => "colophon",
838                    Copyright => "copyright",
839                    Dedication => "dedication",
840                    Epigraph => "epigraph",
841                    Foreword => "foreword",
842                    Loi => "loi",
843                    Lot => "lot",
844                    Notes => "notes",
845                    Preface => "preface",
846                    Text => "text",
847                };
848                log::debug!("content = {:?}", &content);
849                guide.push(format!(
850                    "<reference type=\"{reftype}\" title=\"{title}\" href=\"{href}\"/>",
851                    reftype = html_escape::encode_double_quoted_attribute(&reftype),
852                    title = html_escape::encode_double_quoted_attribute(&content.title),
853                    href = html_escape::encode_double_quoted_attribute(&content.file),
854                ));
855            }
856        }
857
858        let data = {
859            let mut authors: Vec<_> = vec![];
860            for (i, author) in self.metadata.author.iter().enumerate() {
861                let author = upon::value! {
862                    id_attr: html_escape::encode_double_quoted_attribute(&i.to_string()),
863                    name: common::encode_html(author, self.escape_html)
864                };
865                authors.push(author);
866            }
867            upon::value! {
868                author: authors,
869                lang: html_escape::encode_text(&self.metadata.lang),
870                direction: self.metadata.direction.to_string(),
871                title: common::encode_html(&self.metadata.title, self.escape_html),
872                generator_attr: html_escape::encode_double_quoted_attribute(&self.metadata.generator),
873                toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
874                toc_name_attr: html_escape::encode_double_quoted_attribute(&self.metadata.toc_name),
875                optional: common::indent(optional.join("\n"), 2),
876                items: common::indent(items.join("\n"), 2), // Not escaped: XML content
877                itemrefs: common::indent(itemrefs.join("\n"), 2), // Not escaped: XML content
878                date_modified: html_escape::encode_text(&date_modified.to_string()),
879                uuid: html_escape::encode_text(&uuid),
880                guide: common::indent(guide.join("\n"), 2), // Not escaped: XML content
881                date_published: if let Some(date) = date_published { date.to_string() } else { String::new() },
882            }
883        };
884
885        let mut res: Vec<u8> = vec![];
886        match self.version {
887            EpubVersion::V20 => templates::v2::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
888            EpubVersion::V30 => templates::v3::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
889            EpubVersion::V33 => templates::v3::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
890        }
891        .map_err(|e| crate::Error::TemplateError {
892            msg: "could not render template for content.opf".to_string(),
893            cause: e.into(),
894        })?;
895        //.wrap_err("could not render template for content.opf")?;
896
897        Ok(res)
898    }
899
900    /// Render toc.ncx
901    fn render_toc(&mut self) -> Result<Vec<u8>> {
902        let mut nav_points = String::new();
903
904        nav_points.push_str(&self.toc.render_epub(self.escape_html));
905
906        let data = upon::value! {
907            toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
908            nav_points: nav_points
909        };
910        let mut res: Vec<u8> = vec![];
911        templates::TOC_NCX
912            .render(&Engine::new(), &data)
913            .to_writer(&mut res)
914            .map_err(|e| crate::Error::TemplateError {
915                msg: "error rendering toc.ncx template".to_string(),
916                cause: e.into(),
917            })?;
918        Ok(res)
919    }
920
921    /// Render nav.xhtml
922    fn render_nav(&mut self, numbered: bool) -> Result<Vec<u8>> {
923        let content = self.toc.render(numbered, self.escape_html);
924        let mut landmarks: Vec<String> = Vec::new();
925        if self.version > EpubVersion::V20 {
926            for file in &self.files {
927                if let Some(ref reftype) = file.reftype {
928                    use ReferenceType::*;
929                    let reftype = match *reftype {
930                        Cover => "cover",
931                        Text => "bodymatter",
932                        Toc => "toc",
933                        Bibliography => "bibliography",
934                        Epigraph => "epigraph",
935                        Foreword => "foreword",
936                        Preface => "preface",
937                        Notes => "endnotes",
938                        Loi => "loi",
939                        Lot => "lot",
940                        Colophon => "colophon",
941                        TitlePage => "titlepage",
942                        Index => "index",
943                        Glossary => "glossary",
944                        Copyright => "copyright-page",
945                        Acknowledgements => "acknowledgements",
946                        Dedication => "dedication",
947                    };
948                    if !file.title.is_empty() {
949                        landmarks.push(format!(
950                            "<li><a epub:type=\"{reftype}\" href=\"{href}\">\
951                                {title}</a></li>",
952                            reftype = html_escape::encode_double_quoted_attribute(&reftype),
953                            href = html_escape::encode_double_quoted_attribute(&file.file),
954                            title = common::encode_html(&file.title, self.escape_html),
955                        ));
956                    }
957                }
958            }
959        }
960
961        let data = upon::value! {
962            content: content, // Not escaped: XML content
963            toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
964            generator_attr: html_escape::encode_double_quoted_attribute(&self.metadata.generator),
965            landmarks: if !landmarks.is_empty() {
966                common::indent(
967                    format!(
968                        "<ol>\n{}\n</ol>",
969                        common::indent(landmarks.join("\n"), 1), // Not escaped: XML content
970                    ),
971                    2,
972                )
973            } else {
974                String::new()
975            },
976        };
977
978        let mut res: Vec<u8> = vec![];
979        match self.version {
980            EpubVersion::V20 => templates::v2::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
981            EpubVersion::V30 => templates::v3::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
982            EpubVersion::V33 => templates::v3::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
983        }
984        .map_err(|e| crate::Error::TemplateError {
985            msg: "error rendering nav.xhtml template".to_string(),
986            cause: e.into(),
987        })?;
988        Ok(res)
989    }
990}
991
992// The actual rules for ID are here - https://www.w3.org/TR/xml-names11/#NT-NCNameChar
993// Ordering to to look as similar as possible to the W3 Recommendation ruleset
994// Slightly more permissive, there are some that are invalid start chars, but this is ok.
995fn is_id_char(c: char) -> bool {
996    c.is_ascii_uppercase()
997        || c == '_'
998        || c.is_ascii_lowercase()
999        || ('\u{C0}'..='\u{D6}').contains(&c)
1000        || ('\u{D8}'..='\u{F6}').contains(&c)
1001        || ('\u{F8}'..='\u{2FF}').contains(&c)
1002        || ('\u{370}'..='\u{37D}').contains(&c)
1003        || ('\u{37F}'..='\u{1FFF}').contains(&c)
1004        || ('\u{200C}'..='\u{200D}').contains(&c)
1005        || ('\u{2070}'..='\u{218F}').contains(&c)
1006        || ('\u{2C00}'..='\u{2FEF}').contains(&c)
1007        || ('\u{3001}'..='\u{D7FF}').contains(&c)
1008        || ('\u{F900}'..='\u{FDCF}').contains(&c)
1009        || ('\u{FDF0}'..='\u{FFFD}').contains(&c)
1010        || ('\u{10000}'..='\u{EFFFF}').contains(&c)
1011        || c == '-'
1012        || c == '.'
1013        || c.is_ascii_digit()
1014        || c == '\u{B7}'
1015        || ('\u{0300}'..='\u{036F}').contains(&c)
1016        || ('\u{203F}'..='\u{2040}').contains(&c)
1017}
1018
1019// generate an id compatible string, replacing all none ID chars to underscores
1020fn to_id(s: &str) -> String {
1021    "id_".to_string() + &s.replace(|c: char| !is_id_char(c), "_")
1022}