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::{collections::HashMap, 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/// A unified metadata sheet for EPUB publications
331///
332/// This struct provides a simplified, high-level interface for accessing EPUB metadata.
333/// It consolidates metadata from both EPUB 2 and EPUB 3 specifications into a single
334/// convenient structure, with separate storage for multi-value fields and single-value fields.
335#[derive(Debug, Default)]
336pub struct MetadataSheet {
337    /// Contributors to the publication (e.g., editors, translators)
338    pub contributor: Vec<String>,
339    /// Primary creators/authors of the publication
340    pub creator: Vec<String>,
341    /// Date information with optional event types (e.g., publication, creation)
342    pub date: HashMap<String, String>,
343    /// Unique identifiers with their assigned IDs as keys
344    pub identifier: HashMap<String, String>,
345    /// Language codes for the publication content
346    pub language: Vec<String>,
347    /// References to related resources
348    pub relation: Vec<String>,
349    /// Subject keywords or topics
350    pub subject: Vec<String>,
351    /// Title(s) of the publication
352    pub title: Vec<String>,
353
354    /// Spatial or temporal coverage of the publication
355    pub coverage: String,
356    /// Description or abstract of the publication
357    pub description: String,
358    /// Physical or digital format of the publication
359    pub format: String,
360    /// Publisher information
361    pub publisher: String,
362    /// Copyright and licensing rights
363    pub rights: String,
364    /// Reference to the source publication
365    pub source: String,
366    /// EPUB-specific type identifier
367    pub epub_type: String,
368}
369
370impl MetadataSheet {
371    /// Creates a new MetadataSheet instance
372    pub fn new() -> Self {
373        Self {
374            contributor: Vec::new(),
375            creator: Vec::new(),
376            date: HashMap::new(),
377            identifier: HashMap::new(),
378            language: Vec::new(),
379            relation: Vec::new(),
380            subject: Vec::new(),
381            title: Vec::new(),
382
383            coverage: String::new(),
384            description: String::new(),
385            format: String::new(),
386            publisher: String::new(),
387            rights: String::new(),
388            source: String::new(),
389            epub_type: String::new(),
390        }
391    }
392}
393
394#[cfg(feature = "builder")]
395impl MetadataSheet {
396    /// Appends a contributor to the metadata
397    pub fn append_contributor(&mut self, contributor: impl Into<String>) -> &mut Self {
398        self.contributor.push(contributor.into());
399        self
400    }
401
402    /// Appends a creator to the metadata
403    pub fn append_creator(&mut self, creator: impl Into<String>) -> &mut Self {
404        self.creator.push(creator.into());
405        self
406    }
407
408    /// Appends a language to the metadata
409    pub fn append_language(&mut self, language: impl Into<String>) -> &mut Self {
410        self.language.push(language.into());
411        self
412    }
413
414    /// Appends a relation to the metadata
415    pub fn append_relation(&mut self, relation: impl Into<String>) -> &mut Self {
416        self.relation.push(relation.into());
417        self
418    }
419
420    /// Appends a subject to the metadata
421    pub fn append_subject(&mut self, subject: impl Into<String>) -> &mut Self {
422        self.subject.push(subject.into());
423        self
424    }
425
426    /// Appends a title to the metadata
427    pub fn append_title(&mut self, title: impl Into<String>) -> &mut Self {
428        self.title.push(title.into());
429        self
430    }
431
432    /// Sets a date value with optional event type
433    ///
434    /// Parameters:
435    /// - `date`: The date value (used as key to allow multiple dates)
436    /// - `event`: Optional event type (e.g., "publication", "creation", "modification")
437    ///
438    /// Note: Multiple dates can be stored. The date string is used as the key,
439    /// and the event type (if any) is stored as the value.
440    pub fn append_date(&mut self, date: impl Into<String>, event: impl Into<String>) -> &mut Self {
441        self.date.insert(date.into(), event.into());
442        self
443    }
444
445    /// Sets an identifier with id (e.g., "book-id", "isbn-id")
446    pub fn append_identifier(
447        &mut self,
448        id: impl Into<String>,
449        value: impl Into<String>,
450    ) -> &mut Self {
451        self.identifier.insert(id.into(), value.into());
452        self
453    }
454
455    /// Sets coverage
456    pub fn with_coverage(&mut self, coverage: impl Into<String>) -> &mut Self {
457        self.coverage = coverage.into();
458        self
459    }
460
461    /// Sets description
462    pub fn with_description(&mut self, description: impl Into<String>) -> &mut Self {
463        self.description = description.into();
464        self
465    }
466
467    /// Sets format
468    pub fn with_format(&mut self, format: impl Into<String>) -> &mut Self {
469        self.format = format.into();
470        self
471    }
472
473    /// Sets publisher
474    pub fn with_publisher(&mut self, publisher: impl Into<String>) -> &mut Self {
475        self.publisher = publisher.into();
476        self
477    }
478
479    /// Sets rights
480    pub fn with_rights(&mut self, rights: impl Into<String>) -> &mut Self {
481        self.rights = rights.into();
482        self
483    }
484
485    /// Sets source
486    pub fn with_source(&mut self, source: impl Into<String>) -> &mut Self {
487        self.source = source.into();
488        self
489    }
490
491    /// Sets epub type
492    pub fn with_epub_type(&mut self, epub_type: impl Into<String>) -> &mut Self {
493        self.epub_type = epub_type.into();
494        self
495    }
496
497    /// Builds the Metadata instance (returns a clone)
498    pub fn build(&self) -> MetadataSheet {
499        MetadataSheet {
500            contributor: self.contributor.clone(),
501            creator: self.creator.clone(),
502            date: self.date.clone(),
503            identifier: self.identifier.clone(),
504            language: self.language.clone(),
505            relation: self.relation.clone(),
506            subject: self.subject.clone(),
507            title: self.title.clone(),
508            coverage: self.coverage.clone(),
509            description: self.description.clone(),
510            format: self.format.clone(),
511            publisher: self.publisher.clone(),
512            rights: self.rights.clone(),
513            source: self.source.clone(),
514            epub_type: self.epub_type.clone(),
515        }
516    }
517}
518
519#[cfg(feature = "builder")]
520impl From<MetadataSheet> for Vec<MetadataItem> {
521    /// Converts a `MetadataSheet` into a `Vec<MetadataItem>` for EPUB use
522    ///
523    /// This conversion maps Dublin Core metadata fields from `MetadataSheet` to
524    /// the EPUB-compliant `MetadataItem` format. Each field in `MetadataSheet`
525    /// is converted to a corresponding `MetadataItem`.
526    fn from(sheet: MetadataSheet) -> Vec<MetadataItem> {
527        let mut items = Vec::new();
528
529        // Dublin Core Vector Fields - multiple values become separate MetadataItems
530
531        for title in &sheet.title {
532            items.push(MetadataItem::new("title", title));
533        }
534
535        for creator in &sheet.creator {
536            items.push(MetadataItem::new("creator", creator));
537        }
538
539        for contributor in &sheet.contributor {
540            items.push(MetadataItem::new("contributor", contributor));
541        }
542
543        for subject in &sheet.subject {
544            items.push(MetadataItem::new("subject", subject));
545        }
546
547        for language in &sheet.language {
548            items.push(MetadataItem::new("language", language));
549        }
550
551        for relation in &sheet.relation {
552            items.push(MetadataItem::new("relation", relation));
553        }
554
555        // Dublin Core HashMap Fields - date and identifier have key-value structure
556        // For date: key is used as refinement property "event", value is the date
557        // For identifier: key is used as the xml:id attribute
558        for (date, event) in &sheet.date {
559            let mut item = MetadataItem::new("date", date);
560            if !event.is_empty() {
561                let refinement_id = format!("date-{}", items.len());
562                item.id = Some(refinement_id.clone());
563                item.refined
564                    .push(MetadataRefinement::new(&refinement_id, "event", event));
565            }
566            items.push(item);
567        }
568
569        for (id, value) in &sheet.identifier {
570            let mut item = MetadataItem::new("identifier", value);
571            if !id.is_empty() {
572                item.id = Some(id.clone());
573            }
574            items.push(item);
575        }
576
577        // Dublin Core Scalar Fields - single-value fields
578
579        if !sheet.description.is_empty() {
580            items.push(MetadataItem::new("description", &sheet.description));
581        }
582
583        if !sheet.format.is_empty() {
584            items.push(MetadataItem::new("format", &sheet.format));
585        }
586
587        if !sheet.publisher.is_empty() {
588            items.push(MetadataItem::new("publisher", &sheet.publisher));
589        }
590
591        if !sheet.rights.is_empty() {
592            items.push(MetadataItem::new("rights", &sheet.rights));
593        }
594
595        if !sheet.source.is_empty() {
596            items.push(MetadataItem::new("source", &sheet.source));
597        }
598
599        if !sheet.coverage.is_empty() {
600            items.push(MetadataItem::new("coverage", &sheet.coverage));
601        }
602
603        if !sheet.epub_type.is_empty() {
604            items.push(MetadataItem::new("type", &sheet.epub_type));
605        }
606
607        items
608    }
609}
610
611/// Represents a resource item declared in the EPUB manifest
612///
613/// The `ManifestItem` structure represents a single resource file declared in the EPUB
614/// publication's manifest. Each manifest item describes a resource that is part of the
615/// publication, including its location, media type, and optional properties or fallback
616/// relationships.
617///
618/// The manifest serves as a comprehensive inventory of all resources in an EPUB publication.
619/// Every resource that is part of the publication must be declared in the manifest, and
620/// resources not listed in the manifest should not be accessed by reading systems.
621///
622/// Manifest items support the fallback mechanism, allowing alternative versions of a resource
623/// to be specified. This is particularly important for foreign resources (resources with
624/// non-core media types) that may not be supported by all reading systems.
625///
626/// ## Builder Methods
627///
628/// When the `builder` feature is enabled, this struct provides convenient builder methods:
629///
630/// ```
631/// # #[cfg(feature = "builder")] {
632/// use lib_epub::types::ManifestItem;
633///
634/// let manifest_item = ManifestItem::new("cover", "images/cover.jpg")
635///     .unwrap()
636///     .append_property("cover-image")
637///     .with_fallback("cover-fallback")
638///     .build();
639/// # }
640/// ```
641#[derive(Debug, Clone)]
642pub struct ManifestItem {
643    /// The unique identifier for this resource item
644    pub id: String,
645
646    /// The path to the resource file within the EPUB container
647    ///
648    /// This field contains the normalized path to the resource file relative to the
649    /// root of the EPUB container. The path is processed during parsing to handle
650    /// various EPUB path conventions (absolute paths, relative paths, etc.).
651    pub path: PathBuf,
652
653    /// The media type of the resource
654    pub mime: String,
655
656    /// Optional properties associated with this resource
657    ///
658    /// This field contains a space-separated list of properties that apply to this
659    /// resource. Properties provide additional information about how the resource
660    /// should be treated.
661    pub properties: Option<String>,
662
663    /// Optional fallback resource identifier
664    ///
665    /// This field specifies the ID of another manifest item that serves as a fallback
666    /// for this resource. Fallbacks are used when a reading system does not support
667    /// the media type of the primary resource. The fallback chain allows publications
668    /// to include foreign resources while maintaining compatibility with older or
669    /// simpler reading systems.
670    ///
671    /// The value is the ID of another manifest item, which must exist in the manifest.
672    /// If `None`, this resource has no fallback.
673    pub fallback: Option<String>,
674}
675
676#[cfg(feature = "builder")]
677impl ManifestItem {
678    /// Creates a new manifest item
679    ///
680    /// Requires the `builder` feature.
681    ///
682    /// ## Parameters
683    /// - `id` - The unique identifier for this resource
684    /// - `path` - The path to the resource file
685    ///
686    /// ## Errors
687    /// Returns an error if the path starts with "../" which is not allowed.
688    pub fn new(id: &str, path: &str) -> Result<Self, EpubError> {
689        if path.starts_with("../") {
690            return Err(
691                EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() }.into(),
692            );
693        }
694
695        Ok(Self {
696            id: id.to_string(),
697            path: PathBuf::from(path),
698            mime: String::new(),
699            properties: None,
700            fallback: None,
701        })
702    }
703
704    /// Sets the MIME type of the manifest item
705    pub(crate) fn set_mime(self, mime: &str) -> Self {
706        Self {
707            id: self.id,
708            path: self.path,
709            mime: mime.to_string(),
710            properties: self.properties,
711            fallback: self.fallback,
712        }
713    }
714
715    /// Appends a property to the manifest item
716    ///
717    /// Requires the `builder` feature.
718    ///
719    /// ## Parameters
720    /// - `property` - The property to add
721    pub fn append_property(&mut self, property: &str) -> &mut Self {
722        let new_properties = if let Some(properties) = &self.properties {
723            format!("{} {}", properties, property)
724        } else {
725            property.to_string()
726        };
727
728        self.properties = Some(new_properties);
729        self
730    }
731
732    /// Sets the fallback for this manifest item
733    ///
734    /// Requires the `builder` feature.
735    ///
736    /// ## Parameters
737    /// - `fallback` - The ID of the fallback manifest item
738    pub fn with_fallback(&mut self, fallback: &str) -> &mut Self {
739        self.fallback = Some(fallback.to_string());
740        self
741    }
742
743    /// Builds the final manifest item
744    ///
745    /// Requires the `builder` feature.
746    pub fn build(&self) -> Self {
747        Self { ..self.clone() }
748    }
749
750    /// Gets the XML attributes for this manifest item
751    pub fn attributes(&self) -> Vec<(&str, &str)> {
752        let mut attributes = Vec::new();
753
754        attributes.push(("id", self.id.as_str()));
755        attributes.push(("href", self.path.to_str().unwrap()));
756        attributes.push(("media-type", self.mime.as_str()));
757
758        if let Some(properties) = &self.properties {
759            attributes.push(("properties", properties.as_str()));
760        }
761
762        if let Some(fallback) = &self.fallback {
763            attributes.push(("fallback", fallback.as_str()));
764        }
765
766        attributes
767    }
768}
769
770/// Represents an item in the EPUB spine, defining the reading order of the publication
771///
772/// The `SpineItem` structure represents a single item in the EPUB spine, which defines
773/// the linear reading order of the publication's content documents. Each spine item
774/// references a resource declared in the manifest and indicates whether it should be
775/// included in the linear reading sequence.
776///
777/// The spine is a crucial component of an EPUB publication as it determines the recommended
778/// reading order of content documents. Items can be marked as linear (part of the main reading
779/// flow) or non-linear (supplementary content that may be accessed out of sequence).
780///
781/// ## Builder Methods
782///
783/// When the `builder` feature is enabled, this struct provides convenient builder methods:
784///
785/// ```
786/// # #[cfg(feature = "builder")] {
787/// use lib_epub::types::SpineItem;
788///
789/// let spine_item = SpineItem::new("content-1")
790///     .with_id("spine-1")
791///     .append_property("page-spread-right")
792///     .set_linear(false)
793///     .build();
794/// # }
795/// ```
796#[derive(Debug, Clone)]
797pub struct SpineItem {
798    /// The ID reference to a manifest item
799    ///
800    /// This field contains the ID of the manifest item that this spine item references.
801    /// It establishes the connection between the reading order (spine) and the actual
802    /// content resources (manifest). The referenced ID must exist in the manifest.
803    pub idref: String,
804
805    /// Optional identifier for this spine item
806    pub id: Option<String>,
807
808    /// Optional properties associated with this spine item
809    ///
810    /// This field contains a space-separated list of properties that apply to this
811    /// spine item. These properties can indicate special handling requirements,
812    /// layout preferences, or other characteristics.
813    pub properties: Option<String>,
814
815    /// Indicates whether this item is part of the linear reading order
816    ///
817    /// When `true`, this spine item is part of the main linear reading sequence.
818    /// When `false`, this item represents supplementary content that may be accessed
819    /// out of the normal reading order (e.g., through hyperlinks).
820    ///
821    /// Non-linear items are typically used for content like footnotes, endnotes,
822    /// appendices, or other supplementary materials that readers might access
823    /// on-demand rather than sequentially.
824    pub linear: bool,
825}
826
827#[cfg(feature = "builder")]
828impl SpineItem {
829    /// Creates a new spine item referencing a manifest item
830    ///
831    /// Requires the `builder` feature.
832    ///
833    /// By default, spine items are linear.
834    ///
835    /// ## Parameters
836    /// - `idref` - The ID of the manifest item this spine item references
837    pub fn new(idref: &str) -> Self {
838        Self {
839            idref: idref.to_string(),
840            id: None,
841            properties: None,
842            linear: true,
843        }
844    }
845
846    /// Sets the ID of the spine item
847    ///
848    /// Requires the `builder` feature.
849    ///
850    /// ## Parameters
851    /// - `id` - The ID to assign to this spine item
852    pub fn with_id(&mut self, id: &str) -> &mut Self {
853        self.id = Some(id.to_string());
854        self
855    }
856
857    /// Appends a property to the spine item
858    ///
859    /// Requires the `builder` feature.
860    ///
861    /// ## Parameters
862    /// - `property` - The property to add
863    pub fn append_property(&mut self, property: &str) -> &mut Self {
864        let new_properties = if let Some(properties) = &self.properties {
865            format!("{} {}", properties, property)
866        } else {
867            property.to_string()
868        };
869
870        self.properties = Some(new_properties);
871        self
872    }
873
874    /// Sets whether this spine item is part of the linear reading order
875    ///
876    /// Requires the `builder` feature.
877    ///
878    /// ## Parameters
879    /// - `linear` - `true` if the item is part of the linear reading order, `false` otherwise
880    pub fn set_linear(&mut self, linear: bool) -> &mut Self {
881        self.linear = linear;
882        self
883    }
884
885    /// Builds the final spine item
886    ///
887    /// Requires the `builder` feature.
888    pub fn build(&self) -> Self {
889        Self { ..self.clone() }
890    }
891
892    /// Gets the XML attributes for this spine item
893    pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
894        let mut attributes = Vec::new();
895
896        attributes.push(("idref", self.idref.as_str()));
897        attributes.push(("linear", if self.linear { "yes" } else { "no" }));
898
899        if let Some(id) = &self.id {
900            attributes.push(("id", id.as_str()));
901        }
902
903        if let Some(properties) = &self.properties {
904            attributes.push(("properties", properties.as_str()));
905        }
906
907        attributes
908    }
909}
910
911/// Represents encryption information for EPUB resources
912///
913/// This structure holds information about encrypted resources in an EPUB publication,
914/// as defined in the META-INF/encryption.xml file according to the EPUB specification.
915/// It describes which resources are encrypted and what encryption method was used.
916#[derive(Debug, Clone)]
917pub struct EncryptionData {
918    /// The encryption algorithm URI
919    ///
920    /// This field specifies the encryption method used for the resource.
921    /// Supported encryption methods:
922    /// - IDPF font obfuscation: <http://www.idpf.org/2008/embedding>
923    /// - Adobe font obfuscation: <http://ns.adobe.com/pdf/enc#RC>
924    pub method: String,
925
926    /// The URI of the encrypted resource
927    ///
928    /// This field contains the path/URI to the encrypted resource within the EPUB container.
929    /// The path is relative to the root of the EPUB container.
930    pub data: String,
931}
932
933/// Represents a navigation point in an EPUB document's table of contents
934///
935/// The `NavPoint` structure represents a single entry in the hierarchical table of contents
936/// of an EPUB publication. Each navigation point corresponds to a section or chapter in
937/// the publication and may contain nested child navigation points to represent sub-sections.
938///
939/// ## Builder Methods
940///
941/// When the `builder` feature is enabled, this struct provides convenient builder methods:
942///
943/// ```
944/// # #[cfg(feature = "builder")] {
945/// use lib_epub::types::NavPoint;
946///
947/// let nav_point = NavPoint::new("Chapter 1")
948///     .with_content("chapter1.xhtml")
949///     .append_child(
950///         NavPoint::new("Section 1.1")
951///             .with_content("section1_1.xhtml")
952///             .build()
953///     )
954///     .build();
955/// # }
956/// ```
957#[derive(Debug, Eq, Clone)]
958pub struct NavPoint {
959    /// The display label/title of this navigation point
960    ///
961    /// This is the text that should be displayed to users in the table of contents.
962    pub label: String,
963
964    /// The content document path this navigation point references
965    ///
966    /// Can be `None` for navigation points that no relevant information was
967    /// provided in the original data.
968    pub content: Option<PathBuf>,
969
970    /// Child navigation points (sub-sections)
971    pub children: Vec<NavPoint>,
972
973    /// The reading order position of this navigation point
974    ///
975    /// It can be `None` for navigation points that no relevant information was
976    /// provided in the original data.
977    pub play_order: Option<usize>,
978}
979
980#[cfg(feature = "builder")]
981impl NavPoint {
982    /// Creates a new navigation point with the given label
983    ///
984    /// Requires the `builder` feature.
985    ///
986    /// ## Parameters
987    /// - `label` - The display label for this navigation point
988    pub fn new(label: &str) -> Self {
989        Self {
990            label: label.to_string(),
991            content: None,
992            children: vec![],
993            play_order: None,
994        }
995    }
996
997    /// Sets the content path for this navigation point
998    ///
999    /// Requires the `builder` feature.
1000    ///
1001    /// ## Parameters
1002    /// - `content` - The path to the content document
1003    pub fn with_content(&mut self, content: &str) -> &mut Self {
1004        self.content = Some(PathBuf::from(content));
1005        self
1006    }
1007
1008    /// Appends a child navigation point
1009    ///
1010    /// Requires the `builder` feature.
1011    ///
1012    /// ## Parameters
1013    /// - `child` - The child navigation point to add
1014    pub fn append_child(&mut self, child: NavPoint) -> &mut Self {
1015        self.children.push(child);
1016        self
1017    }
1018
1019    /// Sets all child navigation points
1020    ///
1021    /// Requires the `builder` feature.
1022    ///
1023    /// ## Parameters
1024    /// - `children` - Vector of child navigation points
1025    pub fn set_children(&mut self, children: Vec<NavPoint>) -> &mut Self {
1026        self.children = children;
1027        self
1028    }
1029
1030    /// Builds the final navigation point
1031    ///
1032    /// Requires the `builder` feature.
1033    pub fn build(&self) -> Self {
1034        Self { ..self.clone() }
1035    }
1036}
1037
1038impl Ord for NavPoint {
1039    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1040        self.play_order.cmp(&other.play_order)
1041    }
1042}
1043
1044impl PartialOrd for NavPoint {
1045    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1046        Some(self.cmp(other))
1047    }
1048}
1049
1050impl PartialEq for NavPoint {
1051    fn eq(&self, other: &Self) -> bool {
1052        self.play_order == other.play_order
1053    }
1054}
1055
1056/// Represents a footnote in an EPUB content document
1057///
1058/// This structure represents a footnote in an EPUB content document.
1059/// It contains the location within the content document and the content of the footnote.
1060#[cfg(feature = "content-builder")]
1061#[derive(Debug, Clone, Eq, PartialEq)]
1062pub struct Footnote {
1063    /// The position/location of the footnote reference in the content
1064    pub locate: usize,
1065
1066    /// The text content of the footnote
1067    pub content: String,
1068}
1069
1070#[cfg(feature = "content-builder")]
1071impl Ord for Footnote {
1072    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1073        self.locate.cmp(&other.locate)
1074    }
1075}
1076
1077#[cfg(feature = "content-builder")]
1078impl PartialOrd for Footnote {
1079    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1080        Some(self.cmp(other))
1081    }
1082}
1083
1084/// Represents the type of a block element in the content document
1085#[cfg(feature = "content-builder")]
1086#[derive(Debug, Copy, Clone)]
1087pub enum BlockType {
1088    /// A text paragraph block
1089    ///
1090    /// Standard paragraph content with text styling applied.
1091    Text,
1092
1093    /// A quotation block
1094    ///
1095    /// Represents quoted or indented text content, typically rendered
1096    /// with visual distinction from regular paragraphs.
1097    Quote,
1098
1099    /// A title or heading block
1100    ///
1101    /// Represents chapter or section titles with appropriate heading styling.
1102    Title,
1103
1104    /// An image block
1105    ///
1106    /// Contains embedded image content with optional caption support.
1107    Image,
1108
1109    /// An audio block
1110    ///
1111    /// Contains audio content for playback within the document.
1112    Audio,
1113
1114    /// A video block
1115    ///
1116    /// Contains video content for playback within the document.
1117    Video,
1118
1119    /// A MathML block
1120    ///
1121    /// Contains mathematical notation using MathML markup for
1122    /// proper mathematical typesetting.
1123    MathML,
1124}
1125
1126#[cfg(feature = "content-builder")]
1127impl std::fmt::Display for BlockType {
1128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1129        match self {
1130            BlockType::Text => write!(f, "Text"),
1131            BlockType::Quote => write!(f, "Quote"),
1132            BlockType::Title => write!(f, "Title"),
1133            BlockType::Image => write!(f, "Image"),
1134            BlockType::Audio => write!(f, "Audio"),
1135            BlockType::Video => write!(f, "Video"),
1136            BlockType::MathML => write!(f, "MathML"),
1137        }
1138    }
1139}
1140
1141/// Configuration options for document styling
1142///
1143/// This struct aggregates all style-related configuration for an EPUB document,
1144/// including text appearance, color scheme, and page layout settings.
1145#[cfg(feature = "content-builder")]
1146#[derive(Debug, Default, Clone)]
1147pub struct StyleOptions {
1148    /// Text styling configuration
1149    pub text: TextStyle,
1150
1151    /// Color scheme configuration
1152    ///
1153    /// Defines the background, text, and link colors for the document.
1154    pub color_scheme: ColorScheme,
1155
1156    /// Page layout configuration
1157    ///
1158    /// Controls margins, text alignment, and paragraph spacing.
1159    pub layout: PageLayout,
1160}
1161
1162#[cfg(feature = "content-builder")]
1163#[cfg(feature = "builder")]
1164impl StyleOptions {
1165    /// Creates a new style options with default values
1166    pub fn new() -> Self {
1167        Self::default()
1168    }
1169
1170    /// Sets the text style configuration
1171    pub fn with_text(&mut self, text: TextStyle) -> &mut Self {
1172        self.text = text;
1173        self
1174    }
1175
1176    /// Sets the color scheme configuration
1177    pub fn with_color_scheme(&mut self, color_scheme: ColorScheme) -> &mut Self {
1178        self.color_scheme = color_scheme;
1179        self
1180    }
1181
1182    /// Sets the page layout configuration
1183    pub fn with_layout(&mut self, layout: PageLayout) -> &mut Self {
1184        self.layout = layout;
1185        self
1186    }
1187
1188    /// Builds the final style options
1189    pub fn build(&self) -> Self {
1190        Self { ..self.clone() }
1191    }
1192}
1193
1194/// Text styling configuration
1195///
1196/// Defines the visual appearance of text content in the document,
1197/// including font properties, sizing, and spacing.
1198#[cfg(feature = "content-builder")]
1199#[derive(Debug, Clone)]
1200pub struct TextStyle {
1201    /// The base font size (default: 1.0, unit: rem)
1202    ///
1203    /// Relative to the root element, providing consistent sizing
1204    /// across different viewing contexts.
1205    pub font_size: f32,
1206
1207    /// The line height (default: 1.6, unit: em)
1208    ///
1209    /// Controls the vertical spacing between lines of text.
1210    /// Values greater than 1.0 increase spacing, while values
1211    /// less than 1.0 compress the text.
1212    pub line_height: f32,
1213
1214    /// The font family stack (default: "-apple-system, Roboto, sans-serif")
1215    ///
1216    /// A comma-separated list of font families to use, with
1217    /// fallback fonts specified for compatibility.
1218    pub font_family: String,
1219
1220    /// The font weight (default: "normal")
1221    ///
1222    /// Controls the thickness of the font strokes. Common values
1223    /// include "normal" and "bold".
1224    pub font_weight: String,
1225
1226    /// The font style (default: "normal")
1227    ///
1228    /// Controls whether the font is normal, italic, or oblique.
1229    /// Common values include "normal" and "italic".
1230    pub font_style: String,
1231
1232    /// The letter spacing (default: "normal")
1233    ///
1234    /// Controls the space between characters. Common values
1235    /// include "normal" or specific lengths like "0.05em".
1236    pub letter_spacing: String,
1237
1238    /// The text indent for paragraphs (default: 2.0, unit: em)
1239    ///
1240    /// Controls the indentation of the first line of paragraphs.
1241    /// A value of 2.0 means the first line is indented by 2 ems.
1242    pub text_indent: f32,
1243}
1244
1245#[cfg(feature = "content-builder")]
1246impl Default for TextStyle {
1247    fn default() -> Self {
1248        Self {
1249            font_size: 1.0,
1250            line_height: 1.6,
1251            font_family: "-apple-system, Roboto, sans-serif".to_string(),
1252            font_weight: "normal".to_string(),
1253            font_style: "normal".to_string(),
1254            letter_spacing: "normal".to_string(),
1255            text_indent: 2.0,
1256        }
1257    }
1258}
1259
1260#[cfg(feature = "content-builder")]
1261impl TextStyle {
1262    /// Creates a new text style with default values
1263    pub fn new() -> Self {
1264        Self::default()
1265    }
1266
1267    /// Sets the font size
1268    pub fn with_font_size(&mut self, font_size: f32) -> &mut Self {
1269        self.font_size = font_size;
1270        self
1271    }
1272
1273    /// Sets the line height
1274    pub fn with_line_height(&mut self, line_height: f32) -> &mut Self {
1275        self.line_height = line_height;
1276        self
1277    }
1278
1279    /// Sets the font family
1280    pub fn with_font_family(&mut self, font_family: &str) -> &mut Self {
1281        self.font_family = font_family.to_string();
1282        self
1283    }
1284
1285    /// Sets the font weight
1286    pub fn with_font_weight(&mut self, font_weight: &str) -> &mut Self {
1287        self.font_weight = font_weight.to_string();
1288        self
1289    }
1290
1291    /// Sets the font style
1292    pub fn with_font_style(&mut self, font_style: &str) -> &mut Self {
1293        self.font_style = font_style.to_string();
1294        self
1295    }
1296
1297    /// Sets the letter spacing
1298    pub fn with_letter_spacing(&mut self, letter_spacing: &str) -> &mut Self {
1299        self.letter_spacing = letter_spacing.to_string();
1300        self
1301    }
1302
1303    /// Sets the text indent
1304    pub fn with_text_indent(&mut self, text_indent: f32) -> &mut Self {
1305        self.text_indent = text_indent;
1306        self
1307    }
1308
1309    /// Builds the final text style
1310    pub fn build(&self) -> Self {
1311        Self { ..self.clone() }
1312    }
1313}
1314
1315/// Color scheme configuration
1316///
1317/// Defines the color palette for the document, including background,
1318/// text, and link colors.
1319#[cfg(feature = "content-builder")]
1320#[derive(Debug, Clone)]
1321pub struct ColorScheme {
1322    /// The background color (default: "#FFFFFF")
1323    ///
1324    /// The fill color for the document body. Specified as a hex color
1325    /// string (e.g., "#FFFFFF" for white).
1326    pub background: String,
1327
1328    /// The text color (default: "#000000")
1329    ///
1330    /// The primary color for text content. Specified as a hex color
1331    /// string (e.g., "#000000" for black).
1332    pub text: String,
1333
1334    /// The link color (default: "#6f6f6f")
1335    ///
1336    /// The color for hyperlinks in the document. Specified as a hex
1337    /// color string (e.g., "#6f6f6f" for gray).
1338    pub link: String,
1339}
1340
1341#[cfg(feature = "content-builder")]
1342impl Default for ColorScheme {
1343    fn default() -> Self {
1344        Self {
1345            background: "#FFFFFF".to_string(),
1346            text: "#000000".to_string(),
1347            link: "#6f6f6f".to_string(),
1348        }
1349    }
1350}
1351
1352#[cfg(feature = "content-builder")]
1353impl ColorScheme {
1354    /// Creates a new color scheme with default values
1355    pub fn new() -> Self {
1356        Self::default()
1357    }
1358
1359    /// Sets the background color
1360    pub fn with_background(&mut self, background: &str) -> &mut Self {
1361        self.background = background.to_string();
1362        self
1363    }
1364
1365    /// Sets the text color
1366    pub fn with_text(&mut self, text: &str) -> &mut Self {
1367        self.text = text.to_string();
1368        self
1369    }
1370
1371    /// Sets the link color
1372    pub fn with_link(&mut self, link: &str) -> &mut Self {
1373        self.link = link.to_string();
1374        self
1375    }
1376
1377    /// Builds the final color scheme
1378    pub fn build(&self) -> Self {
1379        Self { ..self.clone() }
1380    }
1381}
1382
1383/// Page layout configuration
1384///
1385/// Defines the layout properties for pages in the document, including
1386/// margins, text alignment, and paragraph spacing.
1387#[cfg(feature = "content-builder")]
1388#[derive(Debug, Clone)]
1389pub struct PageLayout {
1390    /// The page margin (default: 20, unit: pixels)
1391    ///
1392    /// Controls the space around the content area on each page.
1393    pub margin: usize,
1394
1395    /// The text alignment mode (default: TextAlign::Left)
1396    ///
1397    /// Controls how text is aligned within the content area.
1398    pub text_align: TextAlign,
1399
1400    /// The spacing between paragraphs (default: 16, unit: pixels)
1401    ///
1402    /// Controls the vertical space between block-level elements.
1403    pub paragraph_spacing: usize,
1404}
1405
1406#[cfg(feature = "content-builder")]
1407impl Default for PageLayout {
1408    fn default() -> Self {
1409        Self {
1410            margin: 20,
1411            text_align: Default::default(),
1412            paragraph_spacing: 16,
1413        }
1414    }
1415}
1416
1417#[cfg(feature = "content-builder")]
1418impl PageLayout {
1419    /// Creates a new page layout with default values
1420    pub fn new() -> Self {
1421        Self::default()
1422    }
1423
1424    /// Sets the page margin
1425    pub fn with_margin(&mut self, margin: usize) -> &mut Self {
1426        self.margin = margin;
1427        self
1428    }
1429
1430    /// Sets the text alignment
1431    pub fn with_text_align(&mut self, text_align: TextAlign) -> &mut Self {
1432        self.text_align = text_align;
1433        self
1434    }
1435
1436    /// Sets the paragraph spacing
1437    pub fn with_paragraph_spacing(&mut self, paragraph_spacing: usize) -> &mut Self {
1438        self.paragraph_spacing = paragraph_spacing;
1439        self
1440    }
1441
1442    /// Builds the final page layout
1443    pub fn build(&self) -> Self {
1444        Self { ..self.clone() }
1445    }
1446}
1447
1448/// Text alignment options
1449///
1450/// Defines the available text alignment modes for content in the document.
1451#[cfg(feature = "content-builder")]
1452#[derive(Debug, Default, Clone, Copy, PartialEq)]
1453pub enum TextAlign {
1454    /// Left-aligned text
1455    ///
1456    /// Text is aligned to the left margin, with the right edge ragged.
1457    #[default]
1458    Left,
1459
1460    /// Right-aligned text
1461    ///
1462    /// Text is aligned to the right margin, with the left edge ragged.
1463    Right,
1464
1465    /// Justified text
1466    ///
1467    /// Text is aligned to both margins by adjusting the spacing between
1468    /// words. The left and right edges are both straight.
1469    Justify,
1470
1471    /// Centered text
1472    ///
1473    /// Text is centered within the content area, with both edges ragged.
1474    Center,
1475}
1476
1477#[cfg(feature = "content-builder")]
1478impl std::fmt::Display for TextAlign {
1479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1480        match self {
1481            TextAlign::Left => write!(f, "left"),
1482            TextAlign::Right => write!(f, "right"),
1483            TextAlign::Justify => write!(f, "justify"),
1484            TextAlign::Center => write!(f, "center"),
1485        }
1486    }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491    mod navpoint_tests {
1492        use std::path::PathBuf;
1493
1494        use crate::types::NavPoint;
1495
1496        /// Testing the equality comparison of NavPoint
1497        #[test]
1498        fn test_navpoint_partial_eq() {
1499            let nav1 = NavPoint {
1500                label: "Chapter 1".to_string(),
1501                content: Some(PathBuf::from("chapter1.html")),
1502                children: vec![],
1503                play_order: Some(1),
1504            };
1505
1506            let nav2 = NavPoint {
1507                label: "Chapter 1".to_string(),
1508                content: Some(PathBuf::from("chapter2.html")),
1509                children: vec![],
1510                play_order: Some(1),
1511            };
1512
1513            let nav3 = NavPoint {
1514                label: "Chapter 2".to_string(),
1515                content: Some(PathBuf::from("chapter1.html")),
1516                children: vec![],
1517                play_order: Some(2),
1518            };
1519
1520            assert_eq!(nav1, nav2); // Same play_order, different contents, should be equal
1521            assert_ne!(nav1, nav3); // Different play_order, Same contents, should be unequal
1522        }
1523
1524        /// Test NavPoint sorting comparison
1525        #[test]
1526        fn test_navpoint_ord() {
1527            let nav1 = NavPoint {
1528                label: "Chapter 1".to_string(),
1529                content: Some(PathBuf::from("chapter1.html")),
1530                children: vec![],
1531                play_order: Some(1),
1532            };
1533
1534            let nav2 = NavPoint {
1535                label: "Chapter 2".to_string(),
1536                content: Some(PathBuf::from("chapter2.html")),
1537                children: vec![],
1538                play_order: Some(2),
1539            };
1540
1541            let nav3 = NavPoint {
1542                label: "Chapter 3".to_string(),
1543                content: Some(PathBuf::from("chapter3.html")),
1544                children: vec![],
1545                play_order: Some(3),
1546            };
1547
1548            // Test function cmp
1549            assert!(nav1 < nav2);
1550            assert!(nav2 > nav1);
1551            assert!(nav1 == nav1);
1552
1553            // Test function partial_cmp
1554            assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
1555            assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
1556            assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
1557
1558            // Test function sort
1559            let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
1560            nav_points.sort();
1561            assert_eq!(nav_points, vec![nav1, nav2, nav3]);
1562        }
1563
1564        /// Test the case of None play_order
1565        #[test]
1566        fn test_navpoint_ord_with_none_play_order() {
1567            let nav_with_order = NavPoint {
1568                label: "Chapter 1".to_string(),
1569                content: Some(PathBuf::from("chapter1.html")),
1570                children: vec![],
1571                play_order: Some(1),
1572            };
1573
1574            let nav_without_order = NavPoint {
1575                label: "Preface".to_string(),
1576                content: Some(PathBuf::from("preface.html")),
1577                children: vec![],
1578                play_order: None,
1579            };
1580
1581            assert!(nav_without_order < nav_with_order);
1582            assert!(nav_with_order > nav_without_order);
1583
1584            let nav_without_order2 = NavPoint {
1585                label: "Introduction".to_string(),
1586                content: Some(PathBuf::from("intro.html")),
1587                children: vec![],
1588                play_order: None,
1589            };
1590
1591            assert!(nav_without_order == nav_without_order2);
1592        }
1593
1594        /// Test NavPoint containing child nodes
1595        #[test]
1596        fn test_navpoint_with_children() {
1597            let child1 = NavPoint {
1598                label: "Section 1.1".to_string(),
1599                content: Some(PathBuf::from("section1_1.html")),
1600                children: vec![],
1601                play_order: Some(1),
1602            };
1603
1604            let child2 = NavPoint {
1605                label: "Section 1.2".to_string(),
1606                content: Some(PathBuf::from("section1_2.html")),
1607                children: vec![],
1608                play_order: Some(2),
1609            };
1610
1611            let parent1 = NavPoint {
1612                label: "Chapter 1".to_string(),
1613                content: Some(PathBuf::from("chapter1.html")),
1614                children: vec![child1.clone(), child2.clone()],
1615                play_order: Some(1),
1616            };
1617
1618            let parent2 = NavPoint {
1619                label: "Chapter 1".to_string(),
1620                content: Some(PathBuf::from("chapter1.html")),
1621                children: vec![child1.clone(), child2.clone()],
1622                play_order: Some(1),
1623            };
1624
1625            assert!(parent1 == parent2);
1626
1627            let parent3 = NavPoint {
1628                label: "Chapter 2".to_string(),
1629                content: Some(PathBuf::from("chapter2.html")),
1630                children: vec![child1.clone(), child2.clone()],
1631                play_order: Some(2),
1632            };
1633
1634            assert!(parent1 != parent3);
1635            assert!(parent1 < parent3);
1636        }
1637
1638        /// Test the case where content is None
1639        #[test]
1640        fn test_navpoint_with_none_content() {
1641            let nav1 = NavPoint {
1642                label: "Chapter 1".to_string(),
1643                content: None,
1644                children: vec![],
1645                play_order: Some(1),
1646            };
1647
1648            let nav2 = NavPoint {
1649                label: "Chapter 1".to_string(),
1650                content: None,
1651                children: vec![],
1652                play_order: Some(1),
1653            };
1654
1655            assert!(nav1 == nav2);
1656        }
1657    }
1658
1659    #[cfg(feature = "builder")]
1660    mod builder_tests {
1661        mod metadata_item {
1662            use crate::types::{MetadataItem, MetadataRefinement};
1663
1664            #[test]
1665            fn test_metadata_item_new() {
1666                let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1667
1668                assert_eq!(metadata_item.property, "title");
1669                assert_eq!(metadata_item.value, "EPUB Test Book");
1670                assert_eq!(metadata_item.id, None);
1671                assert_eq!(metadata_item.lang, None);
1672                assert_eq!(metadata_item.refined.len(), 0);
1673            }
1674
1675            #[test]
1676            fn test_metadata_item_with_id() {
1677                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1678                metadata_item.with_id("creator-1");
1679
1680                assert_eq!(metadata_item.property, "creator");
1681                assert_eq!(metadata_item.value, "John Doe");
1682                assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1683                assert_eq!(metadata_item.lang, None);
1684                assert_eq!(metadata_item.refined.len(), 0);
1685            }
1686
1687            #[test]
1688            fn test_metadata_item_with_lang() {
1689                let mut metadata_item = MetadataItem::new("title", "测试书籍");
1690                metadata_item.with_lang("zh-CN");
1691
1692                assert_eq!(metadata_item.property, "title");
1693                assert_eq!(metadata_item.value, "测试书籍");
1694                assert_eq!(metadata_item.id, None);
1695                assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1696                assert_eq!(metadata_item.refined.len(), 0);
1697            }
1698
1699            #[test]
1700            fn test_metadata_item_append_refinement() {
1701                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1702                metadata_item.with_id("creator-1"); // ID is required for refinements
1703
1704                let refinement = MetadataRefinement::new("creator-1", "role", "author");
1705                metadata_item.append_refinement(refinement);
1706
1707                assert_eq!(metadata_item.refined.len(), 1);
1708                assert_eq!(metadata_item.refined[0].refines, "creator-1");
1709                assert_eq!(metadata_item.refined[0].property, "role");
1710                assert_eq!(metadata_item.refined[0].value, "author");
1711            }
1712
1713            #[test]
1714            fn test_metadata_item_append_refinement_without_id() {
1715                let mut metadata_item = MetadataItem::new("title", "Test Book");
1716                // No ID set
1717
1718                let refinement = MetadataRefinement::new("title", "title-type", "main");
1719                metadata_item.append_refinement(refinement);
1720
1721                // Refinement should not be added because metadata item has no ID
1722                assert_eq!(metadata_item.refined.len(), 0);
1723            }
1724
1725            #[test]
1726            fn test_metadata_item_build() {
1727                let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1728                metadata_item.with_id("pub-id").with_lang("en");
1729
1730                let built = metadata_item.build();
1731
1732                assert_eq!(built.property, "identifier");
1733                assert_eq!(built.value, "urn:isbn:1234567890");
1734                assert_eq!(built.id, Some("pub-id".to_string()));
1735                assert_eq!(built.lang, Some("en".to_string()));
1736                assert_eq!(built.refined.len(), 0);
1737            }
1738
1739            #[test]
1740            fn test_metadata_item_builder_chaining() {
1741                let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1742                metadata_item.with_id("title").with_lang("en");
1743
1744                let refinement = MetadataRefinement::new("title", "title-type", "main");
1745                metadata_item.append_refinement(refinement);
1746
1747                let built = metadata_item.build();
1748
1749                assert_eq!(built.property, "title");
1750                assert_eq!(built.value, "EPUB 3.3 Guide");
1751                assert_eq!(built.id, Some("title".to_string()));
1752                assert_eq!(built.lang, Some("en".to_string()));
1753                assert_eq!(built.refined.len(), 1);
1754            }
1755
1756            #[test]
1757            fn test_metadata_item_attributes_dc_namespace() {
1758                let mut metadata_item = MetadataItem::new("title", "Test Book");
1759                metadata_item.with_id("title-id");
1760
1761                let attributes = metadata_item.attributes();
1762
1763                // For DC namespace properties, no "property" attribute should be added
1764                assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1765                assert!(
1766                    attributes
1767                        .iter()
1768                        .any(|(k, v)| k == &"id" && v == &"title-id")
1769                );
1770            }
1771
1772            #[test]
1773            fn test_metadata_item_attributes_non_dc_namespace() {
1774                let mut metadata_item = MetadataItem::new("meta", "value");
1775                metadata_item.with_id("meta-id");
1776
1777                let attributes = metadata_item.attributes();
1778
1779                // For non-DC namespace properties, "property" attribute should be added
1780                assert!(attributes.iter().any(|(k, _)| k == &"property"));
1781                assert!(
1782                    attributes
1783                        .iter()
1784                        .any(|(k, v)| k == &"id" && v == &"meta-id")
1785                );
1786            }
1787
1788            #[test]
1789            fn test_metadata_item_attributes_with_lang() {
1790                let mut metadata_item = MetadataItem::new("title", "Test Book");
1791                metadata_item.with_id("title-id").with_lang("en");
1792
1793                let attributes = metadata_item.attributes();
1794
1795                assert!(
1796                    attributes
1797                        .iter()
1798                        .any(|(k, v)| k == &"id" && v == &"title-id")
1799                );
1800                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1801            }
1802        }
1803
1804        mod metadata_refinement {
1805            use crate::types::MetadataRefinement;
1806
1807            #[test]
1808            fn test_metadata_refinement_new() {
1809                let refinement = MetadataRefinement::new("title", "title-type", "main");
1810
1811                assert_eq!(refinement.refines, "title");
1812                assert_eq!(refinement.property, "title-type");
1813                assert_eq!(refinement.value, "main");
1814                assert_eq!(refinement.lang, None);
1815                assert_eq!(refinement.scheme, None);
1816            }
1817
1818            #[test]
1819            fn test_metadata_refinement_with_lang() {
1820                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1821                refinement.with_lang("en");
1822
1823                assert_eq!(refinement.refines, "creator");
1824                assert_eq!(refinement.property, "role");
1825                assert_eq!(refinement.value, "author");
1826                assert_eq!(refinement.lang, Some("en".to_string()));
1827                assert_eq!(refinement.scheme, None);
1828            }
1829
1830            #[test]
1831            fn test_metadata_refinement_with_scheme() {
1832                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1833                refinement.with_scheme("marc:relators");
1834
1835                assert_eq!(refinement.refines, "creator");
1836                assert_eq!(refinement.property, "role");
1837                assert_eq!(refinement.value, "author");
1838                assert_eq!(refinement.lang, None);
1839                assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1840            }
1841
1842            #[test]
1843            fn test_metadata_refinement_build() {
1844                let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1845                refinement.with_lang("ja").with_scheme("iso-15924");
1846
1847                let built = refinement.build();
1848
1849                assert_eq!(built.refines, "title");
1850                assert_eq!(built.property, "alternate-script");
1851                assert_eq!(built.value, "テスト");
1852                assert_eq!(built.lang, Some("ja".to_string()));
1853                assert_eq!(built.scheme, Some("iso-15924".to_string()));
1854            }
1855
1856            #[test]
1857            fn test_metadata_refinement_builder_chaining() {
1858                let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1859                refinement.with_lang("en").with_scheme("dcterms");
1860
1861                let built = refinement.build();
1862
1863                assert_eq!(built.refines, "creator");
1864                assert_eq!(built.property, "file-as");
1865                assert_eq!(built.value, "Doe, John");
1866                assert_eq!(built.lang, Some("en".to_string()));
1867                assert_eq!(built.scheme, Some("dcterms".to_string()));
1868            }
1869
1870            #[test]
1871            fn test_metadata_refinement_attributes() {
1872                let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1873                refinement.with_lang("en").with_scheme("onix:codelist5");
1874
1875                let attributes = refinement.attributes();
1876
1877                assert!(
1878                    attributes
1879                        .iter()
1880                        .any(|(k, v)| k == &"refines" && v == &"title")
1881                );
1882                assert!(
1883                    attributes
1884                        .iter()
1885                        .any(|(k, v)| k == &"property" && v == &"title-type")
1886                );
1887                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1888                assert!(
1889                    attributes
1890                        .iter()
1891                        .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1892                );
1893            }
1894
1895            #[test]
1896            fn test_metadata_refinement_attributes_optional_fields() {
1897                let refinement = MetadataRefinement::new("creator", "role", "author");
1898                let attributes = refinement.attributes();
1899
1900                assert!(
1901                    attributes
1902                        .iter()
1903                        .any(|(k, v)| k == &"refines" && v == &"creator")
1904                );
1905                assert!(
1906                    attributes
1907                        .iter()
1908                        .any(|(k, v)| k == &"property" && v == &"role")
1909                );
1910
1911                // Should not contain optional attributes when they are None
1912                assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1913                assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1914            }
1915        }
1916
1917        mod manifest_item {
1918            use std::path::PathBuf;
1919
1920            use crate::types::ManifestItem;
1921
1922            #[test]
1923            fn test_manifest_item_new() {
1924                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1925                assert!(manifest_item.is_ok());
1926
1927                let manifest_item = manifest_item.unwrap();
1928                assert_eq!(manifest_item.id, "cover");
1929                assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1930                assert_eq!(manifest_item.mime, "");
1931                assert_eq!(manifest_item.properties, None);
1932                assert_eq!(manifest_item.fallback, None);
1933            }
1934
1935            #[test]
1936            fn test_manifest_item_append_property() {
1937                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1938                assert!(manifest_item.is_ok());
1939
1940                let mut manifest_item = manifest_item.unwrap();
1941                manifest_item.append_property("nav");
1942
1943                assert_eq!(manifest_item.id, "nav");
1944                assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1945                assert_eq!(manifest_item.mime, "");
1946                assert_eq!(manifest_item.properties, Some("nav".to_string()));
1947                assert_eq!(manifest_item.fallback, None);
1948            }
1949
1950            #[test]
1951            fn test_manifest_item_append_multiple_properties() {
1952                let manifest_item = ManifestItem::new("content", "content.xhtml");
1953                assert!(manifest_item.is_ok());
1954
1955                let mut manifest_item = manifest_item.unwrap();
1956                manifest_item
1957                    .append_property("nav")
1958                    .append_property("scripted")
1959                    .append_property("svg");
1960
1961                assert_eq!(
1962                    manifest_item.properties,
1963                    Some("nav scripted svg".to_string())
1964                );
1965            }
1966
1967            #[test]
1968            fn test_manifest_item_with_fallback() {
1969                let manifest_item = ManifestItem::new("image", "image.tiff");
1970                assert!(manifest_item.is_ok());
1971
1972                let mut manifest_item = manifest_item.unwrap();
1973                manifest_item.with_fallback("image-fallback");
1974
1975                assert_eq!(manifest_item.id, "image");
1976                assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1977                assert_eq!(manifest_item.mime, "");
1978                assert_eq!(manifest_item.properties, None);
1979                assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1980            }
1981
1982            #[test]
1983            fn test_manifest_item_set_mime() {
1984                let manifest_item = ManifestItem::new("style", "style.css");
1985                assert!(manifest_item.is_ok());
1986
1987                let manifest_item = manifest_item.unwrap();
1988                let updated_item = manifest_item.set_mime("text/css");
1989
1990                assert_eq!(updated_item.id, "style");
1991                assert_eq!(updated_item.path, PathBuf::from("style.css"));
1992                assert_eq!(updated_item.mime, "text/css");
1993                assert_eq!(updated_item.properties, None);
1994                assert_eq!(updated_item.fallback, None);
1995            }
1996
1997            #[test]
1998            fn test_manifest_item_build() {
1999                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
2000                assert!(manifest_item.is_ok());
2001
2002                let mut manifest_item = manifest_item.unwrap();
2003                manifest_item
2004                    .append_property("cover-image")
2005                    .with_fallback("cover-fallback");
2006
2007                let built = manifest_item.build();
2008
2009                assert_eq!(built.id, "cover");
2010                assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
2011                assert_eq!(built.mime, "");
2012                assert_eq!(built.properties, Some("cover-image".to_string()));
2013                assert_eq!(built.fallback, Some("cover-fallback".to_string()));
2014            }
2015
2016            #[test]
2017            fn test_manifest_item_builder_chaining() {
2018                let manifest_item = ManifestItem::new("content", "content.xhtml");
2019                assert!(manifest_item.is_ok());
2020
2021                let mut manifest_item = manifest_item.unwrap();
2022                manifest_item
2023                    .append_property("scripted")
2024                    .append_property("svg")
2025                    .with_fallback("fallback-content");
2026
2027                let built = manifest_item.build();
2028
2029                assert_eq!(built.id, "content");
2030                assert_eq!(built.path, PathBuf::from("content.xhtml"));
2031                assert_eq!(built.mime, "");
2032                assert_eq!(built.properties, Some("scripted svg".to_string()));
2033                assert_eq!(built.fallback, Some("fallback-content".to_string()));
2034            }
2035
2036            #[test]
2037            fn test_manifest_item_attributes() {
2038                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
2039                assert!(manifest_item.is_ok());
2040
2041                let mut manifest_item = manifest_item.unwrap();
2042                manifest_item
2043                    .append_property("nav")
2044                    .with_fallback("fallback-nav");
2045
2046                // Manually set mime type for testing
2047                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
2048                let attributes = manifest_item.attributes();
2049
2050                // Check that all expected attributes are present
2051                assert!(attributes.contains(&("id", "nav")));
2052                assert!(attributes.contains(&("href", "nav.xhtml")));
2053                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
2054                assert!(attributes.contains(&("properties", "nav")));
2055                assert!(attributes.contains(&("fallback", "fallback-nav")));
2056            }
2057
2058            #[test]
2059            fn test_manifest_item_attributes_optional_fields() {
2060                let manifest_item = ManifestItem::new("simple", "simple.xhtml");
2061                assert!(manifest_item.is_ok());
2062
2063                let manifest_item = manifest_item.unwrap();
2064                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
2065                let attributes = manifest_item.attributes();
2066
2067                // Should contain required attributes
2068                assert!(attributes.contains(&("id", "simple")));
2069                assert!(attributes.contains(&("href", "simple.xhtml")));
2070                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
2071
2072                // Should not contain optional attributes when they are None
2073                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
2074                assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
2075            }
2076
2077            #[test]
2078            fn test_manifest_item_path_handling() {
2079                let manifest_item = ManifestItem::new("test", "../images/test.png");
2080                assert!(manifest_item.is_err());
2081
2082                let err = manifest_item.unwrap_err();
2083                assert_eq!(
2084                    err.to_string(),
2085                    "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
2086                );
2087            }
2088        }
2089
2090        mod spine_item {
2091            use crate::types::SpineItem;
2092
2093            #[test]
2094            fn test_spine_item_new() {
2095                let spine_item = SpineItem::new("content_001");
2096
2097                assert_eq!(spine_item.idref, "content_001");
2098                assert_eq!(spine_item.id, None);
2099                assert_eq!(spine_item.properties, None);
2100                assert_eq!(spine_item.linear, true);
2101            }
2102
2103            #[test]
2104            fn test_spine_item_with_id() {
2105                let mut spine_item = SpineItem::new("content_001");
2106                spine_item.with_id("spine1");
2107
2108                assert_eq!(spine_item.idref, "content_001");
2109                assert_eq!(spine_item.id, Some("spine1".to_string()));
2110                assert_eq!(spine_item.properties, None);
2111                assert_eq!(spine_item.linear, true);
2112            }
2113
2114            #[test]
2115            fn test_spine_item_append_property() {
2116                let mut spine_item = SpineItem::new("content_001");
2117                spine_item.append_property("page-spread-left");
2118
2119                assert_eq!(spine_item.idref, "content_001");
2120                assert_eq!(spine_item.id, None);
2121                assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
2122                assert_eq!(spine_item.linear, true);
2123            }
2124
2125            #[test]
2126            fn test_spine_item_append_multiple_properties() {
2127                let mut spine_item = SpineItem::new("content_001");
2128                spine_item
2129                    .append_property("page-spread-left")
2130                    .append_property("rendition:layout-pre-paginated");
2131
2132                assert_eq!(
2133                    spine_item.properties,
2134                    Some("page-spread-left rendition:layout-pre-paginated".to_string())
2135                );
2136            }
2137
2138            #[test]
2139            fn test_spine_item_set_linear() {
2140                let mut spine_item = SpineItem::new("content_001");
2141                spine_item.set_linear(false);
2142
2143                assert_eq!(spine_item.idref, "content_001");
2144                assert_eq!(spine_item.id, None);
2145                assert_eq!(spine_item.properties, None);
2146                assert_eq!(spine_item.linear, false);
2147            }
2148
2149            #[test]
2150            fn test_spine_item_build() {
2151                let mut spine_item = SpineItem::new("content_001");
2152                spine_item
2153                    .with_id("spine1")
2154                    .append_property("page-spread-left")
2155                    .set_linear(false);
2156
2157                let built = spine_item.build();
2158
2159                assert_eq!(built.idref, "content_001");
2160                assert_eq!(built.id, Some("spine1".to_string()));
2161                assert_eq!(built.properties, Some("page-spread-left".to_string()));
2162                assert_eq!(built.linear, false);
2163            }
2164
2165            #[test]
2166            fn test_spine_item_builder_chaining() {
2167                let mut spine_item = SpineItem::new("content_001");
2168                spine_item
2169                    .with_id("spine1")
2170                    .append_property("page-spread-left")
2171                    .set_linear(false);
2172
2173                let built = spine_item.build();
2174
2175                assert_eq!(built.idref, "content_001");
2176                assert_eq!(built.id, Some("spine1".to_string()));
2177                assert_eq!(built.properties, Some("page-spread-left".to_string()));
2178                assert_eq!(built.linear, false);
2179            }
2180
2181            #[test]
2182            fn test_spine_item_attributes() {
2183                let mut spine_item = SpineItem::new("content_001");
2184                spine_item
2185                    .with_id("spine1")
2186                    .append_property("page-spread-left")
2187                    .set_linear(false);
2188
2189                let attributes = spine_item.attributes();
2190
2191                // Check that all expected attributes are present
2192                assert!(attributes.contains(&("idref", "content_001")));
2193                assert!(attributes.contains(&("id", "spine1")));
2194                assert!(attributes.contains(&("properties", "page-spread-left")));
2195                assert!(attributes.contains(&("linear", "no"))); // false should become "no"
2196            }
2197
2198            #[test]
2199            fn test_spine_item_attributes_linear_yes() {
2200                let spine_item = SpineItem::new("content_001");
2201                let attributes = spine_item.attributes();
2202
2203                // Linear true should become "yes"
2204                assert!(attributes.contains(&("linear", "yes")));
2205            }
2206
2207            #[test]
2208            fn test_spine_item_attributes_optional_fields() {
2209                let spine_item = SpineItem::new("content_001");
2210                let attributes = spine_item.attributes();
2211
2212                // Should only contain required attributes when optional fields are None
2213                assert!(attributes.contains(&("idref", "content_001")));
2214                assert!(attributes.contains(&("linear", "yes")));
2215
2216                // Should not contain optional attributes when they are None
2217                assert!(!attributes.iter().any(|(k, _)| k == &"id"));
2218                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
2219            }
2220        }
2221
2222        mod metadata_sheet {
2223            use crate::types::{MetadataItem, MetadataSheet};
2224
2225            #[test]
2226            fn test_metadata_sheet_new() {
2227                let sheet = MetadataSheet::new();
2228
2229                assert!(sheet.contributor.is_empty());
2230                assert!(sheet.creator.is_empty());
2231                assert!(sheet.date.is_empty());
2232                assert!(sheet.identifier.is_empty());
2233                assert!(sheet.language.is_empty());
2234                assert!(sheet.relation.is_empty());
2235                assert!(sheet.subject.is_empty());
2236                assert!(sheet.title.is_empty());
2237
2238                assert!(sheet.coverage.is_empty());
2239                assert!(sheet.description.is_empty());
2240                assert!(sheet.format.is_empty());
2241                assert!(sheet.publisher.is_empty());
2242                assert!(sheet.rights.is_empty());
2243                assert!(sheet.source.is_empty());
2244                assert!(sheet.epub_type.is_empty());
2245            }
2246
2247            #[test]
2248            fn test_metadata_sheet_append_vec_fields() {
2249                let mut sheet = MetadataSheet::new();
2250
2251                sheet
2252                    .append_title("Test Book")
2253                    .append_creator("John Doe")
2254                    .append_creator("Jane Smith")
2255                    .append_contributor("Editor One")
2256                    .append_language("en")
2257                    .append_language("zh-CN")
2258                    .append_subject("Fiction")
2259                    .append_subject("Drama")
2260                    .append_relation("prequel");
2261
2262                assert_eq!(sheet.title.len(), 1);
2263                assert_eq!(sheet.title[0], "Test Book");
2264
2265                assert_eq!(sheet.creator.len(), 2);
2266                assert_eq!(sheet.creator[0], "John Doe");
2267                assert_eq!(sheet.creator[1], "Jane Smith");
2268
2269                assert_eq!(sheet.contributor.len(), 1);
2270                assert_eq!(sheet.contributor[0], "Editor One");
2271
2272                assert_eq!(sheet.language.len(), 2);
2273                assert_eq!(sheet.language[0], "en");
2274                assert_eq!(sheet.language[1], "zh-CN");
2275
2276                assert_eq!(sheet.subject.len(), 2);
2277                assert_eq!(sheet.subject[0], "Fiction");
2278                assert_eq!(sheet.subject[1], "Drama");
2279
2280                assert_eq!(sheet.relation.len(), 1);
2281                assert_eq!(sheet.relation[0], "prequel");
2282            }
2283
2284            #[test]
2285            fn test_metadata_sheet_append_date_and_identifier() {
2286                let mut sheet = MetadataSheet::new();
2287
2288                sheet
2289                    .append_date("2024-01-15", "publication")
2290                    .append_date("2024-01-10", "creation")
2291                    .append_identifier("book-id", "urn:isbn:1234567890")
2292                    .append_identifier("uuid-id", "urn:uuid:12345678-1234-1234-1234-123456789012");
2293
2294                assert_eq!(sheet.date.len(), 2);
2295                assert_eq!(
2296                    sheet.date.get("2024-01-15"),
2297                    Some(&"publication".to_string())
2298                );
2299                assert_eq!(sheet.date.get("2024-01-10"), Some(&"creation".to_string()));
2300
2301                assert_eq!(sheet.identifier.len(), 2);
2302                assert_eq!(
2303                    sheet.identifier.get("book-id"),
2304                    Some(&"urn:isbn:1234567890".to_string())
2305                );
2306                assert_eq!(
2307                    sheet.identifier.get("uuid-id"),
2308                    Some(&"urn:uuid:12345678-1234-1234-1234-123456789012".to_string())
2309                );
2310            }
2311
2312            #[test]
2313            fn test_metadata_sheet_with_string_fields() {
2314                let mut sheet = MetadataSheet::new();
2315
2316                sheet
2317                    .with_coverage("Spatial coverage")
2318                    .with_description("A test book description")
2319                    .with_format("application/epub+zip")
2320                    .with_publisher("Test Publisher")
2321                    .with_rights("Copyright 2024")
2322                    .with_source("Original source")
2323                    .with_epub_type("buku");
2324
2325                assert_eq!(sheet.coverage, "Spatial coverage");
2326                assert_eq!(sheet.description, "A test book description");
2327                assert_eq!(sheet.format, "application/epub+zip");
2328                assert_eq!(sheet.publisher, "Test Publisher");
2329                assert_eq!(sheet.rights, "Copyright 2024");
2330                assert_eq!(sheet.source, "Original source");
2331                assert_eq!(sheet.epub_type, "buku");
2332            }
2333
2334            #[test]
2335            fn test_metadata_sheet_builder_chaining() {
2336                let mut sheet = MetadataSheet::new();
2337
2338                sheet
2339                    .append_title("Chained Book")
2340                    .append_creator("Chained Author")
2341                    .append_date("2024-01-01", "")
2342                    .append_identifier("id-1", "test-id")
2343                    .with_publisher("Chained Publisher")
2344                    .with_description("Chained description");
2345
2346                assert_eq!(sheet.title.len(), 1);
2347                assert_eq!(sheet.title[0], "Chained Book");
2348
2349                assert_eq!(sheet.creator.len(), 1);
2350                assert_eq!(sheet.creator[0], "Chained Author");
2351
2352                assert_eq!(sheet.date.len(), 1);
2353                assert_eq!(sheet.identifier.len(), 1);
2354                assert_eq!(sheet.publisher, "Chained Publisher");
2355                assert_eq!(sheet.description, "Chained description");
2356            }
2357
2358            #[test]
2359            fn test_metadata_sheet_build() {
2360                let mut sheet = MetadataSheet::new();
2361                sheet
2362                    .append_title("Original Title")
2363                    .with_publisher("Original Publisher");
2364
2365                let built = sheet.build();
2366
2367                assert_eq!(built.title.len(), 1);
2368                assert_eq!(built.title[0], "Original Title");
2369                assert_eq!(built.publisher, "Original Publisher");
2370
2371                sheet.append_title("New Title");
2372                sheet.with_publisher("New Publisher");
2373
2374                assert_eq!(sheet.title.len(), 2);
2375                assert_eq!(built.title.len(), 1);
2376                assert_eq!(built.publisher, "Original Publisher");
2377            }
2378
2379            #[test]
2380            fn test_metadata_sheet_into_metadata_items() {
2381                let mut sheet = MetadataSheet::new();
2382                sheet
2383                    .append_title("Test Title")
2384                    .append_creator("Test Creator")
2385                    .with_description("Test Description")
2386                    .with_publisher("Test Publisher");
2387
2388                let items: Vec<MetadataItem> = sheet.into();
2389
2390                assert_eq!(items.len(), 4);
2391
2392                assert!(
2393                    items
2394                        .iter()
2395                        .any(|i| i.property == "title" && i.value == "Test Title")
2396                );
2397
2398                assert!(
2399                    items
2400                        .iter()
2401                        .any(|i| i.property == "creator" && i.value == "Test Creator")
2402                );
2403
2404                assert!(
2405                    items
2406                        .iter()
2407                        .any(|i| i.property == "description" && i.value == "Test Description")
2408                );
2409
2410                assert!(
2411                    items
2412                        .iter()
2413                        .any(|i| i.property == "publisher" && i.value == "Test Publisher")
2414                );
2415            }
2416
2417            #[test]
2418            fn test_metadata_sheet_into_metadata_items_with_date_and_identifier() {
2419                let mut sheet = MetadataSheet::new();
2420                sheet
2421                    .append_date("2024-01-15", "publication")
2422                    .append_identifier("book-id", "urn:isbn:9876543210");
2423
2424                let items: Vec<MetadataItem> = sheet.into();
2425
2426                assert_eq!(items.len(), 2);
2427
2428                let date_item = items.iter().find(|i| i.property == "date").unwrap();
2429
2430                assert_eq!(date_item.value, "2024-01-15");
2431                assert!(date_item.id.is_some());
2432                assert_eq!(date_item.refined.len(), 1);
2433                assert_eq!(date_item.refined[0].property, "event");
2434                assert_eq!(date_item.refined[0].value, "publication");
2435
2436                let id_item = items.iter().find(|i| i.property == "identifier").unwrap();
2437
2438                assert_eq!(id_item.value, "urn:isbn:9876543210");
2439                assert_eq!(id_item.id, Some("book-id".to_string()));
2440            }
2441
2442            #[test]
2443            fn test_metadata_sheet_into_metadata_items_ignores_empty_fields() {
2444                let mut sheet = MetadataSheet::new();
2445                sheet.append_title("Valid Title").with_description(""); // Empty string should be ignored
2446
2447                let items: Vec<MetadataItem> = sheet.into();
2448
2449                assert_eq!(items.len(), 1);
2450                assert_eq!(items[0].property, "title");
2451            }
2452        }
2453
2454        mod navpoint {
2455
2456            use std::path::PathBuf;
2457
2458            use crate::types::NavPoint;
2459
2460            #[test]
2461            fn test_navpoint_new() {
2462                let navpoint = NavPoint::new("Test Chapter");
2463
2464                assert_eq!(navpoint.label, "Test Chapter");
2465                assert_eq!(navpoint.content, None);
2466                assert_eq!(navpoint.children.len(), 0);
2467            }
2468
2469            #[test]
2470            fn test_navpoint_with_content() {
2471                let mut navpoint = NavPoint::new("Test Chapter");
2472                navpoint.with_content("chapter1.html");
2473
2474                assert_eq!(navpoint.label, "Test Chapter");
2475                assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
2476                assert_eq!(navpoint.children.len(), 0);
2477            }
2478
2479            #[test]
2480            fn test_navpoint_append_child() {
2481                let mut parent = NavPoint::new("Parent Chapter");
2482
2483                let mut child1 = NavPoint::new("Child Section 1");
2484                child1.with_content("section1.html");
2485
2486                let mut child2 = NavPoint::new("Child Section 2");
2487                child2.with_content("section2.html");
2488
2489                parent.append_child(child1.build());
2490                parent.append_child(child2.build());
2491
2492                assert_eq!(parent.children.len(), 2);
2493                assert_eq!(parent.children[0].label, "Child Section 1");
2494                assert_eq!(parent.children[1].label, "Child Section 2");
2495            }
2496
2497            #[test]
2498            fn test_navpoint_set_children() {
2499                let mut navpoint = NavPoint::new("Main Chapter");
2500                let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
2501
2502                navpoint.set_children(children);
2503
2504                assert_eq!(navpoint.children.len(), 2);
2505                assert_eq!(navpoint.children[0].label, "Section 1");
2506                assert_eq!(navpoint.children[1].label, "Section 2");
2507            }
2508
2509            #[test]
2510            fn test_navpoint_build() {
2511                let mut navpoint = NavPoint::new("Complete Chapter");
2512                navpoint.with_content("complete.html");
2513
2514                let child = NavPoint::new("Sub Section");
2515                navpoint.append_child(child.build());
2516
2517                let built = navpoint.build();
2518
2519                assert_eq!(built.label, "Complete Chapter");
2520                assert_eq!(built.content, Some(PathBuf::from("complete.html")));
2521                assert_eq!(built.children.len(), 1);
2522                assert_eq!(built.children[0].label, "Sub Section");
2523            }
2524
2525            #[test]
2526            fn test_navpoint_builder_chaining() {
2527                let mut navpoint = NavPoint::new("Chained Chapter");
2528
2529                navpoint
2530                    .with_content("chained.html")
2531                    .append_child(NavPoint::new("Child 1").build())
2532                    .append_child(NavPoint::new("Child 2").build());
2533
2534                let built = navpoint.build();
2535
2536                assert_eq!(built.label, "Chained Chapter");
2537                assert_eq!(built.content, Some(PathBuf::from("chained.html")));
2538                assert_eq!(built.children.len(), 2);
2539            }
2540
2541            #[test]
2542            fn test_navpoint_empty_children() {
2543                let navpoint = NavPoint::new("No Children Chapter");
2544                let built = navpoint.build();
2545
2546                assert_eq!(built.children.len(), 0);
2547            }
2548
2549            #[test]
2550            fn test_navpoint_complex_hierarchy() {
2551                let mut root = NavPoint::new("Book");
2552
2553                let mut chapter1 = NavPoint::new("Chapter 1");
2554                chapter1
2555                    .with_content("chapter1.html")
2556                    .append_child(
2557                        NavPoint::new("Section 1.1")
2558                            .with_content("sec1_1.html")
2559                            .build(),
2560                    )
2561                    .append_child(
2562                        NavPoint::new("Section 1.2")
2563                            .with_content("sec1_2.html")
2564                            .build(),
2565                    );
2566
2567                let mut chapter2 = NavPoint::new("Chapter 2");
2568                chapter2.with_content("chapter2.html").append_child(
2569                    NavPoint::new("Section 2.1")
2570                        .with_content("sec2_1.html")
2571                        .build(),
2572                );
2573
2574                root.append_child(chapter1.build())
2575                    .append_child(chapter2.build());
2576
2577                let book = root.build();
2578
2579                assert_eq!(book.label, "Book");
2580                assert_eq!(book.children.len(), 2);
2581
2582                let ch1 = &book.children[0];
2583                assert_eq!(ch1.label, "Chapter 1");
2584                assert_eq!(ch1.children.len(), 2);
2585
2586                let ch2 = &book.children[1];
2587                assert_eq!(ch2.label, "Chapter 2");
2588                assert_eq!(ch2.children.len(), 1);
2589            }
2590        }
2591    }
2592
2593    #[cfg(feature = "content-builder")]
2594    mod footnote_tests {
2595        use crate::types::Footnote;
2596
2597        #[test]
2598        fn test_footnote_basic_creation() {
2599            let footnote = Footnote {
2600                locate: 100,
2601                content: "Sample footnote".to_string(),
2602            };
2603
2604            assert_eq!(footnote.locate, 100);
2605            assert_eq!(footnote.content, "Sample footnote");
2606        }
2607
2608        #[test]
2609        fn test_footnote_equality() {
2610            let footnote1 = Footnote {
2611                locate: 100,
2612                content: "First note".to_string(),
2613            };
2614
2615            let footnote2 = Footnote {
2616                locate: 100,
2617                content: "First note".to_string(),
2618            };
2619
2620            let footnote3 = Footnote {
2621                locate: 100,
2622                content: "Different note".to_string(),
2623            };
2624
2625            let footnote4 = Footnote {
2626                locate: 200,
2627                content: "First note".to_string(),
2628            };
2629
2630            assert_eq!(footnote1, footnote2);
2631            assert_ne!(footnote1, footnote3);
2632            assert_ne!(footnote1, footnote4);
2633        }
2634
2635        #[test]
2636        fn test_footnote_ordering() {
2637            let footnote1 = Footnote {
2638                locate: 100,
2639                content: "First".to_string(),
2640            };
2641
2642            let footnote2 = Footnote {
2643                locate: 200,
2644                content: "Second".to_string(),
2645            };
2646
2647            let footnote3 = Footnote {
2648                locate: 150,
2649                content: "Middle".to_string(),
2650            };
2651
2652            assert!(footnote1 < footnote2);
2653            assert!(footnote2 > footnote1);
2654            assert!(footnote1 < footnote3);
2655            assert!(footnote3 < footnote2);
2656            assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
2657        }
2658
2659        #[test]
2660        fn test_footnote_sorting() {
2661            let mut footnotes = vec![
2662                Footnote {
2663                    locate: 300,
2664                    content: "Third note".to_string(),
2665                },
2666                Footnote {
2667                    locate: 100,
2668                    content: "First note".to_string(),
2669                },
2670                Footnote {
2671                    locate: 200,
2672                    content: "Second note".to_string(),
2673                },
2674            ];
2675
2676            footnotes.sort();
2677
2678            assert_eq!(footnotes[0].locate, 100);
2679            assert_eq!(footnotes[1].locate, 200);
2680            assert_eq!(footnotes[2].locate, 300);
2681
2682            assert_eq!(footnotes[0].content, "First note");
2683            assert_eq!(footnotes[1].content, "Second note");
2684            assert_eq!(footnotes[2].content, "Third note");
2685        }
2686    }
2687
2688    #[cfg(feature = "content-builder")]
2689    mod block_type_tests {
2690        use crate::types::BlockType;
2691
2692        #[test]
2693        fn test_block_type_variants() {
2694            let _ = BlockType::Text;
2695            let _ = BlockType::Quote;
2696            let _ = BlockType::Title;
2697            let _ = BlockType::Image;
2698            let _ = BlockType::Audio;
2699            let _ = BlockType::Video;
2700            let _ = BlockType::MathML;
2701        }
2702
2703        #[test]
2704        fn test_block_type_debug() {
2705            let text = format!("{:?}", BlockType::Text);
2706            assert_eq!(text, "Text");
2707
2708            let quote = format!("{:?}", BlockType::Quote);
2709            assert_eq!(quote, "Quote");
2710
2711            let image = format!("{:?}", BlockType::Image);
2712            assert_eq!(image, "Image");
2713        }
2714    }
2715
2716    #[cfg(feature = "content-builder")]
2717    mod style_options_tests {
2718        use crate::types::{ColorScheme, PageLayout, StyleOptions, TextAlign, TextStyle};
2719
2720        #[test]
2721        fn test_style_options_default() {
2722            let options = StyleOptions::default();
2723
2724            assert_eq!(options.text.font_size, 1.0);
2725            assert_eq!(options.text.line_height, 1.6);
2726            assert_eq!(
2727                options.text.font_family,
2728                "-apple-system, Roboto, sans-serif"
2729            );
2730            assert_eq!(options.text.font_weight, "normal");
2731            assert_eq!(options.text.font_style, "normal");
2732            assert_eq!(options.text.letter_spacing, "normal");
2733            assert_eq!(options.text.text_indent, 2.0);
2734
2735            assert_eq!(options.color_scheme.background, "#FFFFFF");
2736            assert_eq!(options.color_scheme.text, "#000000");
2737            assert_eq!(options.color_scheme.link, "#6f6f6f");
2738
2739            assert_eq!(options.layout.margin, 20);
2740            assert_eq!(options.layout.text_align, TextAlign::Left);
2741            assert_eq!(options.layout.paragraph_spacing, 16);
2742        }
2743
2744        #[test]
2745        fn test_style_options_custom_values() {
2746            let text = TextStyle {
2747                font_size: 1.5,
2748                line_height: 2.0,
2749                font_family: "Georgia, serif".to_string(),
2750                font_weight: "bold".to_string(),
2751                font_style: "italic".to_string(),
2752                letter_spacing: "0.1em".to_string(),
2753                text_indent: 3.0,
2754            };
2755
2756            let color_scheme = ColorScheme {
2757                background: "#F0F0F0".to_string(),
2758                text: "#333333".to_string(),
2759                link: "#0066CC".to_string(),
2760            };
2761
2762            let layout = PageLayout {
2763                margin: 30,
2764                text_align: TextAlign::Center,
2765                paragraph_spacing: 20,
2766            };
2767
2768            let options = StyleOptions { text, color_scheme, layout };
2769
2770            assert_eq!(options.text.font_size, 1.5);
2771            assert_eq!(options.text.font_weight, "bold");
2772            assert_eq!(options.color_scheme.background, "#F0F0F0");
2773            assert_eq!(options.layout.text_align, TextAlign::Center);
2774        }
2775
2776        #[test]
2777        fn test_text_style_default() {
2778            let style = TextStyle::default();
2779
2780            assert_eq!(style.font_size, 1.0);
2781            assert_eq!(style.line_height, 1.6);
2782            assert_eq!(style.font_family, "-apple-system, Roboto, sans-serif");
2783            assert_eq!(style.font_weight, "normal");
2784            assert_eq!(style.font_style, "normal");
2785            assert_eq!(style.letter_spacing, "normal");
2786            assert_eq!(style.text_indent, 2.0);
2787        }
2788
2789        #[test]
2790        fn test_text_style_custom_values() {
2791            let style = TextStyle {
2792                font_size: 2.0,
2793                line_height: 1.8,
2794                font_family: "Times New Roman".to_string(),
2795                font_weight: "bold".to_string(),
2796                font_style: "italic".to_string(),
2797                letter_spacing: "0.05em".to_string(),
2798                text_indent: 0.0,
2799            };
2800
2801            assert_eq!(style.font_size, 2.0);
2802            assert_eq!(style.line_height, 1.8);
2803            assert_eq!(style.font_family, "Times New Roman");
2804            assert_eq!(style.font_weight, "bold");
2805            assert_eq!(style.font_style, "italic");
2806            assert_eq!(style.letter_spacing, "0.05em");
2807            assert_eq!(style.text_indent, 0.0);
2808        }
2809
2810        #[test]
2811        fn test_text_style_debug() {
2812            let style = TextStyle::default();
2813            let debug_str = format!("{:?}", style);
2814            assert!(debug_str.contains("TextStyle"));
2815            assert!(debug_str.contains("font_size"));
2816        }
2817
2818        #[test]
2819        fn test_color_scheme_default() {
2820            let scheme = ColorScheme::default();
2821
2822            assert_eq!(scheme.background, "#FFFFFF");
2823            assert_eq!(scheme.text, "#000000");
2824            assert_eq!(scheme.link, "#6f6f6f");
2825        }
2826
2827        #[test]
2828        fn test_color_scheme_custom_values() {
2829            let scheme = ColorScheme {
2830                background: "#000000".to_string(),
2831                text: "#FFFFFF".to_string(),
2832                link: "#00FF00".to_string(),
2833            };
2834
2835            assert_eq!(scheme.background, "#000000");
2836            assert_eq!(scheme.text, "#FFFFFF");
2837            assert_eq!(scheme.link, "#00FF00");
2838        }
2839
2840        #[test]
2841        fn test_color_scheme_debug() {
2842            let scheme = ColorScheme::default();
2843            let debug_str = format!("{:?}", scheme);
2844            assert!(debug_str.contains("ColorScheme"));
2845            assert!(debug_str.contains("background"));
2846        }
2847
2848        #[test]
2849        fn test_page_layout_default() {
2850            let layout = PageLayout::default();
2851
2852            assert_eq!(layout.margin, 20);
2853            assert_eq!(layout.text_align, TextAlign::Left);
2854            assert_eq!(layout.paragraph_spacing, 16);
2855        }
2856
2857        #[test]
2858        fn test_page_layout_custom_values() {
2859            let layout = PageLayout {
2860                margin: 40,
2861                text_align: TextAlign::Justify,
2862                paragraph_spacing: 24,
2863            };
2864
2865            assert_eq!(layout.margin, 40);
2866            assert_eq!(layout.text_align, TextAlign::Justify);
2867            assert_eq!(layout.paragraph_spacing, 24);
2868        }
2869
2870        #[test]
2871        fn test_page_layout_debug() {
2872            let layout = PageLayout::default();
2873            let debug_str = format!("{:?}", layout);
2874            assert!(debug_str.contains("PageLayout"));
2875            assert!(debug_str.contains("margin"));
2876        }
2877
2878        #[test]
2879        fn test_text_align_default() {
2880            let align = TextAlign::default();
2881            assert_eq!(align, TextAlign::Left);
2882        }
2883
2884        #[test]
2885        fn test_text_align_display() {
2886            assert_eq!(TextAlign::Left.to_string(), "left");
2887            assert_eq!(TextAlign::Right.to_string(), "right");
2888            assert_eq!(TextAlign::Justify.to_string(), "justify");
2889            assert_eq!(TextAlign::Center.to_string(), "center");
2890        }
2891
2892        #[test]
2893        fn test_text_align_all_variants() {
2894            let left = TextAlign::Left;
2895            let right = TextAlign::Right;
2896            let justify = TextAlign::Justify;
2897            let center = TextAlign::Center;
2898
2899            assert!(matches!(left, TextAlign::Left));
2900            assert!(matches!(right, TextAlign::Right));
2901            assert!(matches!(justify, TextAlign::Justify));
2902            assert!(matches!(center, TextAlign::Center));
2903        }
2904
2905        #[test]
2906        fn test_text_align_debug() {
2907            assert_eq!(format!("{:?}", TextAlign::Left), "Left");
2908            assert_eq!(format!("{:?}", TextAlign::Right), "Right");
2909            assert_eq!(format!("{:?}", TextAlign::Justify), "Justify");
2910            assert_eq!(format!("{:?}", TextAlign::Center), "Center");
2911        }
2912
2913        #[test]
2914        fn test_style_options_builder_new() {
2915            let options = StyleOptions::new();
2916            assert_eq!(options.text.font_size, 1.0);
2917            assert_eq!(options.color_scheme.background, "#FFFFFF");
2918            assert_eq!(options.layout.margin, 20);
2919        }
2920
2921        #[test]
2922        fn test_style_options_builder_with_text() {
2923            let mut options = StyleOptions::new();
2924            let text_style = TextStyle::new()
2925                .with_font_size(2.0)
2926                .with_font_weight("bold")
2927                .build();
2928            options.with_text(text_style);
2929
2930            assert_eq!(options.text.font_size, 2.0);
2931            assert_eq!(options.text.font_weight, "bold");
2932        }
2933
2934        #[test]
2935        fn test_style_options_builder_with_color_scheme() {
2936            let mut options = StyleOptions::new();
2937            let color = ColorScheme::new()
2938                .with_background("#000000")
2939                .with_text("#FFFFFF")
2940                .build();
2941            options.with_color_scheme(color);
2942
2943            assert_eq!(options.color_scheme.background, "#000000");
2944            assert_eq!(options.color_scheme.text, "#FFFFFF");
2945        }
2946
2947        #[test]
2948        fn test_style_options_builder_with_layout() {
2949            let mut options = StyleOptions::new();
2950            let layout = PageLayout::new()
2951                .with_margin(40)
2952                .with_text_align(TextAlign::Justify)
2953                .with_paragraph_spacing(24)
2954                .build();
2955            options.with_layout(layout);
2956
2957            assert_eq!(options.layout.margin, 40);
2958            assert_eq!(options.layout.text_align, TextAlign::Justify);
2959            assert_eq!(options.layout.paragraph_spacing, 24);
2960        }
2961
2962        #[test]
2963        fn test_style_options_builder_build() {
2964            let options = StyleOptions::new()
2965                .with_text(TextStyle::new().with_font_size(1.5).build())
2966                .with_color_scheme(ColorScheme::new().with_link("#FF0000").build())
2967                .with_layout(PageLayout::new().with_margin(30).build())
2968                .build();
2969
2970            assert_eq!(options.text.font_size, 1.5);
2971            assert_eq!(options.color_scheme.link, "#FF0000");
2972            assert_eq!(options.layout.margin, 30);
2973        }
2974
2975        #[test]
2976        fn test_style_options_builder_chaining() {
2977            let options = StyleOptions::new()
2978                .with_text(
2979                    TextStyle::new()
2980                        .with_font_size(1.5)
2981                        .with_line_height(2.0)
2982                        .with_font_family("Arial")
2983                        .with_font_weight("bold")
2984                        .with_font_style("italic")
2985                        .with_letter_spacing("0.1em")
2986                        .with_text_indent(1.5)
2987                        .build(),
2988                )
2989                .with_color_scheme(
2990                    ColorScheme::new()
2991                        .with_background("#CCCCCC")
2992                        .with_text("#111111")
2993                        .with_link("#0000FF")
2994                        .build(),
2995                )
2996                .with_layout(
2997                    PageLayout::new()
2998                        .with_margin(25)
2999                        .with_text_align(TextAlign::Right)
3000                        .with_paragraph_spacing(20)
3001                        .build(),
3002                )
3003                .build();
3004
3005            assert_eq!(options.text.font_size, 1.5);
3006            assert_eq!(options.text.line_height, 2.0);
3007            assert_eq!(options.text.font_family, "Arial");
3008            assert_eq!(options.text.font_weight, "bold");
3009            assert_eq!(options.text.font_style, "italic");
3010            assert_eq!(options.text.letter_spacing, "0.1em");
3011            assert_eq!(options.text.text_indent, 1.5);
3012
3013            assert_eq!(options.color_scheme.background, "#CCCCCC");
3014            assert_eq!(options.color_scheme.text, "#111111");
3015            assert_eq!(options.color_scheme.link, "#0000FF");
3016
3017            assert_eq!(options.layout.margin, 25);
3018            assert_eq!(options.layout.text_align, TextAlign::Right);
3019            assert_eq!(options.layout.paragraph_spacing, 20);
3020        }
3021
3022        #[test]
3023        fn test_text_style_builder_new() {
3024            let style = TextStyle::new();
3025            assert_eq!(style.font_size, 1.0);
3026            assert_eq!(style.line_height, 1.6);
3027        }
3028
3029        #[test]
3030        fn test_text_style_builder_with_font_size() {
3031            let mut style = TextStyle::new();
3032            style.with_font_size(2.5);
3033            assert_eq!(style.font_size, 2.5);
3034        }
3035
3036        #[test]
3037        fn test_text_style_builder_with_line_height() {
3038            let mut style = TextStyle::new();
3039            style.with_line_height(2.0);
3040            assert_eq!(style.line_height, 2.0);
3041        }
3042
3043        #[test]
3044        fn test_text_style_builder_with_font_family() {
3045            let mut style = TextStyle::new();
3046            style.with_font_family("Helvetica, Arial");
3047            assert_eq!(style.font_family, "Helvetica, Arial");
3048        }
3049
3050        #[test]
3051        fn test_text_style_builder_with_font_weight() {
3052            let mut style = TextStyle::new();
3053            style.with_font_weight("bold");
3054            assert_eq!(style.font_weight, "bold");
3055        }
3056
3057        #[test]
3058        fn test_text_style_builder_with_font_style() {
3059            let mut style = TextStyle::new();
3060            style.with_font_style("italic");
3061            assert_eq!(style.font_style, "italic");
3062        }
3063
3064        #[test]
3065        fn test_text_style_builder_with_letter_spacing() {
3066            let mut style = TextStyle::new();
3067            style.with_letter_spacing("0.05em");
3068            assert_eq!(style.letter_spacing, "0.05em");
3069        }
3070
3071        #[test]
3072        fn test_text_style_builder_with_text_indent() {
3073            let mut style = TextStyle::new();
3074            style.with_text_indent(3.0);
3075            assert_eq!(style.text_indent, 3.0);
3076        }
3077
3078        #[test]
3079        fn test_text_style_builder_build() {
3080            let style = TextStyle::new()
3081                .with_font_size(1.8)
3082                .with_line_height(1.9)
3083                .build();
3084
3085            assert_eq!(style.font_size, 1.8);
3086            assert_eq!(style.line_height, 1.9);
3087        }
3088
3089        #[test]
3090        fn test_text_style_builder_chaining() {
3091            let style = TextStyle::new()
3092                .with_font_size(2.0)
3093                .with_line_height(1.8)
3094                .with_font_family("Georgia")
3095                .with_font_weight("bold")
3096                .with_font_style("italic")
3097                .with_letter_spacing("0.1em")
3098                .with_text_indent(0.5)
3099                .build();
3100
3101            assert_eq!(style.font_size, 2.0);
3102            assert_eq!(style.line_height, 1.8);
3103            assert_eq!(style.font_family, "Georgia");
3104            assert_eq!(style.font_weight, "bold");
3105            assert_eq!(style.font_style, "italic");
3106            assert_eq!(style.letter_spacing, "0.1em");
3107            assert_eq!(style.text_indent, 0.5);
3108        }
3109
3110        #[test]
3111        fn test_color_scheme_builder_new() {
3112            let scheme = ColorScheme::new();
3113            assert_eq!(scheme.background, "#FFFFFF");
3114            assert_eq!(scheme.text, "#000000");
3115        }
3116
3117        #[test]
3118        fn test_color_scheme_builder_with_background() {
3119            let mut scheme = ColorScheme::new();
3120            scheme.with_background("#FF0000");
3121            assert_eq!(scheme.background, "#FF0000");
3122        }
3123
3124        #[test]
3125        fn test_color_scheme_builder_with_text() {
3126            let mut scheme = ColorScheme::new();
3127            scheme.with_text("#333333");
3128            assert_eq!(scheme.text, "#333333");
3129        }
3130
3131        #[test]
3132        fn test_color_scheme_builder_with_link() {
3133            let mut scheme = ColorScheme::new();
3134            scheme.with_link("#0000FF");
3135            assert_eq!(scheme.link, "#0000FF");
3136        }
3137
3138        #[test]
3139        fn test_color_scheme_builder_build() {
3140            let scheme = ColorScheme::new().with_background("#123456").build();
3141
3142            assert_eq!(scheme.background, "#123456");
3143            assert_eq!(scheme.text, "#000000");
3144        }
3145
3146        #[test]
3147        fn test_color_scheme_builder_chaining() {
3148            let scheme = ColorScheme::new()
3149                .with_background("#AABBCC")
3150                .with_text("#DDEEFF")
3151                .with_link("#112233")
3152                .build();
3153
3154            assert_eq!(scheme.background, "#AABBCC");
3155            assert_eq!(scheme.text, "#DDEEFF");
3156            assert_eq!(scheme.link, "#112233");
3157        }
3158
3159        #[test]
3160        fn test_page_layout_builder_new() {
3161            let layout = PageLayout::new();
3162            assert_eq!(layout.margin, 20);
3163            assert_eq!(layout.text_align, TextAlign::Left);
3164            assert_eq!(layout.paragraph_spacing, 16);
3165        }
3166
3167        #[test]
3168        fn test_page_layout_builder_with_margin() {
3169            let mut layout = PageLayout::new();
3170            layout.with_margin(50);
3171            assert_eq!(layout.margin, 50);
3172        }
3173
3174        #[test]
3175        fn test_page_layout_builder_with_text_align() {
3176            let mut layout = PageLayout::new();
3177            layout.with_text_align(TextAlign::Center);
3178            assert_eq!(layout.text_align, TextAlign::Center);
3179        }
3180
3181        #[test]
3182        fn test_page_layout_builder_with_paragraph_spacing() {
3183            let mut layout = PageLayout::new();
3184            layout.with_paragraph_spacing(30);
3185            assert_eq!(layout.paragraph_spacing, 30);
3186        }
3187
3188        #[test]
3189        fn test_page_layout_builder_build() {
3190            let layout = PageLayout::new().with_margin(35).build();
3191
3192            assert_eq!(layout.margin, 35);
3193            assert_eq!(layout.text_align, TextAlign::Left);
3194        }
3195
3196        #[test]
3197        fn test_page_layout_builder_chaining() {
3198            let layout = PageLayout::new()
3199                .with_margin(45)
3200                .with_text_align(TextAlign::Justify)
3201                .with_paragraph_spacing(28)
3202                .build();
3203
3204            assert_eq!(layout.margin, 45);
3205            assert_eq!(layout.text_align, TextAlign::Justify);
3206            assert_eq!(layout.paragraph_spacing, 28);
3207        }
3208
3209        #[test]
3210        fn test_page_layout_builder_all_text_align_variants() {
3211            let left = PageLayout::new().with_text_align(TextAlign::Left).build();
3212            assert_eq!(left.text_align, TextAlign::Left);
3213
3214            let right = PageLayout::new().with_text_align(TextAlign::Right).build();
3215            assert_eq!(right.text_align, TextAlign::Right);
3216
3217            let center = PageLayout::new().with_text_align(TextAlign::Center).build();
3218            assert_eq!(center.text_align, TextAlign::Center);
3219
3220            let justify = PageLayout::new()
3221                .with_text_align(TextAlign::Justify)
3222                .build();
3223            assert_eq!(justify.text_align, TextAlign::Justify);
3224        }
3225    }
3226}