Skip to main content

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: Vec<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: vec![],
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" => {
447                let value = value.into();
448                if value.is_empty() {
449                    self.metadata.lang = vec![];
450                } else {
451                    self.metadata.lang.push(value.into())
452                }
453            }
454            "direction" => self.metadata.direction = PageDirection::from_str(&value.into())?,
455            "generator" => self.metadata.generator = value.into(),
456            "description" => {
457                let value = value.into();
458                if value.is_empty() {
459                    self.metadata.description = vec![];
460                } else {
461                    self.metadata.description.push(value);
462                }
463            }
464            "subject" => {
465                let value = value.into();
466                if value.is_empty() {
467                    self.metadata.subject = vec![];
468                } else {
469                    self.metadata.subject.push(value);
470                }
471            }
472            "license" => self.metadata.license = Some(value.into()),
473            "toc_name" => self.metadata.toc_name = value.into(),
474            s => Err(crate::Error::InvalidMetadataError(s.to_string()))?,
475        }
476        Ok(self)
477    }
478
479    /// Sets the authors of the EPUB
480    pub fn set_authors(&mut self, value: Vec<String>) {
481        self.metadata.author = value;
482    }
483
484    /// Add an author to the EPUB
485    pub fn add_author<S: Into<String>>(&mut self, value: S) {
486        self.metadata.author.push(value.into());
487    }
488
489    /// Remove all authors from EPUB
490    pub fn clear_authors<S: Into<String>>(&mut self) {
491        self.metadata.author.clear();
492    }
493
494    /// Sets the title of the EPUB
495    pub fn set_title<S: Into<String>>(&mut self, value: S) {
496        self.metadata.title = value.into();
497    }
498
499    /// Tells whether fields should be HTML-escaped.
500    ///
501    /// * `true`: fields such as titles, description, and so on will be HTML-escaped everywhere (default)
502    /// * `false`: fields will be left as is (letting you in charge of making
503    /// sure they do not contain anything illegal, e.g. < and > characters)
504    pub fn escape_html(&mut self, val: bool) {
505        self.escape_html = val;
506    }
507
508    /// Sets a single language of the EPUB
509    #[deprecated(since = "0.8.3", note = "Use set_languages or add_language instead")]
510    pub fn set_lang<S: Into<String>>(&mut self, value: S) {
511        self.clear_languages();
512        self.add_language(value);
513    }
514
515    /// Sets the languages of the EPUB
516    pub fn set_languages(&mut self, value: Vec<String>) {
517        self.metadata.lang = value;
518    }
519
520    /// Adds a language of the EPUB
521    ///
522    /// This is quite important as EPUB renderers rely on it
523    /// for e.g. hyphenating words.
524    pub fn add_language<S: Into<String>>(&mut self, value: S) {
525        self.metadata.lang.push(value.into());
526    }
527
528    /// Remove all languages from EPUB
529    pub fn clear_languages(&mut self) {
530        self.metadata.lang.clear();
531    }
532
533    /// Sets the generator of the book (should be your program name)
534    pub fn set_generator<S: Into<String>>(&mut self, value: S) {
535        self.metadata.generator = value.into();
536    }
537
538    /// Sets the name to use for table of contents. This is by default, "Table of Contents"
539    pub fn set_toc_name<S: Into<String>>(&mut self, value: S) {
540        self.metadata.toc_name = value.into();
541    }
542
543    /// Sets and replaces the description of the EPUB
544    pub fn set_description(&mut self, value: Vec<String>) {
545        self.metadata.description = value;
546    }
547
548    /// Adds a line to the EPUB description
549    pub fn add_description<S: Into<String>>(&mut self, value: S) {
550        self.metadata.description.push(value.into());
551    }
552
553    /// Remove all description paragraphs from EPUB
554    pub fn clear_description(&mut self) {
555        self.metadata.description.clear();
556    }
557
558    /// Sets and replaces the subjects of the EPUB
559    pub fn set_subjects(&mut self, value: Vec<String>) {
560        self.metadata.subject = value;
561    }
562
563    /// Adds a value to the subjects
564    pub fn add_subject<S: Into<String>>(&mut self, value: S) {
565        self.metadata.subject.push(value.into());
566    }
567
568    /// Remove all the subjects from EPUB
569    pub fn clear_subjects(&mut self) {
570        self.metadata.subject.clear();
571    }
572
573    /// Sets the license under which this EPUB is distributed
574    pub fn set_license<S: Into<String>>(&mut self, value: S) {
575        self.metadata.license = Some(value.into());
576    }
577
578    /// Sets the publication date of the EPUB
579    pub fn set_publication_date(&mut self, date_published: chrono::DateTime<chrono::Utc>) {
580        self.metadata.date_published = Some(date_published);
581    }
582    /// Sets the date on which the EPUB was last modified.
583    ///
584    /// This value is part of the metadata. If this function is not called, the time at the
585    /// moment of generation will be used instead.
586    pub fn set_modified_date(&mut self, date_modified: chrono::DateTime<chrono::Utc>) {
587        self.metadata.date_modified = Some(date_modified);
588    }
589    /// Sets the uuid used for the EPUB.
590    ///
591    /// This is useful for reproducibly generating epubs.
592    pub fn set_uuid(&mut self, uuid: uuid::Uuid) {
593        self.metadata.uuid = Some(uuid);
594    }
595
596    /// Sets stylesheet of the EPUB.
597    ///
598    /// This content will be written in a `stylesheet.css` file; it is used by
599    /// some pages (such as nav.xhtml), you don't have use it in your documents though it
600    /// makes sense to also do so.
601    pub fn stylesheet<R: Read>(&mut self, content: R) -> Result<&mut Self> {
602        self.add_resource("stylesheet.css", content, "text/css")?;
603        self.stylesheet = true;
604        Ok(self)
605    }
606
607    /// Adds an inline toc in the document.
608    ///
609    /// If this method is called it adds a page that contains the table of contents
610    /// that appears in the document.
611    ///
612    /// The position where this table of contents will be inserted depends on when
613    /// you call this method: if you call it before adding any content, it will be
614    /// at the beginning, if you call it after, it will be at the end.
615    pub fn inline_toc(&mut self) -> &mut Self {
616        self.inline_toc = true;
617        self.toc.add(TocElement::new(
618            "toc.xhtml",
619            self.metadata.toc_name.as_str(),
620        ));
621        let mut file = Content::new("toc.xhtml", "application/xhtml+xml");
622        file.reftype = Some(ReferenceType::Toc);
623        file.title = self.metadata.toc_name.clone();
624        file.itemref = true;
625        self.files.push(file);
626        self
627    }
628
629    /// Add a resource to the EPUB file
630    ///
631    /// This resource can be a picture, a font, some CSS file, .... Unlike
632    /// `add_content`, files added this way won't appear in the linear
633    /// document.
634    ///
635    /// Note that these files will automatically be inserted into an `OEBPS` directory,
636    /// so you don't need (and shouldn't) prefix your path with `OEBPS/`.
637    ///
638    /// # Arguments
639    ///
640    /// * `path`: the path where this file will be written in the EPUB OEBPS structure,
641    ///   e.g. `data/image_0.png`
642    /// * `content`: the resource to include
643    /// * `mime_type`: the mime type of this file, e.g. "image/png".
644    pub fn add_resource<R, P, S>(&mut self, path: P, content: R, mime_type: S) -> Result<&mut Self>
645    where
646        R: Read,
647        P: AsRef<Path>,
648        S: Into<String>,
649    {
650        self.zip
651            .write_file(Path::new("OEBPS").join(path.as_ref()), content)?;
652        log::debug!("Add resource: {:?}", path.as_ref().display());
653        self.files.push(Content::new(
654            format!("{}", path.as_ref().display()),
655            mime_type,
656        ));
657        Ok(self)
658    }
659
660    /// Add a cover image to the EPUB.
661    ///
662    /// This works similarly to adding the image as a resource with the `add_resource`
663    /// method, except, it signals it in the Manifest section so it is displayed as the
664    /// cover by Ereaders
665    pub fn add_cover_image<R, P, S>(
666        &mut self,
667        path: P,
668        content: R,
669        mime_type: S,
670    ) -> Result<&mut Self>
671    where
672        R: Read,
673        P: AsRef<Path>,
674        S: Into<String>,
675    {
676        self.zip
677            .write_file(Path::new("OEBPS").join(path.as_ref()), content)?;
678        let mut file = Content::new(format!("{}", path.as_ref().display()), mime_type);
679        file.cover = true;
680        self.files.push(file);
681        Ok(self)
682    }
683
684    /// Add a XHTML content file that will be added to the EPUB.
685    ///
686    /// # Examples
687    ///
688    /// ```
689    /// # use epub_builder::{EpubBuilder, ZipLibrary, EpubContent};
690    /// let content = "Some content";
691    /// let mut builder = EpubBuilder::new(ZipLibrary::new().unwrap()).unwrap();
692    /// // Add a chapter that won't be added to the Table of Contents
693    /// builder.add_content(EpubContent::new("intro.xhtml", content.as_bytes())).unwrap();
694    /// ```
695    ///
696    /// ```
697    /// # use epub_builder::{EpubBuilder, ZipLibrary, EpubContent, TocElement};
698    /// # let mut builder = EpubBuilder::new(ZipLibrary::new().unwrap()).unwrap();
699    /// # let content = "Some content";
700    /// // Sets the title of a chapter so it is added to the Table of contents
701    /// // Also add information about its structure
702    /// builder.add_content(EpubContent::new("chapter_1.xhtml", content.as_bytes())
703    ///                      .title("Chapter 1")
704    ///                      .child(TocElement::new("chapter_1.xhtml#1", "1.1"))).unwrap();
705    /// ```
706    ///
707    /// ```
708    /// # use epub_builder::{EpubBuilder, ZipLibrary, EpubContent};
709    /// # let mut builder = EpubBuilder::new(ZipLibrary::new().unwrap()).unwrap();
710    /// # let content = "Some content";
711    /// // Add a section, by setting the level to 2 (instead of the default value 1)
712    /// builder.add_content(EpubContent::new("section.xhtml", content.as_bytes())
713    ///                      .title("Section 1")
714    ///                      .level(2)).unwrap();
715    /// ```
716    ///
717    /// Note that these files will automatically be inserted into an `OEBPS` directory,
718    /// so you don't need (and shouldn't) prefix your path with `OEBPS/`.
719    ///
720    /// # See also
721    ///
722    /// * [`EpubContent`](struct.EpubContent.html)
723    /// * the `add_resource` method, to add other resources in the EPUB file.
724    pub fn add_content<R: Read>(&mut self, content: EpubContent<R>) -> Result<&mut Self> {
725        self.zip.write_file(
726            Path::new("OEBPS").join(content.toc.url.as_str()),
727            content.content,
728        )?;
729        let mut file = Content::new(content.toc.url.as_str(), "application/xhtml+xml");
730        file.itemref = true;
731        file.reftype = content.reftype;
732        if file.reftype.is_some() {
733            file.title = content.toc.title.clone();
734        }
735        self.files.push(file);
736        if !content.toc.title.is_empty() {
737            self.toc.add(content.toc);
738        }
739        Ok(self)
740    }
741
742    /// Generate the EPUB file and write it to the writer
743    ///
744    /// # Example
745    ///
746    /// ```
747    /// # use epub_builder::{EpubBuilder, ZipLibrary};
748    /// let mut builder = EpubBuilder::new(ZipLibrary::new().unwrap()).unwrap();
749    /// // Write the EPUB file into a Vec<u8>
750    /// let mut epub: Vec<u8> = vec!();
751    /// builder.generate(&mut epub).unwrap();
752    /// ```
753    pub fn generate<W: io::Write>(mut self, to: W) -> Result<()> {
754        // If no styleesheet was provided, generate a dummy one
755        if !self.stylesheet {
756            self.stylesheet(b"".as_ref())?;
757        }
758        // Render content.opf
759        let bytes = self.render_opf()?;
760        self.zip.write_file("OEBPS/content.opf", &*bytes)?;
761        // Render toc.ncx
762        let bytes = self.render_toc()?;
763        self.zip.write_file("OEBPS/toc.ncx", &*bytes)?;
764        // Render nav.xhtml
765        let bytes = self.render_nav(true)?;
766        self.zip.write_file("OEBPS/nav.xhtml", &*bytes)?;
767        // Write inline toc if it needs to
768        if self.inline_toc {
769            let bytes = self.render_nav(false)?;
770            self.zip.write_file("OEBPS/toc.xhtml", &*bytes)?;
771        }
772
773        self.zip.generate(to)?;
774        Ok(())
775    }
776
777    /// Render content.opf file
778    fn render_opf(&mut self) -> Result<Vec<u8>> {
779        log::debug!("render_opf...");
780        let mut optional: Vec<String> = Vec::new();
781        for desc in &self.metadata.description {
782            optional.push(format!(
783                "<dc:description>{}</dc:description>",
784                common::encode_html(desc, self.escape_html),
785            ));
786        }
787        for subject in &self.metadata.subject {
788            optional.push(format!(
789                "<dc:subject>{}</dc:subject>",
790                common::encode_html(subject, self.escape_html),
791            ));
792        }
793        if let Some(ref rights) = self.metadata.license {
794            optional.push(format!(
795                "<dc:rights>{}</dc:rights>",
796                common::encode_html(rights, self.escape_html),
797            ));
798        }
799
800        for meta in &self.meta_opf {
801            optional.push(meta.render_opf(self.escape_html))
802        }
803
804        let date_modified = self
805            .metadata
806            .date_modified
807            .unwrap_or_else(chrono::Utc::now)
808            .format("%Y-%m-%dT%H:%M:%SZ");
809        let date_published = self
810            .metadata
811            .date_published
812            .map(|date| date.format("%Y-%m-%dT%H:%M:%SZ"));
813        let uuid = uuid::fmt::Urn::from_uuid(self.metadata.uuid.unwrap_or_else(uuid::Uuid::new_v4))
814            .to_string();
815
816        let mut items: Vec<String> = Vec::new();
817        let mut itemrefs: Vec<String> = Vec::new();
818        let mut guide: Vec<String> = Vec::new();
819
820        for content in &self.files {
821            let id = if content.cover {
822                String::from("cover-image")
823            } else {
824                to_id(&content.file)
825            };
826            let properties = match (self.version, content.cover) {
827                (EpubVersion::V30, true) => "properties=\"cover-image\" ",
828                (EpubVersion::V33, true) => "properties=\"cover-image\" ",
829                _ => "",
830            };
831            if content.cover {
832                optional.push("<meta name=\"cover\" content=\"cover-image\"/>".to_string());
833            }
834            log::debug!("id={:?}, mime={:?}", id, content.mime);
835            items.push(format!(
836                "<item media-type=\"{mime}\" {properties}\
837                        id=\"{id}\" href=\"{href}\"/>",
838                properties = properties, // Not escaped: XML attributes above
839                mime = html_escape::encode_double_quoted_attribute(&content.mime),
840                id = html_escape::encode_double_quoted_attribute(&id),
841                // in the zip the path is always with forward slashes, on windows it is with backslashes
842                href =
843                    html_escape::encode_double_quoted_attribute(&content.file.replace('\\', "/")),
844            ));
845            if content.itemref {
846                itemrefs.push(format!(
847                    "<itemref idref=\"{id}\"/>",
848                    id = html_escape::encode_double_quoted_attribute(&id),
849                ));
850            }
851            if let Some(reftype) = content.reftype {
852                use crate::ReferenceType::*;
853                let reftype = match reftype {
854                    Cover => "cover",
855                    TitlePage => "title-page",
856                    Toc => "toc",
857                    Index => "index",
858                    Glossary => "glossary",
859                    Acknowledgements => "acknowledgements",
860                    Bibliography => "bibliography",
861                    Colophon => "colophon",
862                    Copyright => "copyright",
863                    Dedication => "dedication",
864                    Epigraph => "epigraph",
865                    Foreword => "foreword",
866                    Loi => "loi",
867                    Lot => "lot",
868                    Notes => "notes",
869                    Preface => "preface",
870                    Text => "text",
871                };
872                log::debug!("content = {:?}", &content);
873                guide.push(format!(
874                    "<reference type=\"{reftype}\" title=\"{title}\" href=\"{href}\"/>",
875                    reftype = html_escape::encode_double_quoted_attribute(&reftype),
876                    title = html_escape::encode_double_quoted_attribute(&content.title),
877                    href = html_escape::encode_double_quoted_attribute(&content.file),
878                ));
879            }
880        }
881
882        let data = {
883            let mut authors: Vec<_> = vec![];
884            for (i, author) in self.metadata.author.iter().enumerate() {
885                let author = upon::value! {
886                    id_attr: html_escape::encode_double_quoted_attribute(&i.to_string()),
887                    name: common::encode_html(author, self.escape_html)
888                };
889                authors.push(author);
890            }
891            let mut languages: Vec<_> = vec![];
892            for (i, lang) in self.metadata.lang.iter().enumerate() {
893                let lang = upon::value! {
894                    id_attr: html_escape::encode_double_quoted_attribute(&i.to_string()),
895                    name: common::encode_html(lang, self.escape_html)
896                };
897                languages.push(lang);
898            }
899            upon::value! {
900                author: authors,
901                lang: languages,
902                direction: self.metadata.direction.to_string(),
903                title: common::encode_html(&self.metadata.title, self.escape_html),
904                generator_attr: html_escape::encode_double_quoted_attribute(&self.metadata.generator),
905                toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
906                toc_name_attr: html_escape::encode_double_quoted_attribute(&self.metadata.toc_name),
907                optional: common::indent(optional.join("\n"), 2),
908                items: common::indent(items.join("\n"), 2), // Not escaped: XML content
909                itemrefs: common::indent(itemrefs.join("\n"), 2), // Not escaped: XML content
910                date_modified: html_escape::encode_text(&date_modified.to_string()),
911                uuid: html_escape::encode_text(&uuid),
912                guide: common::indent(guide.join("\n"), 2), // Not escaped: XML content
913                date_published: if let Some(date) = date_published { date.to_string() } else { String::new() },
914            }
915        };
916
917        let mut res: Vec<u8> = vec![];
918        match self.version {
919            EpubVersion::V20 => templates::v2::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
920            EpubVersion::V30 => templates::v3::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
921            EpubVersion::V33 => templates::v3::CONTENT_OPF.render(&Engine::new(), &data).to_writer(&mut res),
922        }
923        .map_err(|e| crate::Error::TemplateError {
924            msg: "could not render template for content.opf".to_string(),
925            cause: e.into(),
926        })?;
927        //.wrap_err("could not render template for content.opf")?;
928
929        Ok(res)
930    }
931
932    /// Render toc.ncx
933    fn render_toc(&mut self) -> Result<Vec<u8>> {
934        let mut nav_points = String::new();
935
936        nav_points.push_str(&self.toc.render_epub(self.escape_html));
937
938        let data = upon::value! {
939            toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
940            nav_points: nav_points
941        };
942        let mut res: Vec<u8> = vec![];
943        templates::TOC_NCX
944            .render(&Engine::new(), &data)
945            .to_writer(&mut res)
946            .map_err(|e| crate::Error::TemplateError {
947                msg: "error rendering toc.ncx template".to_string(),
948                cause: e.into(),
949            })?;
950        Ok(res)
951    }
952
953    /// Render nav.xhtml
954    fn render_nav(&mut self, numbered: bool) -> Result<Vec<u8>> {
955        let content = self.toc.render(numbered, self.escape_html);
956        let mut landmarks: Vec<String> = Vec::new();
957        if self.version > EpubVersion::V20 {
958            for file in &self.files {
959                if let Some(ref reftype) = file.reftype {
960                    use ReferenceType::*;
961                    let reftype = match *reftype {
962                        Cover => "cover",
963                        Text => "bodymatter",
964                        Toc => "toc",
965                        Bibliography => "bibliography",
966                        Epigraph => "epigraph",
967                        Foreword => "foreword",
968                        Preface => "preface",
969                        Notes => "endnotes",
970                        Loi => "loi",
971                        Lot => "lot",
972                        Colophon => "colophon",
973                        TitlePage => "titlepage",
974                        Index => "index",
975                        Glossary => "glossary",
976                        Copyright => "copyright-page",
977                        Acknowledgements => "acknowledgements",
978                        Dedication => "dedication",
979                    };
980                    if !file.title.is_empty() {
981                        landmarks.push(format!(
982                            "<li><a epub:type=\"{reftype}\" href=\"{href}\">\
983                                {title}</a></li>",
984                            reftype = html_escape::encode_double_quoted_attribute(&reftype),
985                            href = html_escape::encode_double_quoted_attribute(&file.file),
986                            title = common::encode_html(&file.title, self.escape_html),
987                        ));
988                    }
989                }
990            }
991        }
992
993        let data = upon::value! {
994            content: content, // Not escaped: XML content
995            toc_name: common::encode_html(&self.metadata.toc_name, self.escape_html),
996            generator_attr: html_escape::encode_double_quoted_attribute(&self.metadata.generator),
997            landmarks: if !landmarks.is_empty() {
998                common::indent(
999                    format!(
1000                        "<ol>\n{}\n</ol>",
1001                        common::indent(landmarks.join("\n"), 1), // Not escaped: XML content
1002                    ),
1003                    2,
1004                )
1005            } else {
1006                String::new()
1007            },
1008        };
1009
1010        let mut res: Vec<u8> = vec![];
1011        match self.version {
1012            EpubVersion::V20 => templates::v2::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
1013            EpubVersion::V30 => templates::v3::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
1014            EpubVersion::V33 => templates::v3::NAV_XHTML.render(&Engine::new(), &data).to_writer(&mut res),
1015        }
1016        .map_err(|e| crate::Error::TemplateError {
1017            msg: "error rendering nav.xhtml template".to_string(),
1018            cause: e.into(),
1019        })?;
1020        Ok(res)
1021    }
1022}
1023
1024// The actual rules for ID are here - https://www.w3.org/TR/xml-names11/#NT-NCNameChar
1025// Ordering to to look as similar as possible to the W3 Recommendation ruleset
1026// Slightly more permissive, there are some that are invalid start chars, but this is ok.
1027fn is_id_char(c: char) -> bool {
1028    c.is_ascii_uppercase()
1029        || c == '_'
1030        || c.is_ascii_lowercase()
1031        || ('\u{C0}'..='\u{D6}').contains(&c)
1032        || ('\u{D8}'..='\u{F6}').contains(&c)
1033        || ('\u{F8}'..='\u{2FF}').contains(&c)
1034        || ('\u{370}'..='\u{37D}').contains(&c)
1035        || ('\u{37F}'..='\u{1FFF}').contains(&c)
1036        || ('\u{200C}'..='\u{200D}').contains(&c)
1037        || ('\u{2070}'..='\u{218F}').contains(&c)
1038        || ('\u{2C00}'..='\u{2FEF}').contains(&c)
1039        || ('\u{3001}'..='\u{D7FF}').contains(&c)
1040        || ('\u{F900}'..='\u{FDCF}').contains(&c)
1041        || ('\u{FDF0}'..='\u{FFFD}').contains(&c)
1042        || ('\u{10000}'..='\u{EFFFF}').contains(&c)
1043        || c == '-'
1044        || c == '.'
1045        || c.is_ascii_digit()
1046        || c == '\u{B7}'
1047        || ('\u{0300}'..='\u{036F}').contains(&c)
1048        || ('\u{203F}'..='\u{2040}').contains(&c)
1049}
1050
1051// generate an id compatible string, replacing all none ID chars to underscores
1052fn to_id(s: &str) -> String {
1053    "id_".to_string() + &s.replace(|c: char| !is_id_char(c), "_")
1054}