Skip to main content

lib_epub/
types.rs

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