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