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// TODO: 需要增加一个函数,用于处理绝对路径‘/’和相对opf路径,将相对路径转为绝对路径
406#[cfg(feature = "builder")]
407impl ManifestItem {
408    /// Creates a new manifest item
409    ///
410    /// Requires the `builder` feature.
411    ///
412    /// # Parameters
413    /// - `id` - The unique identifier for this resource
414    /// - `path` - The path to the resource file
415    ///
416    /// # Errors
417    /// Returns an error if the path starts with "../" which is not allowed.
418    pub fn new(id: &str, path: &str) -> Result<Self, EpubError> {
419        if path.starts_with("../") {
420            return Err(EpubBuilderError::IllegalManifestPath {
421                manifest_id: id.to_string(),
422            }
423            .into());
424        }
425
426        Ok(Self {
427            id: id.to_string(),
428            path: PathBuf::from(path),
429            mime: String::new(),
430            properties: None,
431            fallback: None,
432        })
433    }
434
435    /// Sets the MIME type of the manifest item
436    pub(crate) fn set_mime(self, mime: &str) -> Self {
437        Self {
438            id: self.id,
439            path: self.path,
440            mime: mime.to_string(),
441            properties: self.properties,
442            fallback: self.fallback,
443        }
444    }
445
446    /// Appends a property to the manifest item
447    ///
448    /// Requires the `builder` feature.
449    ///
450    /// # Parameters
451    /// - `property` - The property to add
452    pub fn append_property(&mut self, property: &str) -> &mut Self {
453        let new_properties = if let Some(properties) = &self.properties {
454            format!("{} {}", properties, property)
455        } else {
456            property.to_string()
457        };
458
459        self.properties = Some(new_properties);
460        self
461    }
462
463    /// Sets the fallback for this manifest item
464    ///
465    /// Requires the `builder` feature.
466    ///
467    /// # Parameters
468    /// - `fallback` - The ID of the fallback manifest item
469    pub fn with_fallback(&mut self, fallback: &str) -> &mut Self {
470        self.fallback = Some(fallback.to_string());
471        self
472    }
473
474    /// Builds the final manifest item
475    ///
476    /// Requires the `builder` feature.
477    pub fn build(&self) -> Self {
478        Self { ..self.clone() }
479    }
480
481    /// Gets the XML attributes for this manifest item
482    pub fn attributes(&self) -> Vec<(&str, &str)> {
483        let mut attributes = Vec::new();
484
485        attributes.push(("id", self.id.as_str()));
486        attributes.push(("href", self.path.to_str().unwrap()));
487        attributes.push(("media-type", self.mime.as_str()));
488
489        if let Some(properties) = &self.properties {
490            attributes.push(("properties", properties.as_str()));
491        }
492
493        if let Some(fallback) = &self.fallback {
494            attributes.push(("fallback", fallback.as_str()));
495        }
496
497        attributes
498    }
499}
500
501/// Represents an item in the EPUB spine, defining the reading order of the publication
502///
503/// The `SpineItem` structure represents a single item in the EPUB spine, which defines
504/// the linear reading order of the publication's content documents. Each spine item
505/// references a resource declared in the manifest and indicates whether it should be
506/// included in the linear reading sequence.
507///
508/// The spine is a crucial component of an EPUB publication as it determines the recommended
509/// reading order of content documents. Items can be marked as linear (part of the main reading
510/// flow) or non-linear (supplementary content that may be accessed out of sequence).
511///
512/// # Builder Methods
513///
514/// When the `builder` feature is enabled, this struct provides convenient builder methods:
515///
516/// ```
517/// # #[cfg(feature = "builder")] {
518/// use lib_epub::types::SpineItem;
519///
520/// let spine_item = SpineItem::new("content-1")
521///     .with_id("spine-1")
522///     .append_property("page-spread-right")
523///     .set_linear(false)
524///     .build();
525/// # }
526/// ```
527#[derive(Debug, Clone)]
528pub struct SpineItem {
529    /// The ID reference to a manifest item
530    ///
531    /// This field contains the ID of the manifest item that this spine item references.
532    /// It establishes the connection between the reading order (spine) and the actual
533    /// content resources (manifest). The referenced ID must exist in the manifest.
534    pub idref: String,
535
536    /// Optional identifier for this spine item
537    pub id: Option<String>,
538
539    /// Optional properties associated with this spine item
540    ///
541    /// This field contains a space-separated list of properties that apply to this
542    /// spine item. These properties can indicate special handling requirements,
543    /// layout preferences, or other characteristics.
544    pub properties: Option<String>,
545
546    /// Indicates whether this item is part of the linear reading order
547    ///
548    /// When `true`, this spine item is part of the main linear reading sequence.
549    /// When `false`, this item represents supplementary content that may be accessed
550    /// out of the normal reading order (e.g., through hyperlinks).
551    ///
552    /// Non-linear items are typically used for content like footnotes, endnotes,
553    /// appendices, or other supplementary materials that readers might access
554    /// on-demand rather than sequentially.
555    pub linear: bool,
556}
557
558#[cfg(feature = "builder")]
559impl SpineItem {
560    /// Creates a new spine item referencing a manifest item
561    ///
562    /// Requires the `builder` feature.
563    ///
564    /// By default, spine items are linear.
565    ///
566    /// # Parameters
567    /// - `idref` - The ID of the manifest item this spine item references
568    pub fn new(idref: &str) -> Self {
569        Self {
570            idref: idref.to_string(),
571            id: None,
572            properties: None,
573            linear: true,
574        }
575    }
576
577    /// Sets the ID of the spine item
578    ///
579    /// Requires the `builder` feature.
580    ///
581    /// # Parameters
582    /// - `id` - The ID to assign to this spine item
583    pub fn with_id(&mut self, id: &str) -> &mut Self {
584        self.id = Some(id.to_string());
585        self
586    }
587
588    /// Appends a property to the spine item
589    ///
590    /// Requires the `builder` feature.
591    ///
592    /// # Parameters
593    /// - `property` - The property to add
594    pub fn append_property(&mut self, property: &str) -> &mut Self {
595        let new_properties = if let Some(properties) = &self.properties {
596            format!("{} {}", properties, property)
597        } else {
598            property.to_string()
599        };
600
601        self.properties = Some(new_properties);
602        self
603    }
604
605    /// Sets whether this spine item is part of the linear reading order
606    ///
607    /// Requires the `builder` feature.
608    ///
609    /// # Parameters
610    /// - `linear` - `true` if the item is part of the linear reading order, `false` otherwise
611    pub fn set_linear(&mut self, linear: bool) -> &mut Self {
612        self.linear = linear;
613        self
614    }
615
616    /// Builds the final spine item
617    ///
618    /// Requires the `builder` feature.
619    pub fn build(&self) -> Self {
620        Self { ..self.clone() }
621    }
622
623    /// Gets the XML attributes for this spine item
624    pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
625        let mut attributes = Vec::new();
626
627        attributes.push(("idref", self.idref.as_str()));
628        attributes.push(("linear", if self.linear { "yes" } else { "no" }));
629
630        if let Some(id) = &self.id {
631            attributes.push(("id", id.as_str()));
632        }
633
634        if let Some(properties) = &self.properties {
635            attributes.push(("properties", properties.as_str()));
636        }
637
638        attributes
639    }
640}
641
642/// Represents encryption information for EPUB resources
643///
644/// This structure holds information about encrypted resources in an EPUB publication,
645/// as defined in the META-INF/encryption.xml file according to the EPUB specification.
646/// It describes which resources are encrypted and what encryption method was used.
647#[derive(Debug, Clone)]
648pub struct EncryptionData {
649    /// The encryption algorithm URI
650    ///
651    /// This field specifies the encryption method used for the resource.
652    /// Supported encryption methods:
653    /// - IDPF font obfuscation: <http://www.idpf.org/2008/embedding>
654    /// - Adobe font obfuscation: <http://ns.adobe.com/pdf/enc#RC>
655    pub method: String,
656
657    /// The URI of the encrypted resource
658    ///
659    /// This field contains the path/URI to the encrypted resource within the EPUB container.
660    /// The path is relative to the root of the EPUB container.
661    pub data: String,
662}
663
664/// Represents a navigation point in an EPUB document's table of contents
665///
666/// The `NavPoint` structure represents a single entry in the hierarchical table of contents
667/// of an EPUB publication. Each navigation point corresponds to a section or chapter in
668/// the publication and may contain nested child navigation points to represent sub-sections.
669///
670/// # Builder Methods
671///
672/// When the `builder` feature is enabled, this struct provides convenient builder methods:
673///
674/// ```
675/// # #[cfg(feature = "builder")] {
676/// use lib_epub::types::NavPoint;
677///
678/// let nav_point = NavPoint::new("Chapter 1")
679///     .with_content("chapter1.xhtml")
680///     .append_child(
681///         NavPoint::new("Section 1.1")
682///             .with_content("section1_1.xhtml")
683///             .build()
684///     )
685///     .build();
686/// # }
687/// ```
688#[derive(Debug, Eq, Clone)]
689pub struct NavPoint {
690    /// The display label/title of this navigation point
691    ///
692    /// This is the text that should be displayed to users in the table of contents.
693    pub label: String,
694
695    /// The content document path this navigation point references
696    ///
697    /// Can be `None` for navigation points that no relevant information was
698    /// provided in the original data.
699    pub content: Option<PathBuf>,
700
701    /// Child navigation points (sub-sections)
702    pub children: Vec<NavPoint>,
703
704    /// The reading order position of this navigation point
705    ///
706    /// It can be `None` for navigation points that no relevant information was
707    /// provided in the original data.
708    pub play_order: Option<usize>,
709}
710
711#[cfg(feature = "builder")]
712impl NavPoint {
713    /// Creates a new navigation point with the given label
714    ///
715    /// Requires the `builder` feature.
716    ///
717    /// # Parameters
718    /// - `label` - The display label for this navigation point
719    pub fn new(label: &str) -> Self {
720        Self {
721            label: label.to_string(),
722            content: None,
723            children: vec![],
724            play_order: None,
725        }
726    }
727
728    /// Sets the content path for this navigation point
729    ///
730    /// Requires the `builder` feature.
731    ///
732    /// # Parameters
733    /// - `content` - The path to the content document
734    pub fn with_content(&mut self, content: &str) -> &mut Self {
735        self.content = Some(PathBuf::from(content));
736        self
737    }
738
739    /// Appends a child navigation point
740    ///
741    /// Requires the `builder` feature.
742    ///
743    /// # Parameters
744    /// - `child` - The child navigation point to add
745    pub fn append_child(&mut self, child: NavPoint) -> &mut Self {
746        self.children.push(child);
747        self
748    }
749
750    /// Sets all child navigation points
751    ///
752    /// Requires the `builder` feature.
753    ///
754    /// # Parameters
755    /// - `children` - Vector of child navigation points
756    pub fn set_children(&mut self, children: Vec<NavPoint>) -> &mut Self {
757        self.children = children;
758        self
759    }
760
761    /// Builds the final navigation point
762    ///
763    /// Requires the `builder` feature.
764    pub fn build(&self) -> Self {
765        Self { ..self.clone() }
766    }
767}
768
769impl Ord for NavPoint {
770    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
771        self.play_order.cmp(&other.play_order)
772    }
773}
774
775impl PartialOrd for NavPoint {
776    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
777        Some(self.cmp(other))
778    }
779}
780
781impl PartialEq for NavPoint {
782    fn eq(&self, other: &Self) -> bool {
783        self.play_order == other.play_order
784    }
785}
786
787#[cfg(test)]
788mod tests {
789    mod navpoint_tests {
790        use std::path::PathBuf;
791
792        use crate::types::NavPoint;
793
794        /// Testing the equality comparison of NavPoint
795        #[test]
796        fn test_navpoint_partial_eq() {
797            let nav1 = NavPoint {
798                label: "Chapter 1".to_string(),
799                content: Some(PathBuf::from("chapter1.html")),
800                children: vec![],
801                play_order: Some(1),
802            };
803
804            let nav2 = NavPoint {
805                label: "Chapter 1".to_string(),
806                content: Some(PathBuf::from("chapter2.html")),
807                children: vec![],
808                play_order: Some(1),
809            };
810
811            let nav3 = NavPoint {
812                label: "Chapter 2".to_string(),
813                content: Some(PathBuf::from("chapter1.html")),
814                children: vec![],
815                play_order: Some(2),
816            };
817
818            assert_eq!(nav1, nav2); // Same play_order, different contents, should be equal
819            assert_ne!(nav1, nav3); // Different play_order, Same contents, should be unequal
820        }
821
822        /// Test NavPoint sorting comparison
823        #[test]
824        fn test_navpoint_ord() {
825            let nav1 = NavPoint {
826                label: "Chapter 1".to_string(),
827                content: Some(PathBuf::from("chapter1.html")),
828                children: vec![],
829                play_order: Some(1),
830            };
831
832            let nav2 = NavPoint {
833                label: "Chapter 2".to_string(),
834                content: Some(PathBuf::from("chapter2.html")),
835                children: vec![],
836                play_order: Some(2),
837            };
838
839            let nav3 = NavPoint {
840                label: "Chapter 3".to_string(),
841                content: Some(PathBuf::from("chapter3.html")),
842                children: vec![],
843                play_order: Some(3),
844            };
845
846            // Test function cmp
847            assert!(nav1 < nav2);
848            assert!(nav2 > nav1);
849            assert!(nav1 == nav1);
850
851            // Test function partial_cmp
852            assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
853            assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
854            assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
855
856            // Test function sort
857            let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
858            nav_points.sort();
859            assert_eq!(nav_points, vec![nav1, nav2, nav3]);
860        }
861
862        /// Test the case of None play_order
863        #[test]
864        fn test_navpoint_ord_with_none_play_order() {
865            let nav_with_order = NavPoint {
866                label: "Chapter 1".to_string(),
867                content: Some(PathBuf::from("chapter1.html")),
868                children: vec![],
869                play_order: Some(1),
870            };
871
872            let nav_without_order = NavPoint {
873                label: "Preface".to_string(),
874                content: Some(PathBuf::from("preface.html")),
875                children: vec![],
876                play_order: None,
877            };
878
879            assert!(nav_without_order < nav_with_order);
880            assert!(nav_with_order > nav_without_order);
881
882            let nav_without_order2 = NavPoint {
883                label: "Introduction".to_string(),
884                content: Some(PathBuf::from("intro.html")),
885                children: vec![],
886                play_order: None,
887            };
888
889            assert!(nav_without_order == nav_without_order2);
890        }
891
892        /// Test NavPoint containing child nodes
893        #[test]
894        fn test_navpoint_with_children() {
895            let child1 = NavPoint {
896                label: "Section 1.1".to_string(),
897                content: Some(PathBuf::from("section1_1.html")),
898                children: vec![],
899                play_order: Some(1),
900            };
901
902            let child2 = NavPoint {
903                label: "Section 1.2".to_string(),
904                content: Some(PathBuf::from("section1_2.html")),
905                children: vec![],
906                play_order: Some(2),
907            };
908
909            let parent1 = NavPoint {
910                label: "Chapter 1".to_string(),
911                content: Some(PathBuf::from("chapter1.html")),
912                children: vec![child1.clone(), child2.clone()],
913                play_order: Some(1),
914            };
915
916            let parent2 = NavPoint {
917                label: "Chapter 1".to_string(),
918                content: Some(PathBuf::from("chapter1.html")),
919                children: vec![child1.clone(), child2.clone()],
920                play_order: Some(1),
921            };
922
923            assert!(parent1 == parent2);
924
925            let parent3 = NavPoint {
926                label: "Chapter 2".to_string(),
927                content: Some(PathBuf::from("chapter2.html")),
928                children: vec![child1.clone(), child2.clone()],
929                play_order: Some(2),
930            };
931
932            assert!(parent1 != parent3);
933            assert!(parent1 < parent3);
934        }
935
936        /// Test the case where content is None
937        #[test]
938        fn test_navpoint_with_none_content() {
939            let nav1 = NavPoint {
940                label: "Chapter 1".to_string(),
941                content: None,
942                children: vec![],
943                play_order: Some(1),
944            };
945
946            let nav2 = NavPoint {
947                label: "Chapter 1".to_string(),
948                content: None,
949                children: vec![],
950                play_order: Some(1),
951            };
952
953            assert!(nav1 == nav2);
954        }
955    }
956
957    #[cfg(feature = "builder")]
958    mod builder_tests {
959        mod metadata_item {
960            use crate::types::{MetadataItem, MetadataRefinement};
961
962            #[test]
963            fn test_metadata_item_new() {
964                let metadata_item = MetadataItem::new("title", "EPUB Test Book");
965
966                assert_eq!(metadata_item.property, "title");
967                assert_eq!(metadata_item.value, "EPUB Test Book");
968                assert_eq!(metadata_item.id, None);
969                assert_eq!(metadata_item.lang, None);
970                assert_eq!(metadata_item.refined.len(), 0);
971            }
972
973            #[test]
974            fn test_metadata_item_with_id() {
975                let mut metadata_item = MetadataItem::new("creator", "John Doe");
976                metadata_item.with_id("creator-1");
977
978                assert_eq!(metadata_item.property, "creator");
979                assert_eq!(metadata_item.value, "John Doe");
980                assert_eq!(metadata_item.id, Some("creator-1".to_string()));
981                assert_eq!(metadata_item.lang, None);
982                assert_eq!(metadata_item.refined.len(), 0);
983            }
984
985            #[test]
986            fn test_metadata_item_with_lang() {
987                let mut metadata_item = MetadataItem::new("title", "测试书籍");
988                metadata_item.with_lang("zh-CN");
989
990                assert_eq!(metadata_item.property, "title");
991                assert_eq!(metadata_item.value, "测试书籍");
992                assert_eq!(metadata_item.id, None);
993                assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
994                assert_eq!(metadata_item.refined.len(), 0);
995            }
996
997            #[test]
998            fn test_metadata_item_append_refinement() {
999                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1000                metadata_item.with_id("creator-1"); // ID is required for refinements
1001
1002                let refinement = MetadataRefinement::new("creator-1", "role", "author");
1003                metadata_item.append_refinement(refinement);
1004
1005                assert_eq!(metadata_item.refined.len(), 1);
1006                assert_eq!(metadata_item.refined[0].refines, "creator-1");
1007                assert_eq!(metadata_item.refined[0].property, "role");
1008                assert_eq!(metadata_item.refined[0].value, "author");
1009            }
1010
1011            #[test]
1012            fn test_metadata_item_append_refinement_without_id() {
1013                let mut metadata_item = MetadataItem::new("title", "Test Book");
1014                // No ID set
1015
1016                let refinement = MetadataRefinement::new("title", "title-type", "main");
1017                metadata_item.append_refinement(refinement);
1018
1019                // Refinement should not be added because metadata item has no ID
1020                assert_eq!(metadata_item.refined.len(), 0);
1021            }
1022
1023            #[test]
1024            fn test_metadata_item_build() {
1025                let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1026                metadata_item.with_id("pub-id").with_lang("en");
1027
1028                let built = metadata_item.build();
1029
1030                assert_eq!(built.property, "identifier");
1031                assert_eq!(built.value, "urn:isbn:1234567890");
1032                assert_eq!(built.id, Some("pub-id".to_string()));
1033                assert_eq!(built.lang, Some("en".to_string()));
1034                assert_eq!(built.refined.len(), 0);
1035            }
1036
1037            #[test]
1038            fn test_metadata_item_builder_chaining() {
1039                let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1040                metadata_item.with_id("title").with_lang("en");
1041
1042                let refinement = MetadataRefinement::new("title", "title-type", "main");
1043                metadata_item.append_refinement(refinement);
1044
1045                let built = metadata_item.build();
1046
1047                assert_eq!(built.property, "title");
1048                assert_eq!(built.value, "EPUB 3.3 Guide");
1049                assert_eq!(built.id, Some("title".to_string()));
1050                assert_eq!(built.lang, Some("en".to_string()));
1051                assert_eq!(built.refined.len(), 1);
1052            }
1053
1054            #[test]
1055            fn test_metadata_item_attributes_dc_namespace() {
1056                let mut metadata_item = MetadataItem::new("title", "Test Book");
1057                metadata_item.with_id("title-id");
1058
1059                let attributes = metadata_item.attributes();
1060
1061                // For DC namespace properties, no "property" attribute should be added
1062                assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1063                assert!(
1064                    attributes
1065                        .iter()
1066                        .any(|(k, v)| k == &"id" && v == &"title-id")
1067                );
1068            }
1069
1070            #[test]
1071            fn test_metadata_item_attributes_non_dc_namespace() {
1072                let mut metadata_item = MetadataItem::new("meta", "value");
1073                metadata_item.with_id("meta-id");
1074
1075                let attributes = metadata_item.attributes();
1076
1077                // For non-DC namespace properties, "property" attribute should be added
1078                assert!(attributes.iter().any(|(k, _)| k == &"property"));
1079                assert!(
1080                    attributes
1081                        .iter()
1082                        .any(|(k, v)| k == &"id" && v == &"meta-id")
1083                );
1084            }
1085
1086            #[test]
1087            fn test_metadata_item_attributes_with_lang() {
1088                let mut metadata_item = MetadataItem::new("title", "Test Book");
1089                metadata_item.with_id("title-id").with_lang("en");
1090
1091                let attributes = metadata_item.attributes();
1092
1093                assert!(
1094                    attributes
1095                        .iter()
1096                        .any(|(k, v)| k == &"id" && v == &"title-id")
1097                );
1098                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1099            }
1100        }
1101
1102        mod metadata_refinement {
1103            use crate::types::MetadataRefinement;
1104
1105            #[test]
1106            fn test_metadata_refinement_new() {
1107                let refinement = MetadataRefinement::new("title", "title-type", "main");
1108
1109                assert_eq!(refinement.refines, "title");
1110                assert_eq!(refinement.property, "title-type");
1111                assert_eq!(refinement.value, "main");
1112                assert_eq!(refinement.lang, None);
1113                assert_eq!(refinement.scheme, None);
1114            }
1115
1116            #[test]
1117            fn test_metadata_refinement_with_lang() {
1118                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1119                refinement.with_lang("en");
1120
1121                assert_eq!(refinement.refines, "creator");
1122                assert_eq!(refinement.property, "role");
1123                assert_eq!(refinement.value, "author");
1124                assert_eq!(refinement.lang, Some("en".to_string()));
1125                assert_eq!(refinement.scheme, None);
1126            }
1127
1128            #[test]
1129            fn test_metadata_refinement_with_scheme() {
1130                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1131                refinement.with_scheme("marc:relators");
1132
1133                assert_eq!(refinement.refines, "creator");
1134                assert_eq!(refinement.property, "role");
1135                assert_eq!(refinement.value, "author");
1136                assert_eq!(refinement.lang, None);
1137                assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1138            }
1139
1140            #[test]
1141            fn test_metadata_refinement_build() {
1142                let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1143                refinement.with_lang("ja").with_scheme("iso-15924");
1144
1145                let built = refinement.build();
1146
1147                assert_eq!(built.refines, "title");
1148                assert_eq!(built.property, "alternate-script");
1149                assert_eq!(built.value, "テスト");
1150                assert_eq!(built.lang, Some("ja".to_string()));
1151                assert_eq!(built.scheme, Some("iso-15924".to_string()));
1152            }
1153
1154            #[test]
1155            fn test_metadata_refinement_builder_chaining() {
1156                let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1157                refinement.with_lang("en").with_scheme("dcterms");
1158
1159                let built = refinement.build();
1160
1161                assert_eq!(built.refines, "creator");
1162                assert_eq!(built.property, "file-as");
1163                assert_eq!(built.value, "Doe, John");
1164                assert_eq!(built.lang, Some("en".to_string()));
1165                assert_eq!(built.scheme, Some("dcterms".to_string()));
1166            }
1167
1168            #[test]
1169            fn test_metadata_refinement_attributes() {
1170                let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1171                refinement.with_lang("en").with_scheme("onix:codelist5");
1172
1173                let attributes = refinement.attributes();
1174
1175                assert!(
1176                    attributes
1177                        .iter()
1178                        .any(|(k, v)| k == &"refines" && v == &"title")
1179                );
1180                assert!(
1181                    attributes
1182                        .iter()
1183                        .any(|(k, v)| k == &"property" && v == &"title-type")
1184                );
1185                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1186                assert!(
1187                    attributes
1188                        .iter()
1189                        .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1190                );
1191            }
1192
1193            #[test]
1194            fn test_metadata_refinement_attributes_optional_fields() {
1195                let refinement = MetadataRefinement::new("creator", "role", "author");
1196                let attributes = refinement.attributes();
1197
1198                assert!(
1199                    attributes
1200                        .iter()
1201                        .any(|(k, v)| k == &"refines" && v == &"creator")
1202                );
1203                assert!(
1204                    attributes
1205                        .iter()
1206                        .any(|(k, v)| k == &"property" && v == &"role")
1207                );
1208
1209                // Should not contain optional attributes when they are None
1210                assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1211                assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1212            }
1213        }
1214
1215        mod manifest_item {
1216            use std::path::PathBuf;
1217
1218            use crate::types::ManifestItem;
1219
1220            #[test]
1221            fn test_manifest_item_new() {
1222                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1223                assert!(manifest_item.is_ok());
1224
1225                let manifest_item = manifest_item.unwrap();
1226                assert_eq!(manifest_item.id, "cover");
1227                assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1228                assert_eq!(manifest_item.mime, "");
1229                assert_eq!(manifest_item.properties, None);
1230                assert_eq!(manifest_item.fallback, None);
1231            }
1232
1233            #[test]
1234            fn test_manifest_item_append_property() {
1235                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1236                assert!(manifest_item.is_ok());
1237
1238                let mut manifest_item = manifest_item.unwrap();
1239                manifest_item.append_property("nav");
1240
1241                assert_eq!(manifest_item.id, "nav");
1242                assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1243                assert_eq!(manifest_item.mime, "");
1244                assert_eq!(manifest_item.properties, Some("nav".to_string()));
1245                assert_eq!(manifest_item.fallback, None);
1246            }
1247
1248            #[test]
1249            fn test_manifest_item_append_multiple_properties() {
1250                let manifest_item = ManifestItem::new("content", "content.xhtml");
1251                assert!(manifest_item.is_ok());
1252
1253                let mut manifest_item = manifest_item.unwrap();
1254                manifest_item
1255                    .append_property("nav")
1256                    .append_property("scripted")
1257                    .append_property("svg");
1258
1259                assert_eq!(
1260                    manifest_item.properties,
1261                    Some("nav scripted svg".to_string())
1262                );
1263            }
1264
1265            #[test]
1266            fn test_manifest_item_with_fallback() {
1267                let manifest_item = ManifestItem::new("image", "image.tiff");
1268                assert!(manifest_item.is_ok());
1269
1270                let mut manifest_item = manifest_item.unwrap();
1271                manifest_item.with_fallback("image-fallback");
1272
1273                assert_eq!(manifest_item.id, "image");
1274                assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1275                assert_eq!(manifest_item.mime, "");
1276                assert_eq!(manifest_item.properties, None);
1277                assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1278            }
1279
1280            #[test]
1281            fn test_manifest_item_set_mime() {
1282                let manifest_item = ManifestItem::new("style", "style.css");
1283                assert!(manifest_item.is_ok());
1284
1285                let manifest_item = manifest_item.unwrap();
1286                let updated_item = manifest_item.set_mime("text/css");
1287
1288                assert_eq!(updated_item.id, "style");
1289                assert_eq!(updated_item.path, PathBuf::from("style.css"));
1290                assert_eq!(updated_item.mime, "text/css");
1291                assert_eq!(updated_item.properties, None);
1292                assert_eq!(updated_item.fallback, None);
1293            }
1294
1295            #[test]
1296            fn test_manifest_item_build() {
1297                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1298                assert!(manifest_item.is_ok());
1299
1300                let mut manifest_item = manifest_item.unwrap();
1301                manifest_item
1302                    .append_property("cover-image")
1303                    .with_fallback("cover-fallback");
1304
1305                let built = manifest_item.build();
1306
1307                assert_eq!(built.id, "cover");
1308                assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
1309                assert_eq!(built.mime, "");
1310                assert_eq!(built.properties, Some("cover-image".to_string()));
1311                assert_eq!(built.fallback, Some("cover-fallback".to_string()));
1312            }
1313
1314            #[test]
1315            fn test_manifest_item_builder_chaining() {
1316                let manifest_item = ManifestItem::new("content", "content.xhtml");
1317                assert!(manifest_item.is_ok());
1318
1319                let mut manifest_item = manifest_item.unwrap();
1320                manifest_item
1321                    .append_property("scripted")
1322                    .append_property("svg")
1323                    .with_fallback("fallback-content");
1324
1325                let built = manifest_item.build();
1326
1327                assert_eq!(built.id, "content");
1328                assert_eq!(built.path, PathBuf::from("content.xhtml"));
1329                assert_eq!(built.mime, "");
1330                assert_eq!(built.properties, Some("scripted svg".to_string()));
1331                assert_eq!(built.fallback, Some("fallback-content".to_string()));
1332            }
1333
1334            #[test]
1335            fn test_manifest_item_attributes() {
1336                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1337                assert!(manifest_item.is_ok());
1338
1339                let mut manifest_item = manifest_item.unwrap();
1340                manifest_item
1341                    .append_property("nav")
1342                    .with_fallback("fallback-nav");
1343
1344                // Manually set mime type for testing
1345                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1346                let attributes = manifest_item.attributes();
1347
1348                // Check that all expected attributes are present
1349                assert!(attributes.contains(&("id", "nav")));
1350                assert!(attributes.contains(&("href", "nav.xhtml")));
1351                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1352                assert!(attributes.contains(&("properties", "nav")));
1353                assert!(attributes.contains(&("fallback", "fallback-nav")));
1354            }
1355
1356            #[test]
1357            fn test_manifest_item_attributes_optional_fields() {
1358                let manifest_item = ManifestItem::new("simple", "simple.xhtml");
1359                assert!(manifest_item.is_ok());
1360
1361                let manifest_item = manifest_item.unwrap();
1362                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1363                let attributes = manifest_item.attributes();
1364
1365                // Should contain required attributes
1366                assert!(attributes.contains(&("id", "simple")));
1367                assert!(attributes.contains(&("href", "simple.xhtml")));
1368                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1369
1370                // Should not contain optional attributes when they are None
1371                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1372                assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
1373            }
1374
1375            #[test]
1376            fn test_manifest_item_path_handling() {
1377                let manifest_item = ManifestItem::new("test", "../images/test.png");
1378                assert!(manifest_item.is_err());
1379
1380                let err = manifest_item.unwrap_err();
1381                assert_eq!(
1382                    err.to_string(),
1383                    "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
1384                );
1385            }
1386        }
1387
1388        mod spine_item {
1389            use crate::types::SpineItem;
1390
1391            #[test]
1392            fn test_spine_item_new() {
1393                let spine_item = SpineItem::new("content_001");
1394
1395                assert_eq!(spine_item.idref, "content_001");
1396                assert_eq!(spine_item.id, None);
1397                assert_eq!(spine_item.properties, None);
1398                assert_eq!(spine_item.linear, true);
1399            }
1400
1401            #[test]
1402            fn test_spine_item_with_id() {
1403                let mut spine_item = SpineItem::new("content_001");
1404                spine_item.with_id("spine1");
1405
1406                assert_eq!(spine_item.idref, "content_001");
1407                assert_eq!(spine_item.id, Some("spine1".to_string()));
1408                assert_eq!(spine_item.properties, None);
1409                assert_eq!(spine_item.linear, true);
1410            }
1411
1412            #[test]
1413            fn test_spine_item_append_property() {
1414                let mut spine_item = SpineItem::new("content_001");
1415                spine_item.append_property("page-spread-left");
1416
1417                assert_eq!(spine_item.idref, "content_001");
1418                assert_eq!(spine_item.id, None);
1419                assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
1420                assert_eq!(spine_item.linear, true);
1421            }
1422
1423            #[test]
1424            fn test_spine_item_append_multiple_properties() {
1425                let mut spine_item = SpineItem::new("content_001");
1426                spine_item
1427                    .append_property("page-spread-left")
1428                    .append_property("rendition:layout-pre-paginated");
1429
1430                assert_eq!(
1431                    spine_item.properties,
1432                    Some("page-spread-left rendition:layout-pre-paginated".to_string())
1433                );
1434            }
1435
1436            #[test]
1437            fn test_spine_item_set_linear() {
1438                let mut spine_item = SpineItem::new("content_001");
1439                spine_item.set_linear(false);
1440
1441                assert_eq!(spine_item.idref, "content_001");
1442                assert_eq!(spine_item.id, None);
1443                assert_eq!(spine_item.properties, None);
1444                assert_eq!(spine_item.linear, false);
1445            }
1446
1447            #[test]
1448            fn test_spine_item_build() {
1449                let mut spine_item = SpineItem::new("content_001");
1450                spine_item
1451                    .with_id("spine1")
1452                    .append_property("page-spread-left")
1453                    .set_linear(false);
1454
1455                let built = spine_item.build();
1456
1457                assert_eq!(built.idref, "content_001");
1458                assert_eq!(built.id, Some("spine1".to_string()));
1459                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1460                assert_eq!(built.linear, false);
1461            }
1462
1463            #[test]
1464            fn test_spine_item_builder_chaining() {
1465                let mut spine_item = SpineItem::new("content_001");
1466                spine_item
1467                    .with_id("spine1")
1468                    .append_property("page-spread-left")
1469                    .set_linear(false);
1470
1471                let built = spine_item.build();
1472
1473                assert_eq!(built.idref, "content_001");
1474                assert_eq!(built.id, Some("spine1".to_string()));
1475                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1476                assert_eq!(built.linear, false);
1477            }
1478
1479            #[test]
1480            fn test_spine_item_attributes() {
1481                let mut spine_item = SpineItem::new("content_001");
1482                spine_item
1483                    .with_id("spine1")
1484                    .append_property("page-spread-left")
1485                    .set_linear(false);
1486
1487                let attributes = spine_item.attributes();
1488
1489                // Check that all expected attributes are present
1490                assert!(attributes.contains(&("idref", "content_001")));
1491                assert!(attributes.contains(&("id", "spine1")));
1492                assert!(attributes.contains(&("properties", "page-spread-left")));
1493                assert!(attributes.contains(&("linear", "no"))); // false should become "no"
1494            }
1495
1496            #[test]
1497            fn test_spine_item_attributes_linear_yes() {
1498                let spine_item = SpineItem::new("content_001");
1499                let attributes = spine_item.attributes();
1500
1501                // Linear true should become "yes"
1502                assert!(attributes.contains(&("linear", "yes")));
1503            }
1504
1505            #[test]
1506            fn test_spine_item_attributes_optional_fields() {
1507                let spine_item = SpineItem::new("content_001");
1508                let attributes = spine_item.attributes();
1509
1510                // Should only contain required attributes when optional fields are None
1511                assert!(attributes.contains(&("idref", "content_001")));
1512                assert!(attributes.contains(&("linear", "yes")));
1513
1514                // Should not contain optional attributes when they are None
1515                assert!(!attributes.iter().any(|(k, _)| k == &"id"));
1516                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1517            }
1518        }
1519
1520        mod navpoint {
1521
1522            use std::path::PathBuf;
1523
1524            use crate::types::NavPoint;
1525
1526            #[test]
1527            fn test_navpoint_new() {
1528                let navpoint = NavPoint::new("Test Chapter");
1529
1530                assert_eq!(navpoint.label, "Test Chapter");
1531                assert_eq!(navpoint.content, None);
1532                assert_eq!(navpoint.children.len(), 0);
1533            }
1534
1535            #[test]
1536            fn test_navpoint_with_content() {
1537                let mut navpoint = NavPoint::new("Test Chapter");
1538                navpoint.with_content("chapter1.html");
1539
1540                assert_eq!(navpoint.label, "Test Chapter");
1541                assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
1542                assert_eq!(navpoint.children.len(), 0);
1543            }
1544
1545            #[test]
1546            fn test_navpoint_append_child() {
1547                let mut parent = NavPoint::new("Parent Chapter");
1548
1549                let mut child1 = NavPoint::new("Child Section 1");
1550                child1.with_content("section1.html");
1551
1552                let mut child2 = NavPoint::new("Child Section 2");
1553                child2.with_content("section2.html");
1554
1555                parent.append_child(child1.build());
1556                parent.append_child(child2.build());
1557
1558                assert_eq!(parent.children.len(), 2);
1559                assert_eq!(parent.children[0].label, "Child Section 1");
1560                assert_eq!(parent.children[1].label, "Child Section 2");
1561            }
1562
1563            #[test]
1564            fn test_navpoint_set_children() {
1565                let mut navpoint = NavPoint::new("Main Chapter");
1566                let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
1567
1568                navpoint.set_children(children);
1569
1570                assert_eq!(navpoint.children.len(), 2);
1571                assert_eq!(navpoint.children[0].label, "Section 1");
1572                assert_eq!(navpoint.children[1].label, "Section 2");
1573            }
1574
1575            #[test]
1576            fn test_navpoint_build() {
1577                let mut navpoint = NavPoint::new("Complete Chapter");
1578                navpoint.with_content("complete.html");
1579
1580                let child = NavPoint::new("Sub Section");
1581                navpoint.append_child(child.build());
1582
1583                let built = navpoint.build();
1584
1585                assert_eq!(built.label, "Complete Chapter");
1586                assert_eq!(built.content, Some(PathBuf::from("complete.html")));
1587                assert_eq!(built.children.len(), 1);
1588                assert_eq!(built.children[0].label, "Sub Section");
1589            }
1590
1591            #[test]
1592            fn test_navpoint_builder_chaining() {
1593                let mut navpoint = NavPoint::new("Chained Chapter");
1594
1595                navpoint
1596                    .with_content("chained.html")
1597                    .append_child(NavPoint::new("Child 1").build())
1598                    .append_child(NavPoint::new("Child 2").build());
1599
1600                let built = navpoint.build();
1601
1602                assert_eq!(built.label, "Chained Chapter");
1603                assert_eq!(built.content, Some(PathBuf::from("chained.html")));
1604                assert_eq!(built.children.len(), 2);
1605            }
1606
1607            #[test]
1608            fn test_navpoint_empty_children() {
1609                let navpoint = NavPoint::new("No Children Chapter");
1610                let built = navpoint.build();
1611
1612                assert_eq!(built.children.len(), 0);
1613            }
1614
1615            #[test]
1616            fn test_navpoint_complex_hierarchy() {
1617                let mut root = NavPoint::new("Book");
1618
1619                let mut chapter1 = NavPoint::new("Chapter 1");
1620                chapter1
1621                    .with_content("chapter1.html")
1622                    .append_child(
1623                        NavPoint::new("Section 1.1")
1624                            .with_content("sec1_1.html")
1625                            .build(),
1626                    )
1627                    .append_child(
1628                        NavPoint::new("Section 1.2")
1629                            .with_content("sec1_2.html")
1630                            .build(),
1631                    );
1632
1633                let mut chapter2 = NavPoint::new("Chapter 2");
1634                chapter2.with_content("chapter2.html").append_child(
1635                    NavPoint::new("Section 2.1")
1636                        .with_content("sec2_1.html")
1637                        .build(),
1638                );
1639
1640                root.append_child(chapter1.build())
1641                    .append_child(chapter2.build());
1642
1643                let book = root.build();
1644
1645                assert_eq!(book.label, "Book");
1646                assert_eq!(book.children.len(), 2);
1647
1648                let ch1 = &book.children[0];
1649                assert_eq!(ch1.label, "Chapter 1");
1650                assert_eq!(ch1.children.len(), 2);
1651
1652                let ch2 = &book.children[1];
1653                assert_eq!(ch2.label, "Chapter 2");
1654                assert_eq!(ch2.children.len(), 1);
1655            }
1656        }
1657    }
1658}