Skip to main content

lib_epub/
types.rs

1//! Types and data structures for EPUB processing
2//!
3//! This module defines all the core data structures used throughout the EPUB library.
4//! These structures represent the various components of an EPUB publication according to
5//! the EPUB specification, including metadata, manifest items, spine items, navigation points,
6//! and encryption information.
7//!
8//! The types in this module are designed to be compatible with both EPUB 2 and EPUB 3
9//! specifications, providing a unified interface for working with different versions
10//! of EPUB publications.
11//!
12//! ## Main Components
13//!
14//! - [MetadataItem] - Represents metadata entries in the publication
15//! - [MetadataRefinement] - Additional details for metadata items (EPUB 3.x)
16//! - [MetadataLinkItem] - Links to external metadata resources
17//! - [ManifestItem] - Resources declared in the publication manifest
18//! - [SpineItem] - Items defining the reading order
19//! - [NavPoint] - Navigation points in the table of contents
20//! - [EncryptionData] - Information about encrypted resources
21//!
22//! ## Builder Pattern
23//!
24//! Many of these types implement a builder pattern for easier construction when the
25//! `builder` feature is enabled. See individual type documentation for details.
26
27use std::path::PathBuf;
28
29#[cfg(feature = "builder")]
30use crate::{
31    error::{EpubBuilderError, EpubError},
32    utils::ELEMENT_IN_DC_NAMESPACE,
33};
34
35/// Represents the EPUB version
36///
37/// This enum is used to distinguish between different versions of the EPUB specification.
38#[derive(Debug, PartialEq, Eq)]
39pub enum EpubVersion {
40    Version2_0,
41    Version3_0,
42}
43
44/// Represents a metadata item in the EPUB publication
45///
46/// The `MetadataItem` structure represents a single piece of metadata from the EPUB publication.
47/// Metadata items contain information about the publication such as title, author, identifier,
48/// language, and other descriptive information.
49///
50/// In EPUB 3.0, metadata items can have refinements that provide additional details about
51/// the main metadata item. For example, a title metadata item might have refinements that
52/// specify it is the main title of the publication.
53///
54/// ## Builder Methods
55///
56/// When the `builder` feature is enabled, this struct provides convenient builder methods:
57///
58/// ```rust
59/// # #[cfg(feature = "builder")] {
60/// use lib_epub::types::MetadataItem;
61///
62/// let metadata = MetadataItem::new("title", "Sample Book")
63///     .with_id("title-1")
64///     .with_lang("en")
65///     .build();
66/// # }
67/// ```
68#[derive(Debug, Clone)]
69pub struct MetadataItem {
70    /// Optional unique identifier for this metadata item
71    ///
72    /// Used to reference this metadata item from other elements or refinements.
73    /// In EPUB 3.0, this ID is particularly important for linking with metadata refinements.
74    pub id: Option<String>,
75
76    /// The metadata property name
77    ///
78    /// This field specifies the type of metadata this item represents. Common properties
79    /// include "title", "creator", "identifier", "language", "publisher", etc.
80    /// These typically correspond to Dublin Core metadata terms.
81    pub property: String,
82
83    /// The metadata value
84    pub value: String,
85
86    /// Optional language code for this metadata item
87    pub lang: Option<String>,
88
89    /// Refinements of this metadata item
90    ///
91    /// In EPUB 3.x, metadata items can have associated refinements that provide additional
92    /// information about the main metadata item. For example, a creator metadata item might
93    /// have refinements specifying the creator's role (author, illustrator, etc.) or file-as.
94    ///
95    /// In EPUB 2.x, metadata items may contain custom attributes, which will also be parsed as refinement.
96    pub refined: Vec<MetadataRefinement>,
97}
98
99#[cfg(feature = "builder")]
100impl MetadataItem {
101    /// Creates a new metadata item with the given property and value
102    ///
103    /// Requires the `builder` feature.
104    ///
105    /// ## Parameters
106    /// - `property` - The metadata property name (e.g., "title", "creator")
107    /// - `value` - The metadata value
108    pub fn new(property: &str, value: &str) -> Self {
109        Self {
110            id: None,
111            property: property.to_string(),
112            value: value.to_string(),
113            lang: None,
114            refined: vec![],
115        }
116    }
117
118    /// Sets the ID of the metadata item
119    ///
120    /// Requires the `builder` feature.
121    ///
122    /// ## Parameters
123    /// - `id` - The ID to assign to this metadata item
124    pub fn with_id(&mut self, id: &str) -> &mut Self {
125        self.id = Some(id.to_string());
126        self
127    }
128
129    /// Sets the language of the metadata item
130    ///
131    /// Requires the `builder` feature.
132    ///
133    /// ## Parameters
134    /// - `lang` - The language code (e.g., "en", "fr", "zh-CN")
135    pub fn with_lang(&mut self, lang: &str) -> &mut Self {
136        self.lang = Some(lang.to_string());
137        self
138    }
139
140    /// Adds a refinement to this metadata item
141    ///
142    /// Requires the `builder` feature.
143    ///
144    /// ## Parameters
145    /// - `refine` - The refinement to add
146    ///
147    /// ## Notes
148    /// - The metadata item must have an ID for refinements to be added.
149    pub fn append_refinement(&mut self, refine: MetadataRefinement) -> &mut Self {
150        if self.id.is_some() {
151            self.refined.push(refine);
152        } else {
153            // TODO: alert warning
154        }
155
156        self
157    }
158
159    /// Builds the final metadata item
160    ///
161    /// Requires the `builder` feature.
162    pub fn build(&self) -> Self {
163        Self { ..self.clone() }
164    }
165
166    /// Gets the XML attributes for this metadata item
167    pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
168        let mut attributes = Vec::new();
169
170        if !ELEMENT_IN_DC_NAMESPACE.contains(&self.property.as_str()) {
171            attributes.push(("property", self.property.as_str()));
172        }
173
174        if let Some(id) = &self.id {
175            attributes.push(("id", id.as_str()));
176        };
177
178        if let Some(lang) = &self.lang {
179            attributes.push(("lang", lang.as_str()));
180        };
181
182        attributes
183    }
184}
185
186/// Represents a refinement of a metadata item in an EPUB 3.0 publication
187///
188/// The `MetadataRefinement` structure provides additional details about a parent metadata item.
189/// Refinements are used in EPUB 3.0 to add granular metadata information that would be difficult
190/// to express with the basic metadata structure alone.
191///
192/// For example, a creator metadata item might have refinements specifying the creator's role
193/// or the scheme used for an identifier.
194///
195/// ## Builder Methods
196///
197/// When the `builder` feature is enabled, this struct provides convenient builder methods:
198///
199/// ```rust
200/// # #[cfg(feature = "builder")] {
201/// use lib_epub::types::MetadataRefinement;
202///
203/// let refinement = MetadataRefinement::new("creator-1", "role", "author")
204///     .with_lang("en")
205///     .with_scheme("marc:relators")
206///     .build();
207/// # }
208/// ```
209#[derive(Debug, Clone)]
210pub struct MetadataRefinement {
211    pub refines: String,
212
213    /// The refinement property name
214    ///
215    /// Specifies what aspect of the parent metadata item this refinement describes.
216    /// Common refinement properties include "role", "file-as", "alternate-script", etc.
217    pub property: String,
218
219    /// The refinement value
220    pub value: String,
221
222    /// Optional language code for this refinement
223    pub lang: Option<String>,
224
225    /// Optional scheme identifier for this refinement
226    ///
227    /// Specifies the vocabulary or scheme used for the refinement value. For example,
228    /// "marc:relators" for MARC relator codes, or "onix:codelist5" for ONIX roles.
229    pub scheme: Option<String>,
230}
231
232#[cfg(feature = "builder")]
233impl MetadataRefinement {
234    /// Creates a new metadata refinement
235    ///
236    /// Requires the `builder` feature.
237    ///
238    /// ## Parameters
239    /// - `refines` - The ID of the metadata item being refined
240    /// - `property` - The refinement property name
241    /// - `value` - The refinement value
242    pub fn new(refines: &str, property: &str, value: &str) -> Self {
243        Self {
244            refines: refines.to_string(),
245            property: property.to_string(),
246            value: value.to_string(),
247            lang: None,
248            scheme: None,
249        }
250    }
251
252    /// Sets the language of the refinement
253    ///
254    /// Requires the `builder` feature.
255    ///
256    /// ## Parameters
257    /// - `lang` - The language code
258    pub fn with_lang(&mut self, lang: &str) -> &mut Self {
259        self.lang = Some(lang.to_string());
260        self
261    }
262
263    /// Sets the scheme of the refinement
264    ///
265    /// Requires the `builder` feature.
266    ///
267    /// ## Parameters
268    /// - `scheme` - The scheme identifier
269    pub fn with_scheme(&mut self, scheme: &str) -> &mut Self {
270        self.scheme = Some(scheme.to_string());
271        self
272    }
273
274    /// Builds the final metadata refinement
275    ///
276    /// Requires the `builder` feature.
277    pub fn build(&self) -> Self {
278        Self { ..self.clone() }
279    }
280
281    /// Gets the XML attributes for this refinement
282    pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
283        let mut attributes = Vec::new();
284
285        attributes.push(("refines", self.refines.as_str()));
286        attributes.push(("property", self.property.as_str()));
287
288        if let Some(lang) = &self.lang {
289            attributes.push(("lang", lang.as_str()));
290        };
291
292        if let Some(scheme) = &self.scheme {
293            attributes.push(("scheme", scheme.as_str()));
294        };
295
296        attributes
297    }
298}
299
300/// Represents a metadata link item in an EPUB publication
301///
302/// The `MetadataLinkItem` structure represents a link from the publication's metadata to
303/// external resources. These links are typically used to associate the publication with
304/// external records, alternate editions, or related resources.
305///
306/// Link metadata items are defined in the OPF file using `<link>` elements in the metadata
307/// section and follow the EPUB 3.0 metadata link specification.
308#[derive(Debug)]
309pub struct MetadataLinkItem {
310    /// The URI of the linked resource
311    pub href: String,
312
313    /// The relationship between this publication and the linked resource
314    pub rel: String,
315
316    /// Optional language of the linked resource
317    pub hreflang: Option<String>,
318
319    /// Optional unique identifier for this link item
320    ///
321    /// Provides an ID that can be used to reference this link from other elements.
322    pub id: Option<String>,
323
324    /// Optional MIME type of the linked resource
325    pub mime: Option<String>,
326
327    /// Optional properties of this link
328    ///
329    /// Contains space-separated property values that describe characteristics of the link
330    /// or the linked resource. For example, "onix-3.0" to indicate an ONIX 3.0 record.
331    pub properties: Option<String>,
332
333    /// Optional reference to another metadata item
334    ///
335    /// In EPUB 3.0, links can refine other metadata items. This field contains the ID
336    /// of the metadata item that this link refines, prefixed with "#".
337    pub refines: Option<String>,
338}
339
340/// Represents a resource item declared in the EPUB manifest
341///
342/// The `ManifestItem` structure represents a single resource file declared in the EPUB
343/// publication's manifest. Each manifest item describes a resource that is part of the
344/// publication, including its location, media type, and optional properties or fallback
345/// relationships.
346///
347/// The manifest serves as a comprehensive inventory of all resources in an EPUB publication.
348/// Every resource that is part of the publication must be declared in the manifest, and
349/// resources not listed in the manifest should not be accessed by reading systems.
350///
351/// Manifest items support the fallback mechanism, allowing alternative versions of a resource
352/// to be specified. This is particularly important for foreign resources (resources with
353/// non-core media types) that may not be supported by all reading systems.
354///
355/// ## Builder Methods
356///
357/// When the `builder` feature is enabled, this struct provides convenient builder methods:
358///
359/// ```
360/// # #[cfg(feature = "builder")] {
361/// use lib_epub::types::ManifestItem;
362///
363/// let manifest_item = ManifestItem::new("cover", "images/cover.jpg")
364///     .unwrap()
365///     .append_property("cover-image")
366///     .with_fallback("cover-fallback")
367///     .build();
368/// # }
369/// ```
370#[derive(Debug, Clone)]
371pub struct ManifestItem {
372    /// The unique identifier for this resource item
373    pub id: String,
374
375    /// The path to the resource file within the EPUB container
376    ///
377    /// This field contains the normalized path to the resource file relative to the
378    /// root of the EPUB container. The path is processed during parsing to handle
379    /// various EPUB path conventions (absolute paths, relative paths, etc.).
380    pub path: PathBuf,
381
382    /// The media type of the resource
383    pub mime: String,
384
385    /// Optional properties associated with this resource
386    ///
387    /// This field contains a space-separated list of properties that apply to this
388    /// resource. Properties provide additional information about how the resource
389    /// should be treated.
390    pub properties: Option<String>,
391
392    /// Optional fallback resource identifier
393    ///
394    /// This field specifies the ID of another manifest item that serves as a fallback
395    /// for this resource. Fallbacks are used when a reading system does not support
396    /// the media type of the primary resource. The fallback chain allows publications
397    /// to include foreign resources while maintaining compatibility with older or
398    /// simpler reading systems.
399    ///
400    /// The value is the ID of another manifest item, which must exist in the manifest.
401    /// If `None`, this resource has no fallback.
402    pub fallback: Option<String>,
403}
404
405#[cfg(feature = "builder")]
406impl ManifestItem {
407    /// Creates a new manifest item
408    ///
409    /// Requires the `builder` feature.
410    ///
411    /// ## Parameters
412    /// - `id` - The unique identifier for this resource
413    /// - `path` - The path to the resource file
414    ///
415    /// ## Errors
416    /// Returns an error if the path starts with "../" which is not allowed.
417    pub fn new(id: &str, path: &str) -> Result<Self, EpubError> {
418        if path.starts_with("../") {
419            return Err(
420                EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() }.into(),
421            );
422        }
423
424        Ok(Self {
425            id: id.to_string(),
426            path: PathBuf::from(path),
427            mime: String::new(),
428            properties: None,
429            fallback: None,
430        })
431    }
432
433    /// Sets the MIME type of the manifest item
434    pub(crate) fn set_mime(self, mime: &str) -> Self {
435        Self {
436            id: self.id,
437            path: self.path,
438            mime: mime.to_string(),
439            properties: self.properties,
440            fallback: self.fallback,
441        }
442    }
443
444    /// Appends a property to the manifest item
445    ///
446    /// Requires the `builder` feature.
447    ///
448    /// ## Parameters
449    /// - `property` - The property to add
450    pub fn append_property(&mut self, property: &str) -> &mut Self {
451        let new_properties = if let Some(properties) = &self.properties {
452            format!("{} {}", properties, property)
453        } else {
454            property.to_string()
455        };
456
457        self.properties = Some(new_properties);
458        self
459    }
460
461    /// Sets the fallback for this manifest item
462    ///
463    /// Requires the `builder` feature.
464    ///
465    /// ## Parameters
466    /// - `fallback` - The ID of the fallback manifest item
467    pub fn with_fallback(&mut self, fallback: &str) -> &mut Self {
468        self.fallback = Some(fallback.to_string());
469        self
470    }
471
472    /// Builds the final manifest item
473    ///
474    /// Requires the `builder` feature.
475    pub fn build(&self) -> Self {
476        Self { ..self.clone() }
477    }
478
479    /// Gets the XML attributes for this manifest item
480    pub fn attributes(&self) -> Vec<(&str, &str)> {
481        let mut attributes = Vec::new();
482
483        attributes.push(("id", self.id.as_str()));
484        attributes.push(("href", self.path.to_str().unwrap()));
485        attributes.push(("media-type", self.mime.as_str()));
486
487        if let Some(properties) = &self.properties {
488            attributes.push(("properties", properties.as_str()));
489        }
490
491        if let Some(fallback) = &self.fallback {
492            attributes.push(("fallback", fallback.as_str()));
493        }
494
495        attributes
496    }
497}
498
499/// Represents an item in the EPUB spine, defining the reading order of the publication
500///
501/// The `SpineItem` structure represents a single item in the EPUB spine, which defines
502/// the linear reading order of the publication's content documents. Each spine item
503/// references a resource declared in the manifest and indicates whether it should be
504/// included in the linear reading sequence.
505///
506/// The spine is a crucial component of an EPUB publication as it determines the recommended
507/// reading order of content documents. Items can be marked as linear (part of the main reading
508/// flow) or non-linear (supplementary content that may be accessed out of sequence).
509///
510/// ## Builder Methods
511///
512/// When the `builder` feature is enabled, this struct provides convenient builder methods:
513///
514/// ```
515/// # #[cfg(feature = "builder")] {
516/// use lib_epub::types::SpineItem;
517///
518/// let spine_item = SpineItem::new("content-1")
519///     .with_id("spine-1")
520///     .append_property("page-spread-right")
521///     .set_linear(false)
522///     .build();
523/// # }
524/// ```
525#[derive(Debug, Clone)]
526pub struct SpineItem {
527    /// The ID reference to a manifest item
528    ///
529    /// This field contains the ID of the manifest item that this spine item references.
530    /// It establishes the connection between the reading order (spine) and the actual
531    /// content resources (manifest). The referenced ID must exist in the manifest.
532    pub idref: String,
533
534    /// Optional identifier for this spine item
535    pub id: Option<String>,
536
537    /// Optional properties associated with this spine item
538    ///
539    /// This field contains a space-separated list of properties that apply to this
540    /// spine item. These properties can indicate special handling requirements,
541    /// layout preferences, or other characteristics.
542    pub properties: Option<String>,
543
544    /// Indicates whether this item is part of the linear reading order
545    ///
546    /// When `true`, this spine item is part of the main linear reading sequence.
547    /// When `false`, this item represents supplementary content that may be accessed
548    /// out of the normal reading order (e.g., through hyperlinks).
549    ///
550    /// Non-linear items are typically used for content like footnotes, endnotes,
551    /// appendices, or other supplementary materials that readers might access
552    /// on-demand rather than sequentially.
553    pub linear: bool,
554}
555
556#[cfg(feature = "builder")]
557impl SpineItem {
558    /// Creates a new spine item referencing a manifest item
559    ///
560    /// Requires the `builder` feature.
561    ///
562    /// By default, spine items are linear.
563    ///
564    /// ## Parameters
565    /// - `idref` - The ID of the manifest item this spine item references
566    pub fn new(idref: &str) -> Self {
567        Self {
568            idref: idref.to_string(),
569            id: None,
570            properties: None,
571            linear: true,
572        }
573    }
574
575    /// Sets the ID of the spine item
576    ///
577    /// Requires the `builder` feature.
578    ///
579    /// ## Parameters
580    /// - `id` - The ID to assign to this spine item
581    pub fn with_id(&mut self, id: &str) -> &mut Self {
582        self.id = Some(id.to_string());
583        self
584    }
585
586    /// Appends a property to the spine item
587    ///
588    /// Requires the `builder` feature.
589    ///
590    /// ## Parameters
591    /// - `property` - The property to add
592    pub fn append_property(&mut self, property: &str) -> &mut Self {
593        let new_properties = if let Some(properties) = &self.properties {
594            format!("{} {}", properties, property)
595        } else {
596            property.to_string()
597        };
598
599        self.properties = Some(new_properties);
600        self
601    }
602
603    /// Sets whether this spine item is part of the linear reading order
604    ///
605    /// Requires the `builder` feature.
606    ///
607    /// ## Parameters
608    /// - `linear` - `true` if the item is part of the linear reading order, `false` otherwise
609    pub fn set_linear(&mut self, linear: bool) -> &mut Self {
610        self.linear = linear;
611        self
612    }
613
614    /// Builds the final spine item
615    ///
616    /// Requires the `builder` feature.
617    pub fn build(&self) -> Self {
618        Self { ..self.clone() }
619    }
620
621    /// Gets the XML attributes for this spine item
622    pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
623        let mut attributes = Vec::new();
624
625        attributes.push(("idref", self.idref.as_str()));
626        attributes.push(("linear", if self.linear { "yes" } else { "no" }));
627
628        if let Some(id) = &self.id {
629            attributes.push(("id", id.as_str()));
630        }
631
632        if let Some(properties) = &self.properties {
633            attributes.push(("properties", properties.as_str()));
634        }
635
636        attributes
637    }
638}
639
640/// Represents encryption information for EPUB resources
641///
642/// This structure holds information about encrypted resources in an EPUB publication,
643/// as defined in the META-INF/encryption.xml file according to the EPUB specification.
644/// It describes which resources are encrypted and what encryption method was used.
645#[derive(Debug, Clone)]
646pub struct EncryptionData {
647    /// The encryption algorithm URI
648    ///
649    /// This field specifies the encryption method used for the resource.
650    /// Supported encryption methods:
651    /// - IDPF font obfuscation: <http://www.idpf.org/2008/embedding>
652    /// - Adobe font obfuscation: <http://ns.adobe.com/pdf/enc#RC>
653    pub method: String,
654
655    /// The URI of the encrypted resource
656    ///
657    /// This field contains the path/URI to the encrypted resource within the EPUB container.
658    /// The path is relative to the root of the EPUB container.
659    pub data: String,
660}
661
662/// Represents a navigation point in an EPUB document's table of contents
663///
664/// The `NavPoint` structure represents a single entry in the hierarchical table of contents
665/// of an EPUB publication. Each navigation point corresponds to a section or chapter in
666/// the publication and may contain nested child navigation points to represent sub-sections.
667///
668/// ## Builder Methods
669///
670/// When the `builder` feature is enabled, this struct provides convenient builder methods:
671///
672/// ```
673/// # #[cfg(feature = "builder")] {
674/// use lib_epub::types::NavPoint;
675///
676/// let nav_point = NavPoint::new("Chapter 1")
677///     .with_content("chapter1.xhtml")
678///     .append_child(
679///         NavPoint::new("Section 1.1")
680///             .with_content("section1_1.xhtml")
681///             .build()
682///     )
683///     .build();
684/// # }
685/// ```
686#[derive(Debug, Eq, Clone)]
687pub struct NavPoint {
688    /// The display label/title of this navigation point
689    ///
690    /// This is the text that should be displayed to users in the table of contents.
691    pub label: String,
692
693    /// The content document path this navigation point references
694    ///
695    /// Can be `None` for navigation points that no relevant information was
696    /// provided in the original data.
697    pub content: Option<PathBuf>,
698
699    /// Child navigation points (sub-sections)
700    pub children: Vec<NavPoint>,
701
702    /// The reading order position of this navigation point
703    ///
704    /// It can be `None` for navigation points that no relevant information was
705    /// provided in the original data.
706    pub play_order: Option<usize>,
707}
708
709#[cfg(feature = "builder")]
710impl NavPoint {
711    /// Creates a new navigation point with the given label
712    ///
713    /// Requires the `builder` feature.
714    ///
715    /// ## Parameters
716    /// - `label` - The display label for this navigation point
717    pub fn new(label: &str) -> Self {
718        Self {
719            label: label.to_string(),
720            content: None,
721            children: vec![],
722            play_order: None,
723        }
724    }
725
726    /// Sets the content path for this navigation point
727    ///
728    /// Requires the `builder` feature.
729    ///
730    /// ## Parameters
731    /// - `content` - The path to the content document
732    pub fn with_content(&mut self, content: &str) -> &mut Self {
733        self.content = Some(PathBuf::from(content));
734        self
735    }
736
737    /// Appends a child navigation point
738    ///
739    /// Requires the `builder` feature.
740    ///
741    /// ## Parameters
742    /// - `child` - The child navigation point to add
743    pub fn append_child(&mut self, child: NavPoint) -> &mut Self {
744        self.children.push(child);
745        self
746    }
747
748    /// Sets all child navigation points
749    ///
750    /// Requires the `builder` feature.
751    ///
752    /// ## Parameters
753    /// - `children` - Vector of child navigation points
754    pub fn set_children(&mut self, children: Vec<NavPoint>) -> &mut Self {
755        self.children = children;
756        self
757    }
758
759    /// Builds the final navigation point
760    ///
761    /// Requires the `builder` feature.
762    pub fn build(&self) -> Self {
763        Self { ..self.clone() }
764    }
765}
766
767impl Ord for NavPoint {
768    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
769        self.play_order.cmp(&other.play_order)
770    }
771}
772
773impl PartialOrd for NavPoint {
774    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
775        Some(self.cmp(other))
776    }
777}
778
779impl PartialEq for NavPoint {
780    fn eq(&self, other: &Self) -> bool {
781        self.play_order == other.play_order
782    }
783}
784
785/// Represents a footnote in an content document
786///
787/// This structure represents a footnote in an EPUB content document.
788/// It contains the location within the content document and the content of the footnote.
789#[cfg(feature = "content_builder")]
790#[derive(Debug, Clone, Eq, PartialEq)]
791pub struct Footnote {
792    pub locate: usize,
793    pub content: String,
794}
795
796#[cfg(feature = "content_builder")]
797impl Ord for Footnote {
798    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
799        self.locate.cmp(&other.locate)
800    }
801}
802
803#[cfg(feature = "content_builder")]
804impl PartialOrd for Footnote {
805    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
806        Some(self.cmp(other))
807    }
808}
809
810/// Represents the type of a block element in the content document
811#[cfg(feature = "content_builder")]
812pub enum BlockType {
813    Text,
814    Quote,
815    Title,
816    Image,
817    Audio,
818    Video,
819    MathML,
820}
821
822#[cfg(test)]
823mod tests {
824    mod navpoint_tests {
825        use std::path::PathBuf;
826
827        use crate::types::NavPoint;
828
829        /// Testing the equality comparison of NavPoint
830        #[test]
831        fn test_navpoint_partial_eq() {
832            let nav1 = NavPoint {
833                label: "Chapter 1".to_string(),
834                content: Some(PathBuf::from("chapter1.html")),
835                children: vec![],
836                play_order: Some(1),
837            };
838
839            let nav2 = NavPoint {
840                label: "Chapter 1".to_string(),
841                content: Some(PathBuf::from("chapter2.html")),
842                children: vec![],
843                play_order: Some(1),
844            };
845
846            let nav3 = NavPoint {
847                label: "Chapter 2".to_string(),
848                content: Some(PathBuf::from("chapter1.html")),
849                children: vec![],
850                play_order: Some(2),
851            };
852
853            assert_eq!(nav1, nav2); // Same play_order, different contents, should be equal
854            assert_ne!(nav1, nav3); // Different play_order, Same contents, should be unequal
855        }
856
857        /// Test NavPoint sorting comparison
858        #[test]
859        fn test_navpoint_ord() {
860            let nav1 = NavPoint {
861                label: "Chapter 1".to_string(),
862                content: Some(PathBuf::from("chapter1.html")),
863                children: vec![],
864                play_order: Some(1),
865            };
866
867            let nav2 = NavPoint {
868                label: "Chapter 2".to_string(),
869                content: Some(PathBuf::from("chapter2.html")),
870                children: vec![],
871                play_order: Some(2),
872            };
873
874            let nav3 = NavPoint {
875                label: "Chapter 3".to_string(),
876                content: Some(PathBuf::from("chapter3.html")),
877                children: vec![],
878                play_order: Some(3),
879            };
880
881            // Test function cmp
882            assert!(nav1 < nav2);
883            assert!(nav2 > nav1);
884            assert!(nav1 == nav1);
885
886            // Test function partial_cmp
887            assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
888            assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
889            assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
890
891            // Test function sort
892            let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
893            nav_points.sort();
894            assert_eq!(nav_points, vec![nav1, nav2, nav3]);
895        }
896
897        /// Test the case of None play_order
898        #[test]
899        fn test_navpoint_ord_with_none_play_order() {
900            let nav_with_order = NavPoint {
901                label: "Chapter 1".to_string(),
902                content: Some(PathBuf::from("chapter1.html")),
903                children: vec![],
904                play_order: Some(1),
905            };
906
907            let nav_without_order = NavPoint {
908                label: "Preface".to_string(),
909                content: Some(PathBuf::from("preface.html")),
910                children: vec![],
911                play_order: None,
912            };
913
914            assert!(nav_without_order < nav_with_order);
915            assert!(nav_with_order > nav_without_order);
916
917            let nav_without_order2 = NavPoint {
918                label: "Introduction".to_string(),
919                content: Some(PathBuf::from("intro.html")),
920                children: vec![],
921                play_order: None,
922            };
923
924            assert!(nav_without_order == nav_without_order2);
925        }
926
927        /// Test NavPoint containing child nodes
928        #[test]
929        fn test_navpoint_with_children() {
930            let child1 = NavPoint {
931                label: "Section 1.1".to_string(),
932                content: Some(PathBuf::from("section1_1.html")),
933                children: vec![],
934                play_order: Some(1),
935            };
936
937            let child2 = NavPoint {
938                label: "Section 1.2".to_string(),
939                content: Some(PathBuf::from("section1_2.html")),
940                children: vec![],
941                play_order: Some(2),
942            };
943
944            let parent1 = NavPoint {
945                label: "Chapter 1".to_string(),
946                content: Some(PathBuf::from("chapter1.html")),
947                children: vec![child1.clone(), child2.clone()],
948                play_order: Some(1),
949            };
950
951            let parent2 = NavPoint {
952                label: "Chapter 1".to_string(),
953                content: Some(PathBuf::from("chapter1.html")),
954                children: vec![child1.clone(), child2.clone()],
955                play_order: Some(1),
956            };
957
958            assert!(parent1 == parent2);
959
960            let parent3 = NavPoint {
961                label: "Chapter 2".to_string(),
962                content: Some(PathBuf::from("chapter2.html")),
963                children: vec![child1.clone(), child2.clone()],
964                play_order: Some(2),
965            };
966
967            assert!(parent1 != parent3);
968            assert!(parent1 < parent3);
969        }
970
971        /// Test the case where content is None
972        #[test]
973        fn test_navpoint_with_none_content() {
974            let nav1 = NavPoint {
975                label: "Chapter 1".to_string(),
976                content: None,
977                children: vec![],
978                play_order: Some(1),
979            };
980
981            let nav2 = NavPoint {
982                label: "Chapter 1".to_string(),
983                content: None,
984                children: vec![],
985                play_order: Some(1),
986            };
987
988            assert!(nav1 == nav2);
989        }
990    }
991
992    #[cfg(feature = "builder")]
993    mod builder_tests {
994        mod metadata_item {
995            use crate::types::{MetadataItem, MetadataRefinement};
996
997            #[test]
998            fn test_metadata_item_new() {
999                let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1000
1001                assert_eq!(metadata_item.property, "title");
1002                assert_eq!(metadata_item.value, "EPUB Test Book");
1003                assert_eq!(metadata_item.id, None);
1004                assert_eq!(metadata_item.lang, None);
1005                assert_eq!(metadata_item.refined.len(), 0);
1006            }
1007
1008            #[test]
1009            fn test_metadata_item_with_id() {
1010                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1011                metadata_item.with_id("creator-1");
1012
1013                assert_eq!(metadata_item.property, "creator");
1014                assert_eq!(metadata_item.value, "John Doe");
1015                assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1016                assert_eq!(metadata_item.lang, None);
1017                assert_eq!(metadata_item.refined.len(), 0);
1018            }
1019
1020            #[test]
1021            fn test_metadata_item_with_lang() {
1022                let mut metadata_item = MetadataItem::new("title", "测试书籍");
1023                metadata_item.with_lang("zh-CN");
1024
1025                assert_eq!(metadata_item.property, "title");
1026                assert_eq!(metadata_item.value, "测试书籍");
1027                assert_eq!(metadata_item.id, None);
1028                assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1029                assert_eq!(metadata_item.refined.len(), 0);
1030            }
1031
1032            #[test]
1033            fn test_metadata_item_append_refinement() {
1034                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1035                metadata_item.with_id("creator-1"); // ID is required for refinements
1036
1037                let refinement = MetadataRefinement::new("creator-1", "role", "author");
1038                metadata_item.append_refinement(refinement);
1039
1040                assert_eq!(metadata_item.refined.len(), 1);
1041                assert_eq!(metadata_item.refined[0].refines, "creator-1");
1042                assert_eq!(metadata_item.refined[0].property, "role");
1043                assert_eq!(metadata_item.refined[0].value, "author");
1044            }
1045
1046            #[test]
1047            fn test_metadata_item_append_refinement_without_id() {
1048                let mut metadata_item = MetadataItem::new("title", "Test Book");
1049                // No ID set
1050
1051                let refinement = MetadataRefinement::new("title", "title-type", "main");
1052                metadata_item.append_refinement(refinement);
1053
1054                // Refinement should not be added because metadata item has no ID
1055                assert_eq!(metadata_item.refined.len(), 0);
1056            }
1057
1058            #[test]
1059            fn test_metadata_item_build() {
1060                let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1061                metadata_item.with_id("pub-id").with_lang("en");
1062
1063                let built = metadata_item.build();
1064
1065                assert_eq!(built.property, "identifier");
1066                assert_eq!(built.value, "urn:isbn:1234567890");
1067                assert_eq!(built.id, Some("pub-id".to_string()));
1068                assert_eq!(built.lang, Some("en".to_string()));
1069                assert_eq!(built.refined.len(), 0);
1070            }
1071
1072            #[test]
1073            fn test_metadata_item_builder_chaining() {
1074                let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1075                metadata_item.with_id("title").with_lang("en");
1076
1077                let refinement = MetadataRefinement::new("title", "title-type", "main");
1078                metadata_item.append_refinement(refinement);
1079
1080                let built = metadata_item.build();
1081
1082                assert_eq!(built.property, "title");
1083                assert_eq!(built.value, "EPUB 3.3 Guide");
1084                assert_eq!(built.id, Some("title".to_string()));
1085                assert_eq!(built.lang, Some("en".to_string()));
1086                assert_eq!(built.refined.len(), 1);
1087            }
1088
1089            #[test]
1090            fn test_metadata_item_attributes_dc_namespace() {
1091                let mut metadata_item = MetadataItem::new("title", "Test Book");
1092                metadata_item.with_id("title-id");
1093
1094                let attributes = metadata_item.attributes();
1095
1096                // For DC namespace properties, no "property" attribute should be added
1097                assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1098                assert!(
1099                    attributes
1100                        .iter()
1101                        .any(|(k, v)| k == &"id" && v == &"title-id")
1102                );
1103            }
1104
1105            #[test]
1106            fn test_metadata_item_attributes_non_dc_namespace() {
1107                let mut metadata_item = MetadataItem::new("meta", "value");
1108                metadata_item.with_id("meta-id");
1109
1110                let attributes = metadata_item.attributes();
1111
1112                // For non-DC namespace properties, "property" attribute should be added
1113                assert!(attributes.iter().any(|(k, _)| k == &"property"));
1114                assert!(
1115                    attributes
1116                        .iter()
1117                        .any(|(k, v)| k == &"id" && v == &"meta-id")
1118                );
1119            }
1120
1121            #[test]
1122            fn test_metadata_item_attributes_with_lang() {
1123                let mut metadata_item = MetadataItem::new("title", "Test Book");
1124                metadata_item.with_id("title-id").with_lang("en");
1125
1126                let attributes = metadata_item.attributes();
1127
1128                assert!(
1129                    attributes
1130                        .iter()
1131                        .any(|(k, v)| k == &"id" && v == &"title-id")
1132                );
1133                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1134            }
1135        }
1136
1137        mod metadata_refinement {
1138            use crate::types::MetadataRefinement;
1139
1140            #[test]
1141            fn test_metadata_refinement_new() {
1142                let refinement = MetadataRefinement::new("title", "title-type", "main");
1143
1144                assert_eq!(refinement.refines, "title");
1145                assert_eq!(refinement.property, "title-type");
1146                assert_eq!(refinement.value, "main");
1147                assert_eq!(refinement.lang, None);
1148                assert_eq!(refinement.scheme, None);
1149            }
1150
1151            #[test]
1152            fn test_metadata_refinement_with_lang() {
1153                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1154                refinement.with_lang("en");
1155
1156                assert_eq!(refinement.refines, "creator");
1157                assert_eq!(refinement.property, "role");
1158                assert_eq!(refinement.value, "author");
1159                assert_eq!(refinement.lang, Some("en".to_string()));
1160                assert_eq!(refinement.scheme, None);
1161            }
1162
1163            #[test]
1164            fn test_metadata_refinement_with_scheme() {
1165                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1166                refinement.with_scheme("marc:relators");
1167
1168                assert_eq!(refinement.refines, "creator");
1169                assert_eq!(refinement.property, "role");
1170                assert_eq!(refinement.value, "author");
1171                assert_eq!(refinement.lang, None);
1172                assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1173            }
1174
1175            #[test]
1176            fn test_metadata_refinement_build() {
1177                let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1178                refinement.with_lang("ja").with_scheme("iso-15924");
1179
1180                let built = refinement.build();
1181
1182                assert_eq!(built.refines, "title");
1183                assert_eq!(built.property, "alternate-script");
1184                assert_eq!(built.value, "テスト");
1185                assert_eq!(built.lang, Some("ja".to_string()));
1186                assert_eq!(built.scheme, Some("iso-15924".to_string()));
1187            }
1188
1189            #[test]
1190            fn test_metadata_refinement_builder_chaining() {
1191                let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1192                refinement.with_lang("en").with_scheme("dcterms");
1193
1194                let built = refinement.build();
1195
1196                assert_eq!(built.refines, "creator");
1197                assert_eq!(built.property, "file-as");
1198                assert_eq!(built.value, "Doe, John");
1199                assert_eq!(built.lang, Some("en".to_string()));
1200                assert_eq!(built.scheme, Some("dcterms".to_string()));
1201            }
1202
1203            #[test]
1204            fn test_metadata_refinement_attributes() {
1205                let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1206                refinement.with_lang("en").with_scheme("onix:codelist5");
1207
1208                let attributes = refinement.attributes();
1209
1210                assert!(
1211                    attributes
1212                        .iter()
1213                        .any(|(k, v)| k == &"refines" && v == &"title")
1214                );
1215                assert!(
1216                    attributes
1217                        .iter()
1218                        .any(|(k, v)| k == &"property" && v == &"title-type")
1219                );
1220                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1221                assert!(
1222                    attributes
1223                        .iter()
1224                        .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1225                );
1226            }
1227
1228            #[test]
1229            fn test_metadata_refinement_attributes_optional_fields() {
1230                let refinement = MetadataRefinement::new("creator", "role", "author");
1231                let attributes = refinement.attributes();
1232
1233                assert!(
1234                    attributes
1235                        .iter()
1236                        .any(|(k, v)| k == &"refines" && v == &"creator")
1237                );
1238                assert!(
1239                    attributes
1240                        .iter()
1241                        .any(|(k, v)| k == &"property" && v == &"role")
1242                );
1243
1244                // Should not contain optional attributes when they are None
1245                assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1246                assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1247            }
1248        }
1249
1250        mod manifest_item {
1251            use std::path::PathBuf;
1252
1253            use crate::types::ManifestItem;
1254
1255            #[test]
1256            fn test_manifest_item_new() {
1257                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1258                assert!(manifest_item.is_ok());
1259
1260                let manifest_item = manifest_item.unwrap();
1261                assert_eq!(manifest_item.id, "cover");
1262                assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1263                assert_eq!(manifest_item.mime, "");
1264                assert_eq!(manifest_item.properties, None);
1265                assert_eq!(manifest_item.fallback, None);
1266            }
1267
1268            #[test]
1269            fn test_manifest_item_append_property() {
1270                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1271                assert!(manifest_item.is_ok());
1272
1273                let mut manifest_item = manifest_item.unwrap();
1274                manifest_item.append_property("nav");
1275
1276                assert_eq!(manifest_item.id, "nav");
1277                assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1278                assert_eq!(manifest_item.mime, "");
1279                assert_eq!(manifest_item.properties, Some("nav".to_string()));
1280                assert_eq!(manifest_item.fallback, None);
1281            }
1282
1283            #[test]
1284            fn test_manifest_item_append_multiple_properties() {
1285                let manifest_item = ManifestItem::new("content", "content.xhtml");
1286                assert!(manifest_item.is_ok());
1287
1288                let mut manifest_item = manifest_item.unwrap();
1289                manifest_item
1290                    .append_property("nav")
1291                    .append_property("scripted")
1292                    .append_property("svg");
1293
1294                assert_eq!(
1295                    manifest_item.properties,
1296                    Some("nav scripted svg".to_string())
1297                );
1298            }
1299
1300            #[test]
1301            fn test_manifest_item_with_fallback() {
1302                let manifest_item = ManifestItem::new("image", "image.tiff");
1303                assert!(manifest_item.is_ok());
1304
1305                let mut manifest_item = manifest_item.unwrap();
1306                manifest_item.with_fallback("image-fallback");
1307
1308                assert_eq!(manifest_item.id, "image");
1309                assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1310                assert_eq!(manifest_item.mime, "");
1311                assert_eq!(manifest_item.properties, None);
1312                assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1313            }
1314
1315            #[test]
1316            fn test_manifest_item_set_mime() {
1317                let manifest_item = ManifestItem::new("style", "style.css");
1318                assert!(manifest_item.is_ok());
1319
1320                let manifest_item = manifest_item.unwrap();
1321                let updated_item = manifest_item.set_mime("text/css");
1322
1323                assert_eq!(updated_item.id, "style");
1324                assert_eq!(updated_item.path, PathBuf::from("style.css"));
1325                assert_eq!(updated_item.mime, "text/css");
1326                assert_eq!(updated_item.properties, None);
1327                assert_eq!(updated_item.fallback, None);
1328            }
1329
1330            #[test]
1331            fn test_manifest_item_build() {
1332                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1333                assert!(manifest_item.is_ok());
1334
1335                let mut manifest_item = manifest_item.unwrap();
1336                manifest_item
1337                    .append_property("cover-image")
1338                    .with_fallback("cover-fallback");
1339
1340                let built = manifest_item.build();
1341
1342                assert_eq!(built.id, "cover");
1343                assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
1344                assert_eq!(built.mime, "");
1345                assert_eq!(built.properties, Some("cover-image".to_string()));
1346                assert_eq!(built.fallback, Some("cover-fallback".to_string()));
1347            }
1348
1349            #[test]
1350            fn test_manifest_item_builder_chaining() {
1351                let manifest_item = ManifestItem::new("content", "content.xhtml");
1352                assert!(manifest_item.is_ok());
1353
1354                let mut manifest_item = manifest_item.unwrap();
1355                manifest_item
1356                    .append_property("scripted")
1357                    .append_property("svg")
1358                    .with_fallback("fallback-content");
1359
1360                let built = manifest_item.build();
1361
1362                assert_eq!(built.id, "content");
1363                assert_eq!(built.path, PathBuf::from("content.xhtml"));
1364                assert_eq!(built.mime, "");
1365                assert_eq!(built.properties, Some("scripted svg".to_string()));
1366                assert_eq!(built.fallback, Some("fallback-content".to_string()));
1367            }
1368
1369            #[test]
1370            fn test_manifest_item_attributes() {
1371                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1372                assert!(manifest_item.is_ok());
1373
1374                let mut manifest_item = manifest_item.unwrap();
1375                manifest_item
1376                    .append_property("nav")
1377                    .with_fallback("fallback-nav");
1378
1379                // Manually set mime type for testing
1380                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1381                let attributes = manifest_item.attributes();
1382
1383                // Check that all expected attributes are present
1384                assert!(attributes.contains(&("id", "nav")));
1385                assert!(attributes.contains(&("href", "nav.xhtml")));
1386                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1387                assert!(attributes.contains(&("properties", "nav")));
1388                assert!(attributes.contains(&("fallback", "fallback-nav")));
1389            }
1390
1391            #[test]
1392            fn test_manifest_item_attributes_optional_fields() {
1393                let manifest_item = ManifestItem::new("simple", "simple.xhtml");
1394                assert!(manifest_item.is_ok());
1395
1396                let manifest_item = manifest_item.unwrap();
1397                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1398                let attributes = manifest_item.attributes();
1399
1400                // Should contain required attributes
1401                assert!(attributes.contains(&("id", "simple")));
1402                assert!(attributes.contains(&("href", "simple.xhtml")));
1403                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1404
1405                // Should not contain optional attributes when they are None
1406                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1407                assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
1408            }
1409
1410            #[test]
1411            fn test_manifest_item_path_handling() {
1412                let manifest_item = ManifestItem::new("test", "../images/test.png");
1413                assert!(manifest_item.is_err());
1414
1415                let err = manifest_item.unwrap_err();
1416                assert_eq!(
1417                    err.to_string(),
1418                    "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
1419                );
1420            }
1421        }
1422
1423        mod spine_item {
1424            use crate::types::SpineItem;
1425
1426            #[test]
1427            fn test_spine_item_new() {
1428                let spine_item = SpineItem::new("content_001");
1429
1430                assert_eq!(spine_item.idref, "content_001");
1431                assert_eq!(spine_item.id, None);
1432                assert_eq!(spine_item.properties, None);
1433                assert_eq!(spine_item.linear, true);
1434            }
1435
1436            #[test]
1437            fn test_spine_item_with_id() {
1438                let mut spine_item = SpineItem::new("content_001");
1439                spine_item.with_id("spine1");
1440
1441                assert_eq!(spine_item.idref, "content_001");
1442                assert_eq!(spine_item.id, Some("spine1".to_string()));
1443                assert_eq!(spine_item.properties, None);
1444                assert_eq!(spine_item.linear, true);
1445            }
1446
1447            #[test]
1448            fn test_spine_item_append_property() {
1449                let mut spine_item = SpineItem::new("content_001");
1450                spine_item.append_property("page-spread-left");
1451
1452                assert_eq!(spine_item.idref, "content_001");
1453                assert_eq!(spine_item.id, None);
1454                assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
1455                assert_eq!(spine_item.linear, true);
1456            }
1457
1458            #[test]
1459            fn test_spine_item_append_multiple_properties() {
1460                let mut spine_item = SpineItem::new("content_001");
1461                spine_item
1462                    .append_property("page-spread-left")
1463                    .append_property("rendition:layout-pre-paginated");
1464
1465                assert_eq!(
1466                    spine_item.properties,
1467                    Some("page-spread-left rendition:layout-pre-paginated".to_string())
1468                );
1469            }
1470
1471            #[test]
1472            fn test_spine_item_set_linear() {
1473                let mut spine_item = SpineItem::new("content_001");
1474                spine_item.set_linear(false);
1475
1476                assert_eq!(spine_item.idref, "content_001");
1477                assert_eq!(spine_item.id, None);
1478                assert_eq!(spine_item.properties, None);
1479                assert_eq!(spine_item.linear, false);
1480            }
1481
1482            #[test]
1483            fn test_spine_item_build() {
1484                let mut spine_item = SpineItem::new("content_001");
1485                spine_item
1486                    .with_id("spine1")
1487                    .append_property("page-spread-left")
1488                    .set_linear(false);
1489
1490                let built = spine_item.build();
1491
1492                assert_eq!(built.idref, "content_001");
1493                assert_eq!(built.id, Some("spine1".to_string()));
1494                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1495                assert_eq!(built.linear, false);
1496            }
1497
1498            #[test]
1499            fn test_spine_item_builder_chaining() {
1500                let mut spine_item = SpineItem::new("content_001");
1501                spine_item
1502                    .with_id("spine1")
1503                    .append_property("page-spread-left")
1504                    .set_linear(false);
1505
1506                let built = spine_item.build();
1507
1508                assert_eq!(built.idref, "content_001");
1509                assert_eq!(built.id, Some("spine1".to_string()));
1510                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1511                assert_eq!(built.linear, false);
1512            }
1513
1514            #[test]
1515            fn test_spine_item_attributes() {
1516                let mut spine_item = SpineItem::new("content_001");
1517                spine_item
1518                    .with_id("spine1")
1519                    .append_property("page-spread-left")
1520                    .set_linear(false);
1521
1522                let attributes = spine_item.attributes();
1523
1524                // Check that all expected attributes are present
1525                assert!(attributes.contains(&("idref", "content_001")));
1526                assert!(attributes.contains(&("id", "spine1")));
1527                assert!(attributes.contains(&("properties", "page-spread-left")));
1528                assert!(attributes.contains(&("linear", "no"))); // false should become "no"
1529            }
1530
1531            #[test]
1532            fn test_spine_item_attributes_linear_yes() {
1533                let spine_item = SpineItem::new("content_001");
1534                let attributes = spine_item.attributes();
1535
1536                // Linear true should become "yes"
1537                assert!(attributes.contains(&("linear", "yes")));
1538            }
1539
1540            #[test]
1541            fn test_spine_item_attributes_optional_fields() {
1542                let spine_item = SpineItem::new("content_001");
1543                let attributes = spine_item.attributes();
1544
1545                // Should only contain required attributes when optional fields are None
1546                assert!(attributes.contains(&("idref", "content_001")));
1547                assert!(attributes.contains(&("linear", "yes")));
1548
1549                // Should not contain optional attributes when they are None
1550                assert!(!attributes.iter().any(|(k, _)| k == &"id"));
1551                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1552            }
1553        }
1554
1555        mod navpoint {
1556
1557            use std::path::PathBuf;
1558
1559            use crate::types::NavPoint;
1560
1561            #[test]
1562            fn test_navpoint_new() {
1563                let navpoint = NavPoint::new("Test Chapter");
1564
1565                assert_eq!(navpoint.label, "Test Chapter");
1566                assert_eq!(navpoint.content, None);
1567                assert_eq!(navpoint.children.len(), 0);
1568            }
1569
1570            #[test]
1571            fn test_navpoint_with_content() {
1572                let mut navpoint = NavPoint::new("Test Chapter");
1573                navpoint.with_content("chapter1.html");
1574
1575                assert_eq!(navpoint.label, "Test Chapter");
1576                assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
1577                assert_eq!(navpoint.children.len(), 0);
1578            }
1579
1580            #[test]
1581            fn test_navpoint_append_child() {
1582                let mut parent = NavPoint::new("Parent Chapter");
1583
1584                let mut child1 = NavPoint::new("Child Section 1");
1585                child1.with_content("section1.html");
1586
1587                let mut child2 = NavPoint::new("Child Section 2");
1588                child2.with_content("section2.html");
1589
1590                parent.append_child(child1.build());
1591                parent.append_child(child2.build());
1592
1593                assert_eq!(parent.children.len(), 2);
1594                assert_eq!(parent.children[0].label, "Child Section 1");
1595                assert_eq!(parent.children[1].label, "Child Section 2");
1596            }
1597
1598            #[test]
1599            fn test_navpoint_set_children() {
1600                let mut navpoint = NavPoint::new("Main Chapter");
1601                let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
1602
1603                navpoint.set_children(children);
1604
1605                assert_eq!(navpoint.children.len(), 2);
1606                assert_eq!(navpoint.children[0].label, "Section 1");
1607                assert_eq!(navpoint.children[1].label, "Section 2");
1608            }
1609
1610            #[test]
1611            fn test_navpoint_build() {
1612                let mut navpoint = NavPoint::new("Complete Chapter");
1613                navpoint.with_content("complete.html");
1614
1615                let child = NavPoint::new("Sub Section");
1616                navpoint.append_child(child.build());
1617
1618                let built = navpoint.build();
1619
1620                assert_eq!(built.label, "Complete Chapter");
1621                assert_eq!(built.content, Some(PathBuf::from("complete.html")));
1622                assert_eq!(built.children.len(), 1);
1623                assert_eq!(built.children[0].label, "Sub Section");
1624            }
1625
1626            #[test]
1627            fn test_navpoint_builder_chaining() {
1628                let mut navpoint = NavPoint::new("Chained Chapter");
1629
1630                navpoint
1631                    .with_content("chained.html")
1632                    .append_child(NavPoint::new("Child 1").build())
1633                    .append_child(NavPoint::new("Child 2").build());
1634
1635                let built = navpoint.build();
1636
1637                assert_eq!(built.label, "Chained Chapter");
1638                assert_eq!(built.content, Some(PathBuf::from("chained.html")));
1639                assert_eq!(built.children.len(), 2);
1640            }
1641
1642            #[test]
1643            fn test_navpoint_empty_children() {
1644                let navpoint = NavPoint::new("No Children Chapter");
1645                let built = navpoint.build();
1646
1647                assert_eq!(built.children.len(), 0);
1648            }
1649
1650            #[test]
1651            fn test_navpoint_complex_hierarchy() {
1652                let mut root = NavPoint::new("Book");
1653
1654                let mut chapter1 = NavPoint::new("Chapter 1");
1655                chapter1
1656                    .with_content("chapter1.html")
1657                    .append_child(
1658                        NavPoint::new("Section 1.1")
1659                            .with_content("sec1_1.html")
1660                            .build(),
1661                    )
1662                    .append_child(
1663                        NavPoint::new("Section 1.2")
1664                            .with_content("sec1_2.html")
1665                            .build(),
1666                    );
1667
1668                let mut chapter2 = NavPoint::new("Chapter 2");
1669                chapter2.with_content("chapter2.html").append_child(
1670                    NavPoint::new("Section 2.1")
1671                        .with_content("sec2_1.html")
1672                        .build(),
1673                );
1674
1675                root.append_child(chapter1.build())
1676                    .append_child(chapter2.build());
1677
1678                let book = root.build();
1679
1680                assert_eq!(book.label, "Book");
1681                assert_eq!(book.children.len(), 2);
1682
1683                let ch1 = &book.children[0];
1684                assert_eq!(ch1.label, "Chapter 1");
1685                assert_eq!(ch1.children.len(), 2);
1686
1687                let ch2 = &book.children[1];
1688                assert_eq!(ch2.label, "Chapter 2");
1689                assert_eq!(ch2.children.len(), 1);
1690            }
1691        }
1692    }
1693
1694    mod footnote_tests {
1695        use crate::types::Footnote;
1696
1697        #[test]
1698        fn test_footnote_basic_creation() {
1699            let footnote = Footnote {
1700                locate: 100,
1701                content: "Sample footnote".to_string(),
1702            };
1703
1704            assert_eq!(footnote.locate, 100);
1705            assert_eq!(footnote.content, "Sample footnote");
1706        }
1707
1708        #[test]
1709        fn test_footnote_equality() {
1710            let footnote1 = Footnote {
1711                locate: 100,
1712                content: "First note".to_string(),
1713            };
1714
1715            let footnote2 = Footnote {
1716                locate: 100,
1717                content: "First note".to_string(),
1718            };
1719
1720            let footnote3 = Footnote {
1721                locate: 100,
1722                content: "Different note".to_string(),
1723            };
1724
1725            let footnote4 = Footnote {
1726                locate: 200,
1727                content: "First note".to_string(),
1728            };
1729
1730            assert_eq!(footnote1, footnote2);
1731            assert_ne!(footnote1, footnote3);
1732            assert_ne!(footnote1, footnote4);
1733        }
1734
1735        #[test]
1736        fn test_footnote_ordering() {
1737            let footnote1 = Footnote {
1738                locate: 100,
1739                content: "First".to_string(),
1740            };
1741
1742            let footnote2 = Footnote {
1743                locate: 200,
1744                content: "Second".to_string(),
1745            };
1746
1747            let footnote3 = Footnote {
1748                locate: 150,
1749                content: "Middle".to_string(),
1750            };
1751
1752            assert!(footnote1 < footnote2);
1753            assert!(footnote2 > footnote1);
1754            assert!(footnote1 < footnote3);
1755            assert!(footnote3 < footnote2);
1756            assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
1757        }
1758
1759        #[test]
1760        fn test_footnote_sorting() {
1761            let mut footnotes = vec![
1762                Footnote {
1763                    locate: 300,
1764                    content: "Third note".to_string(),
1765                },
1766                Footnote {
1767                    locate: 100,
1768                    content: "First note".to_string(),
1769                },
1770                Footnote {
1771                    locate: 200,
1772                    content: "Second note".to_string(),
1773                },
1774            ];
1775
1776            footnotes.sort();
1777
1778            assert_eq!(footnotes[0].locate, 100);
1779            assert_eq!(footnotes[1].locate, 200);
1780            assert_eq!(footnotes[2].locate, 300);
1781
1782            assert_eq!(footnotes[0].content, "First note");
1783            assert_eq!(footnotes[1].content, "Second note");
1784            assert_eq!(footnotes[2].content, "Third note");
1785        }
1786    }
1787}