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")]
1163impl StyleOptions {
1164    /// Creates a new style options with default values
1165    pub fn new() -> Self {
1166        Self::default()
1167    }
1168
1169    /// Sets the text style configuration
1170    pub fn with_text(&mut self, text: TextStyle) -> &mut Self {
1171        self.text = text;
1172        self
1173    }
1174
1175    /// Sets the color scheme configuration
1176    pub fn with_color_scheme(&mut self, color_scheme: ColorScheme) -> &mut Self {
1177        self.color_scheme = color_scheme;
1178        self
1179    }
1180
1181    /// Sets the page layout configuration
1182    pub fn with_layout(&mut self, layout: PageLayout) -> &mut Self {
1183        self.layout = layout;
1184        self
1185    }
1186
1187    /// Builds the final style options
1188    pub fn build(&self) -> Self {
1189        Self { ..self.clone() }
1190    }
1191}
1192
1193/// Text styling configuration
1194///
1195/// Defines the visual appearance of text content in the document,
1196/// including font properties, sizing, and spacing.
1197#[cfg(feature = "content-builder")]
1198#[derive(Debug, Clone)]
1199pub struct TextStyle {
1200    /// The base font size (default: 1.0, unit: rem)
1201    ///
1202    /// Relative to the root element, providing consistent sizing
1203    /// across different viewing contexts.
1204    pub font_size: f32,
1205
1206    /// The line height (default: 1.6, unit: em)
1207    ///
1208    /// Controls the vertical spacing between lines of text.
1209    /// Values greater than 1.0 increase spacing, while values
1210    /// less than 1.0 compress the text.
1211    pub line_height: f32,
1212
1213    /// The font family stack (default: "-apple-system, Roboto, sans-serif")
1214    ///
1215    /// A comma-separated list of font families to use, with
1216    /// fallback fonts specified for compatibility.
1217    pub font_family: String,
1218
1219    /// The font weight (default: "normal")
1220    ///
1221    /// Controls the thickness of the font strokes. Common values
1222    /// include "normal" and "bold".
1223    pub font_weight: String,
1224
1225    /// The font style (default: "normal")
1226    ///
1227    /// Controls whether the font is normal, italic, or oblique.
1228    /// Common values include "normal" and "italic".
1229    pub font_style: String,
1230
1231    /// The letter spacing (default: "normal")
1232    ///
1233    /// Controls the space between characters. Common values
1234    /// include "normal" or specific lengths like "0.05em".
1235    pub letter_spacing: String,
1236
1237    /// The text indent for paragraphs (default: 2.0, unit: em)
1238    ///
1239    /// Controls the indentation of the first line of paragraphs.
1240    /// A value of 2.0 means the first line is indented by 2 ems.
1241    pub text_indent: f32,
1242}
1243
1244#[cfg(feature = "content-builder")]
1245impl Default for TextStyle {
1246    fn default() -> Self {
1247        Self {
1248            font_size: 1.0,
1249            line_height: 1.6,
1250            font_family: "-apple-system, Roboto, sans-serif".to_string(),
1251            font_weight: "normal".to_string(),
1252            font_style: "normal".to_string(),
1253            letter_spacing: "normal".to_string(),
1254            text_indent: 2.0,
1255        }
1256    }
1257}
1258
1259#[cfg(feature = "content-builder")]
1260impl TextStyle {
1261    /// Creates a new text style with default values
1262    pub fn new() -> Self {
1263        Self::default()
1264    }
1265
1266    /// Sets the font size
1267    pub fn with_font_size(&mut self, font_size: f32) -> &mut Self {
1268        self.font_size = font_size;
1269        self
1270    }
1271
1272    /// Sets the line height
1273    pub fn with_line_height(&mut self, line_height: f32) -> &mut Self {
1274        self.line_height = line_height;
1275        self
1276    }
1277
1278    /// Sets the font family
1279    pub fn with_font_family(&mut self, font_family: &str) -> &mut Self {
1280        self.font_family = font_family.to_string();
1281        self
1282    }
1283
1284    /// Sets the font weight
1285    pub fn with_font_weight(&mut self, font_weight: &str) -> &mut Self {
1286        self.font_weight = font_weight.to_string();
1287        self
1288    }
1289
1290    /// Sets the font style
1291    pub fn with_font_style(&mut self, font_style: &str) -> &mut Self {
1292        self.font_style = font_style.to_string();
1293        self
1294    }
1295
1296    /// Sets the letter spacing
1297    pub fn with_letter_spacing(&mut self, letter_spacing: &str) -> &mut Self {
1298        self.letter_spacing = letter_spacing.to_string();
1299        self
1300    }
1301
1302    /// Sets the text indent
1303    pub fn with_text_indent(&mut self, text_indent: f32) -> &mut Self {
1304        self.text_indent = text_indent;
1305        self
1306    }
1307
1308    /// Builds the final text style
1309    pub fn build(&self) -> Self {
1310        Self { ..self.clone() }
1311    }
1312}
1313
1314/// Color scheme configuration
1315///
1316/// Defines the color palette for the document, including background,
1317/// text, and link colors.
1318#[cfg(feature = "content-builder")]
1319#[derive(Debug, Clone)]
1320pub struct ColorScheme {
1321    /// The background color (default: "#FFFFFF")
1322    ///
1323    /// The fill color for the document body. Specified as a hex color
1324    /// string (e.g., "#FFFFFF" for white).
1325    pub background: String,
1326
1327    /// The text color (default: "#000000")
1328    ///
1329    /// The primary color for text content. Specified as a hex color
1330    /// string (e.g., "#000000" for black).
1331    pub text: String,
1332
1333    /// The link color (default: "#6f6f6f")
1334    ///
1335    /// The color for hyperlinks in the document. Specified as a hex
1336    /// color string (e.g., "#6f6f6f" for gray).
1337    pub link: String,
1338}
1339
1340#[cfg(feature = "content-builder")]
1341impl Default for ColorScheme {
1342    fn default() -> Self {
1343        Self {
1344            background: "#FFFFFF".to_string(),
1345            text: "#000000".to_string(),
1346            link: "#6f6f6f".to_string(),
1347        }
1348    }
1349}
1350
1351#[cfg(feature = "content-builder")]
1352impl ColorScheme {
1353    /// Creates a new color scheme with default values
1354    pub fn new() -> Self {
1355        Self::default()
1356    }
1357
1358    /// Sets the background color
1359    pub fn with_background(&mut self, background: &str) -> &mut Self {
1360        self.background = background.to_string();
1361        self
1362    }
1363
1364    /// Sets the text color
1365    pub fn with_text(&mut self, text: &str) -> &mut Self {
1366        self.text = text.to_string();
1367        self
1368    }
1369
1370    /// Sets the link color
1371    pub fn with_link(&mut self, link: &str) -> &mut Self {
1372        self.link = link.to_string();
1373        self
1374    }
1375
1376    /// Builds the final color scheme
1377    pub fn build(&self) -> Self {
1378        Self { ..self.clone() }
1379    }
1380}
1381
1382/// Page layout configuration
1383///
1384/// Defines the layout properties for pages in the document, including
1385/// margins, text alignment, and paragraph spacing.
1386#[cfg(feature = "content-builder")]
1387#[derive(Debug, Clone)]
1388pub struct PageLayout {
1389    /// The page margin (default: 20, unit: pixels)
1390    ///
1391    /// Controls the space around the content area on each page.
1392    pub margin: usize,
1393
1394    /// The text alignment mode (default: TextAlign::Left)
1395    ///
1396    /// Controls how text is aligned within the content area.
1397    pub text_align: TextAlign,
1398
1399    /// The spacing between paragraphs (default: 16, unit: pixels)
1400    ///
1401    /// Controls the vertical space between block-level elements.
1402    pub paragraph_spacing: usize,
1403}
1404
1405#[cfg(feature = "content-builder")]
1406impl Default for PageLayout {
1407    fn default() -> Self {
1408        Self {
1409            margin: 20,
1410            text_align: Default::default(),
1411            paragraph_spacing: 16,
1412        }
1413    }
1414}
1415
1416#[cfg(feature = "content-builder")]
1417impl PageLayout {
1418    /// Creates a new page layout with default values
1419    pub fn new() -> Self {
1420        Self::default()
1421    }
1422
1423    /// Sets the page margin
1424    pub fn with_margin(&mut self, margin: usize) -> &mut Self {
1425        self.margin = margin;
1426        self
1427    }
1428
1429    /// Sets the text alignment
1430    pub fn with_text_align(&mut self, text_align: TextAlign) -> &mut Self {
1431        self.text_align = text_align;
1432        self
1433    }
1434
1435    /// Sets the paragraph spacing
1436    pub fn with_paragraph_spacing(&mut self, paragraph_spacing: usize) -> &mut Self {
1437        self.paragraph_spacing = paragraph_spacing;
1438        self
1439    }
1440
1441    /// Builds the final page layout
1442    pub fn build(&self) -> Self {
1443        Self { ..self.clone() }
1444    }
1445}
1446
1447/// Text alignment options
1448///
1449/// Defines the available text alignment modes for content in the document.
1450#[cfg(feature = "content-builder")]
1451#[derive(Debug, Default, Clone, Copy, PartialEq)]
1452pub enum TextAlign {
1453    /// Left-aligned text
1454    ///
1455    /// Text is aligned to the left margin, with the right edge ragged.
1456    #[default]
1457    Left,
1458
1459    /// Right-aligned text
1460    ///
1461    /// Text is aligned to the right margin, with the left edge ragged.
1462    Right,
1463
1464    /// Justified text
1465    ///
1466    /// Text is aligned to both margins by adjusting the spacing between
1467    /// words. The left and right edges are both straight.
1468    Justify,
1469
1470    /// Centered text
1471    ///
1472    /// Text is centered within the content area, with both edges ragged.
1473    Center,
1474}
1475
1476#[cfg(feature = "content-builder")]
1477impl std::fmt::Display for TextAlign {
1478    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1479        match self {
1480            TextAlign::Left => write!(f, "left"),
1481            TextAlign::Right => write!(f, "right"),
1482            TextAlign::Justify => write!(f, "justify"),
1483            TextAlign::Center => write!(f, "center"),
1484        }
1485    }
1486}
1487
1488#[cfg(test)]
1489mod tests {
1490    mod navpoint_tests {
1491        use std::path::PathBuf;
1492
1493        use crate::types::NavPoint;
1494
1495        /// Testing the equality comparison of NavPoint
1496        #[test]
1497        fn test_navpoint_partial_eq() {
1498            let nav1 = NavPoint {
1499                label: "Chapter 1".to_string(),
1500                content: Some(PathBuf::from("chapter1.html")),
1501                children: vec![],
1502                play_order: Some(1),
1503            };
1504
1505            let nav2 = NavPoint {
1506                label: "Chapter 1".to_string(),
1507                content: Some(PathBuf::from("chapter2.html")),
1508                children: vec![],
1509                play_order: Some(1),
1510            };
1511
1512            let nav3 = NavPoint {
1513                label: "Chapter 2".to_string(),
1514                content: Some(PathBuf::from("chapter1.html")),
1515                children: vec![],
1516                play_order: Some(2),
1517            };
1518
1519            assert_eq!(nav1, nav2); // Same play_order, different contents, should be equal
1520            assert_ne!(nav1, nav3); // Different play_order, Same contents, should be unequal
1521        }
1522
1523        /// Test NavPoint sorting comparison
1524        #[test]
1525        fn test_navpoint_ord() {
1526            let nav1 = NavPoint {
1527                label: "Chapter 1".to_string(),
1528                content: Some(PathBuf::from("chapter1.html")),
1529                children: vec![],
1530                play_order: Some(1),
1531            };
1532
1533            let nav2 = NavPoint {
1534                label: "Chapter 2".to_string(),
1535                content: Some(PathBuf::from("chapter2.html")),
1536                children: vec![],
1537                play_order: Some(2),
1538            };
1539
1540            let nav3 = NavPoint {
1541                label: "Chapter 3".to_string(),
1542                content: Some(PathBuf::from("chapter3.html")),
1543                children: vec![],
1544                play_order: Some(3),
1545            };
1546
1547            // Test function cmp
1548            assert!(nav1 < nav2);
1549            assert!(nav2 > nav1);
1550            assert!(nav1 == nav1);
1551
1552            // Test function partial_cmp
1553            assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
1554            assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
1555            assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
1556
1557            // Test function sort
1558            let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
1559            nav_points.sort();
1560            assert_eq!(nav_points, vec![nav1, nav2, nav3]);
1561        }
1562
1563        /// Test the case of None play_order
1564        #[test]
1565        fn test_navpoint_ord_with_none_play_order() {
1566            let nav_with_order = NavPoint {
1567                label: "Chapter 1".to_string(),
1568                content: Some(PathBuf::from("chapter1.html")),
1569                children: vec![],
1570                play_order: Some(1),
1571            };
1572
1573            let nav_without_order = NavPoint {
1574                label: "Preface".to_string(),
1575                content: Some(PathBuf::from("preface.html")),
1576                children: vec![],
1577                play_order: None,
1578            };
1579
1580            assert!(nav_without_order < nav_with_order);
1581            assert!(nav_with_order > nav_without_order);
1582
1583            let nav_without_order2 = NavPoint {
1584                label: "Introduction".to_string(),
1585                content: Some(PathBuf::from("intro.html")),
1586                children: vec![],
1587                play_order: None,
1588            };
1589
1590            assert!(nav_without_order == nav_without_order2);
1591        }
1592
1593        /// Test NavPoint containing child nodes
1594        #[test]
1595        fn test_navpoint_with_children() {
1596            let child1 = NavPoint {
1597                label: "Section 1.1".to_string(),
1598                content: Some(PathBuf::from("section1_1.html")),
1599                children: vec![],
1600                play_order: Some(1),
1601            };
1602
1603            let child2 = NavPoint {
1604                label: "Section 1.2".to_string(),
1605                content: Some(PathBuf::from("section1_2.html")),
1606                children: vec![],
1607                play_order: Some(2),
1608            };
1609
1610            let parent1 = NavPoint {
1611                label: "Chapter 1".to_string(),
1612                content: Some(PathBuf::from("chapter1.html")),
1613                children: vec![child1.clone(), child2.clone()],
1614                play_order: Some(1),
1615            };
1616
1617            let parent2 = NavPoint {
1618                label: "Chapter 1".to_string(),
1619                content: Some(PathBuf::from("chapter1.html")),
1620                children: vec![child1.clone(), child2.clone()],
1621                play_order: Some(1),
1622            };
1623
1624            assert!(parent1 == parent2);
1625
1626            let parent3 = NavPoint {
1627                label: "Chapter 2".to_string(),
1628                content: Some(PathBuf::from("chapter2.html")),
1629                children: vec![child1.clone(), child2.clone()],
1630                play_order: Some(2),
1631            };
1632
1633            assert!(parent1 != parent3);
1634            assert!(parent1 < parent3);
1635        }
1636
1637        /// Test the case where content is None
1638        #[test]
1639        fn test_navpoint_with_none_content() {
1640            let nav1 = NavPoint {
1641                label: "Chapter 1".to_string(),
1642                content: None,
1643                children: vec![],
1644                play_order: Some(1),
1645            };
1646
1647            let nav2 = NavPoint {
1648                label: "Chapter 1".to_string(),
1649                content: None,
1650                children: vec![],
1651                play_order: Some(1),
1652            };
1653
1654            assert!(nav1 == nav2);
1655        }
1656    }
1657
1658    #[cfg(feature = "builder")]
1659    mod builder_tests {
1660        mod metadata_item {
1661            use crate::types::{MetadataItem, MetadataRefinement};
1662
1663            #[test]
1664            fn test_metadata_item_new() {
1665                let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1666
1667                assert_eq!(metadata_item.property, "title");
1668                assert_eq!(metadata_item.value, "EPUB Test Book");
1669                assert_eq!(metadata_item.id, None);
1670                assert_eq!(metadata_item.lang, None);
1671                assert_eq!(metadata_item.refined.len(), 0);
1672            }
1673
1674            #[test]
1675            fn test_metadata_item_with_id() {
1676                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1677                metadata_item.with_id("creator-1");
1678
1679                assert_eq!(metadata_item.property, "creator");
1680                assert_eq!(metadata_item.value, "John Doe");
1681                assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1682                assert_eq!(metadata_item.lang, None);
1683                assert_eq!(metadata_item.refined.len(), 0);
1684            }
1685
1686            #[test]
1687            fn test_metadata_item_with_lang() {
1688                let mut metadata_item = MetadataItem::new("title", "测试书籍");
1689                metadata_item.with_lang("zh-CN");
1690
1691                assert_eq!(metadata_item.property, "title");
1692                assert_eq!(metadata_item.value, "测试书籍");
1693                assert_eq!(metadata_item.id, None);
1694                assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1695                assert_eq!(metadata_item.refined.len(), 0);
1696            }
1697
1698            #[test]
1699            fn test_metadata_item_append_refinement() {
1700                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1701                metadata_item.with_id("creator-1"); // ID is required for refinements
1702
1703                let refinement = MetadataRefinement::new("creator-1", "role", "author");
1704                metadata_item.append_refinement(refinement);
1705
1706                assert_eq!(metadata_item.refined.len(), 1);
1707                assert_eq!(metadata_item.refined[0].refines, "creator-1");
1708                assert_eq!(metadata_item.refined[0].property, "role");
1709                assert_eq!(metadata_item.refined[0].value, "author");
1710            }
1711
1712            #[test]
1713            fn test_metadata_item_append_refinement_without_id() {
1714                let mut metadata_item = MetadataItem::new("title", "Test Book");
1715                // No ID set
1716
1717                let refinement = MetadataRefinement::new("title", "title-type", "main");
1718                metadata_item.append_refinement(refinement);
1719
1720                // Refinement should not be added because metadata item has no ID
1721                assert_eq!(metadata_item.refined.len(), 0);
1722            }
1723
1724            #[test]
1725            fn test_metadata_item_build() {
1726                let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1727                metadata_item.with_id("pub-id").with_lang("en");
1728
1729                let built = metadata_item.build();
1730
1731                assert_eq!(built.property, "identifier");
1732                assert_eq!(built.value, "urn:isbn:1234567890");
1733                assert_eq!(built.id, Some("pub-id".to_string()));
1734                assert_eq!(built.lang, Some("en".to_string()));
1735                assert_eq!(built.refined.len(), 0);
1736            }
1737
1738            #[test]
1739            fn test_metadata_item_builder_chaining() {
1740                let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1741                metadata_item.with_id("title").with_lang("en");
1742
1743                let refinement = MetadataRefinement::new("title", "title-type", "main");
1744                metadata_item.append_refinement(refinement);
1745
1746                let built = metadata_item.build();
1747
1748                assert_eq!(built.property, "title");
1749                assert_eq!(built.value, "EPUB 3.3 Guide");
1750                assert_eq!(built.id, Some("title".to_string()));
1751                assert_eq!(built.lang, Some("en".to_string()));
1752                assert_eq!(built.refined.len(), 1);
1753            }
1754
1755            #[test]
1756            fn test_metadata_item_attributes_dc_namespace() {
1757                let mut metadata_item = MetadataItem::new("title", "Test Book");
1758                metadata_item.with_id("title-id");
1759
1760                let attributes = metadata_item.attributes();
1761
1762                // For DC namespace properties, no "property" attribute should be added
1763                assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1764                assert!(
1765                    attributes
1766                        .iter()
1767                        .any(|(k, v)| k == &"id" && v == &"title-id")
1768                );
1769            }
1770
1771            #[test]
1772            fn test_metadata_item_attributes_non_dc_namespace() {
1773                let mut metadata_item = MetadataItem::new("meta", "value");
1774                metadata_item.with_id("meta-id");
1775
1776                let attributes = metadata_item.attributes();
1777
1778                // For non-DC namespace properties, "property" attribute should be added
1779                assert!(attributes.iter().any(|(k, _)| k == &"property"));
1780                assert!(
1781                    attributes
1782                        .iter()
1783                        .any(|(k, v)| k == &"id" && v == &"meta-id")
1784                );
1785            }
1786
1787            #[test]
1788            fn test_metadata_item_attributes_with_lang() {
1789                let mut metadata_item = MetadataItem::new("title", "Test Book");
1790                metadata_item.with_id("title-id").with_lang("en");
1791
1792                let attributes = metadata_item.attributes();
1793
1794                assert!(
1795                    attributes
1796                        .iter()
1797                        .any(|(k, v)| k == &"id" && v == &"title-id")
1798                );
1799                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1800            }
1801        }
1802
1803        mod metadata_refinement {
1804            use crate::types::MetadataRefinement;
1805
1806            #[test]
1807            fn test_metadata_refinement_new() {
1808                let refinement = MetadataRefinement::new("title", "title-type", "main");
1809
1810                assert_eq!(refinement.refines, "title");
1811                assert_eq!(refinement.property, "title-type");
1812                assert_eq!(refinement.value, "main");
1813                assert_eq!(refinement.lang, None);
1814                assert_eq!(refinement.scheme, None);
1815            }
1816
1817            #[test]
1818            fn test_metadata_refinement_with_lang() {
1819                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1820                refinement.with_lang("en");
1821
1822                assert_eq!(refinement.refines, "creator");
1823                assert_eq!(refinement.property, "role");
1824                assert_eq!(refinement.value, "author");
1825                assert_eq!(refinement.lang, Some("en".to_string()));
1826                assert_eq!(refinement.scheme, None);
1827            }
1828
1829            #[test]
1830            fn test_metadata_refinement_with_scheme() {
1831                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1832                refinement.with_scheme("marc:relators");
1833
1834                assert_eq!(refinement.refines, "creator");
1835                assert_eq!(refinement.property, "role");
1836                assert_eq!(refinement.value, "author");
1837                assert_eq!(refinement.lang, None);
1838                assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1839            }
1840
1841            #[test]
1842            fn test_metadata_refinement_build() {
1843                let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1844                refinement.with_lang("ja").with_scheme("iso-15924");
1845
1846                let built = refinement.build();
1847
1848                assert_eq!(built.refines, "title");
1849                assert_eq!(built.property, "alternate-script");
1850                assert_eq!(built.value, "テスト");
1851                assert_eq!(built.lang, Some("ja".to_string()));
1852                assert_eq!(built.scheme, Some("iso-15924".to_string()));
1853            }
1854
1855            #[test]
1856            fn test_metadata_refinement_builder_chaining() {
1857                let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1858                refinement.with_lang("en").with_scheme("dcterms");
1859
1860                let built = refinement.build();
1861
1862                assert_eq!(built.refines, "creator");
1863                assert_eq!(built.property, "file-as");
1864                assert_eq!(built.value, "Doe, John");
1865                assert_eq!(built.lang, Some("en".to_string()));
1866                assert_eq!(built.scheme, Some("dcterms".to_string()));
1867            }
1868
1869            #[test]
1870            fn test_metadata_refinement_attributes() {
1871                let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1872                refinement.with_lang("en").with_scheme("onix:codelist5");
1873
1874                let attributes = refinement.attributes();
1875
1876                assert!(
1877                    attributes
1878                        .iter()
1879                        .any(|(k, v)| k == &"refines" && v == &"title")
1880                );
1881                assert!(
1882                    attributes
1883                        .iter()
1884                        .any(|(k, v)| k == &"property" && v == &"title-type")
1885                );
1886                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1887                assert!(
1888                    attributes
1889                        .iter()
1890                        .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1891                );
1892            }
1893
1894            #[test]
1895            fn test_metadata_refinement_attributes_optional_fields() {
1896                let refinement = MetadataRefinement::new("creator", "role", "author");
1897                let attributes = refinement.attributes();
1898
1899                assert!(
1900                    attributes
1901                        .iter()
1902                        .any(|(k, v)| k == &"refines" && v == &"creator")
1903                );
1904                assert!(
1905                    attributes
1906                        .iter()
1907                        .any(|(k, v)| k == &"property" && v == &"role")
1908                );
1909
1910                // Should not contain optional attributes when they are None
1911                assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1912                assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1913            }
1914        }
1915
1916        mod manifest_item {
1917            use std::path::PathBuf;
1918
1919            use crate::types::ManifestItem;
1920
1921            #[test]
1922            fn test_manifest_item_new() {
1923                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1924                assert!(manifest_item.is_ok());
1925
1926                let manifest_item = manifest_item.unwrap();
1927                assert_eq!(manifest_item.id, "cover");
1928                assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1929                assert_eq!(manifest_item.mime, "");
1930                assert_eq!(manifest_item.properties, None);
1931                assert_eq!(manifest_item.fallback, None);
1932            }
1933
1934            #[test]
1935            fn test_manifest_item_append_property() {
1936                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1937                assert!(manifest_item.is_ok());
1938
1939                let mut manifest_item = manifest_item.unwrap();
1940                manifest_item.append_property("nav");
1941
1942                assert_eq!(manifest_item.id, "nav");
1943                assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1944                assert_eq!(manifest_item.mime, "");
1945                assert_eq!(manifest_item.properties, Some("nav".to_string()));
1946                assert_eq!(manifest_item.fallback, None);
1947            }
1948
1949            #[test]
1950            fn test_manifest_item_append_multiple_properties() {
1951                let manifest_item = ManifestItem::new("content", "content.xhtml");
1952                assert!(manifest_item.is_ok());
1953
1954                let mut manifest_item = manifest_item.unwrap();
1955                manifest_item
1956                    .append_property("nav")
1957                    .append_property("scripted")
1958                    .append_property("svg");
1959
1960                assert_eq!(
1961                    manifest_item.properties,
1962                    Some("nav scripted svg".to_string())
1963                );
1964            }
1965
1966            #[test]
1967            fn test_manifest_item_with_fallback() {
1968                let manifest_item = ManifestItem::new("image", "image.tiff");
1969                assert!(manifest_item.is_ok());
1970
1971                let mut manifest_item = manifest_item.unwrap();
1972                manifest_item.with_fallback("image-fallback");
1973
1974                assert_eq!(manifest_item.id, "image");
1975                assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1976                assert_eq!(manifest_item.mime, "");
1977                assert_eq!(manifest_item.properties, None);
1978                assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1979            }
1980
1981            #[test]
1982            fn test_manifest_item_set_mime() {
1983                let manifest_item = ManifestItem::new("style", "style.css");
1984                assert!(manifest_item.is_ok());
1985
1986                let manifest_item = manifest_item.unwrap();
1987                let updated_item = manifest_item.set_mime("text/css");
1988
1989                assert_eq!(updated_item.id, "style");
1990                assert_eq!(updated_item.path, PathBuf::from("style.css"));
1991                assert_eq!(updated_item.mime, "text/css");
1992                assert_eq!(updated_item.properties, None);
1993                assert_eq!(updated_item.fallback, None);
1994            }
1995
1996            #[test]
1997            fn test_manifest_item_build() {
1998                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1999                assert!(manifest_item.is_ok());
2000
2001                let mut manifest_item = manifest_item.unwrap();
2002                manifest_item
2003                    .append_property("cover-image")
2004                    .with_fallback("cover-fallback");
2005
2006                let built = manifest_item.build();
2007
2008                assert_eq!(built.id, "cover");
2009                assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
2010                assert_eq!(built.mime, "");
2011                assert_eq!(built.properties, Some("cover-image".to_string()));
2012                assert_eq!(built.fallback, Some("cover-fallback".to_string()));
2013            }
2014
2015            #[test]
2016            fn test_manifest_item_builder_chaining() {
2017                let manifest_item = ManifestItem::new("content", "content.xhtml");
2018                assert!(manifest_item.is_ok());
2019
2020                let mut manifest_item = manifest_item.unwrap();
2021                manifest_item
2022                    .append_property("scripted")
2023                    .append_property("svg")
2024                    .with_fallback("fallback-content");
2025
2026                let built = manifest_item.build();
2027
2028                assert_eq!(built.id, "content");
2029                assert_eq!(built.path, PathBuf::from("content.xhtml"));
2030                assert_eq!(built.mime, "");
2031                assert_eq!(built.properties, Some("scripted svg".to_string()));
2032                assert_eq!(built.fallback, Some("fallback-content".to_string()));
2033            }
2034
2035            #[test]
2036            fn test_manifest_item_attributes() {
2037                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
2038                assert!(manifest_item.is_ok());
2039
2040                let mut manifest_item = manifest_item.unwrap();
2041                manifest_item
2042                    .append_property("nav")
2043                    .with_fallback("fallback-nav");
2044
2045                // Manually set mime type for testing
2046                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
2047                let attributes = manifest_item.attributes();
2048
2049                // Check that all expected attributes are present
2050                assert!(attributes.contains(&("id", "nav")));
2051                assert!(attributes.contains(&("href", "nav.xhtml")));
2052                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
2053                assert!(attributes.contains(&("properties", "nav")));
2054                assert!(attributes.contains(&("fallback", "fallback-nav")));
2055            }
2056
2057            #[test]
2058            fn test_manifest_item_attributes_optional_fields() {
2059                let manifest_item = ManifestItem::new("simple", "simple.xhtml");
2060                assert!(manifest_item.is_ok());
2061
2062                let manifest_item = manifest_item.unwrap();
2063                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
2064                let attributes = manifest_item.attributes();
2065
2066                // Should contain required attributes
2067                assert!(attributes.contains(&("id", "simple")));
2068                assert!(attributes.contains(&("href", "simple.xhtml")));
2069                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
2070
2071                // Should not contain optional attributes when they are None
2072                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
2073                assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
2074            }
2075
2076            #[test]
2077            fn test_manifest_item_path_handling() {
2078                let manifest_item = ManifestItem::new("test", "../images/test.png");
2079                assert!(manifest_item.is_err());
2080
2081                let err = manifest_item.unwrap_err();
2082                assert_eq!(
2083                    err.to_string(),
2084                    "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
2085                );
2086            }
2087        }
2088
2089        mod spine_item {
2090            use crate::types::SpineItem;
2091
2092            #[test]
2093            fn test_spine_item_new() {
2094                let spine_item = SpineItem::new("content_001");
2095
2096                assert_eq!(spine_item.idref, "content_001");
2097                assert_eq!(spine_item.id, None);
2098                assert_eq!(spine_item.properties, None);
2099                assert_eq!(spine_item.linear, true);
2100            }
2101
2102            #[test]
2103            fn test_spine_item_with_id() {
2104                let mut spine_item = SpineItem::new("content_001");
2105                spine_item.with_id("spine1");
2106
2107                assert_eq!(spine_item.idref, "content_001");
2108                assert_eq!(spine_item.id, Some("spine1".to_string()));
2109                assert_eq!(spine_item.properties, None);
2110                assert_eq!(spine_item.linear, true);
2111            }
2112
2113            #[test]
2114            fn test_spine_item_append_property() {
2115                let mut spine_item = SpineItem::new("content_001");
2116                spine_item.append_property("page-spread-left");
2117
2118                assert_eq!(spine_item.idref, "content_001");
2119                assert_eq!(spine_item.id, None);
2120                assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
2121                assert_eq!(spine_item.linear, true);
2122            }
2123
2124            #[test]
2125            fn test_spine_item_append_multiple_properties() {
2126                let mut spine_item = SpineItem::new("content_001");
2127                spine_item
2128                    .append_property("page-spread-left")
2129                    .append_property("rendition:layout-pre-paginated");
2130
2131                assert_eq!(
2132                    spine_item.properties,
2133                    Some("page-spread-left rendition:layout-pre-paginated".to_string())
2134                );
2135            }
2136
2137            #[test]
2138            fn test_spine_item_set_linear() {
2139                let mut spine_item = SpineItem::new("content_001");
2140                spine_item.set_linear(false);
2141
2142                assert_eq!(spine_item.idref, "content_001");
2143                assert_eq!(spine_item.id, None);
2144                assert_eq!(spine_item.properties, None);
2145                assert_eq!(spine_item.linear, false);
2146            }
2147
2148            #[test]
2149            fn test_spine_item_build() {
2150                let mut spine_item = SpineItem::new("content_001");
2151                spine_item
2152                    .with_id("spine1")
2153                    .append_property("page-spread-left")
2154                    .set_linear(false);
2155
2156                let built = spine_item.build();
2157
2158                assert_eq!(built.idref, "content_001");
2159                assert_eq!(built.id, Some("spine1".to_string()));
2160                assert_eq!(built.properties, Some("page-spread-left".to_string()));
2161                assert_eq!(built.linear, false);
2162            }
2163
2164            #[test]
2165            fn test_spine_item_builder_chaining() {
2166                let mut spine_item = SpineItem::new("content_001");
2167                spine_item
2168                    .with_id("spine1")
2169                    .append_property("page-spread-left")
2170                    .set_linear(false);
2171
2172                let built = spine_item.build();
2173
2174                assert_eq!(built.idref, "content_001");
2175                assert_eq!(built.id, Some("spine1".to_string()));
2176                assert_eq!(built.properties, Some("page-spread-left".to_string()));
2177                assert_eq!(built.linear, false);
2178            }
2179
2180            #[test]
2181            fn test_spine_item_attributes() {
2182                let mut spine_item = SpineItem::new("content_001");
2183                spine_item
2184                    .with_id("spine1")
2185                    .append_property("page-spread-left")
2186                    .set_linear(false);
2187
2188                let attributes = spine_item.attributes();
2189
2190                // Check that all expected attributes are present
2191                assert!(attributes.contains(&("idref", "content_001")));
2192                assert!(attributes.contains(&("id", "spine1")));
2193                assert!(attributes.contains(&("properties", "page-spread-left")));
2194                assert!(attributes.contains(&("linear", "no"))); // false should become "no"
2195            }
2196
2197            #[test]
2198            fn test_spine_item_attributes_linear_yes() {
2199                let spine_item = SpineItem::new("content_001");
2200                let attributes = spine_item.attributes();
2201
2202                // Linear true should become "yes"
2203                assert!(attributes.contains(&("linear", "yes")));
2204            }
2205
2206            #[test]
2207            fn test_spine_item_attributes_optional_fields() {
2208                let spine_item = SpineItem::new("content_001");
2209                let attributes = spine_item.attributes();
2210
2211                // Should only contain required attributes when optional fields are None
2212                assert!(attributes.contains(&("idref", "content_001")));
2213                assert!(attributes.contains(&("linear", "yes")));
2214
2215                // Should not contain optional attributes when they are None
2216                assert!(!attributes.iter().any(|(k, _)| k == &"id"));
2217                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
2218            }
2219        }
2220
2221        mod metadata_sheet {
2222            use crate::types::{MetadataItem, MetadataSheet};
2223
2224            #[test]
2225            fn test_metadata_sheet_new() {
2226                let sheet = MetadataSheet::new();
2227
2228                assert!(sheet.contributor.is_empty());
2229                assert!(sheet.creator.is_empty());
2230                assert!(sheet.date.is_empty());
2231                assert!(sheet.identifier.is_empty());
2232                assert!(sheet.language.is_empty());
2233                assert!(sheet.relation.is_empty());
2234                assert!(sheet.subject.is_empty());
2235                assert!(sheet.title.is_empty());
2236
2237                assert!(sheet.coverage.is_empty());
2238                assert!(sheet.description.is_empty());
2239                assert!(sheet.format.is_empty());
2240                assert!(sheet.publisher.is_empty());
2241                assert!(sheet.rights.is_empty());
2242                assert!(sheet.source.is_empty());
2243                assert!(sheet.epub_type.is_empty());
2244            }
2245
2246            #[test]
2247            fn test_metadata_sheet_append_vec_fields() {
2248                let mut sheet = MetadataSheet::new();
2249
2250                sheet
2251                    .append_title("Test Book")
2252                    .append_creator("John Doe")
2253                    .append_creator("Jane Smith")
2254                    .append_contributor("Editor One")
2255                    .append_language("en")
2256                    .append_language("zh-CN")
2257                    .append_subject("Fiction")
2258                    .append_subject("Drama")
2259                    .append_relation("prequel");
2260
2261                assert_eq!(sheet.title.len(), 1);
2262                assert_eq!(sheet.title[0], "Test Book");
2263
2264                assert_eq!(sheet.creator.len(), 2);
2265                assert_eq!(sheet.creator[0], "John Doe");
2266                assert_eq!(sheet.creator[1], "Jane Smith");
2267
2268                assert_eq!(sheet.contributor.len(), 1);
2269                assert_eq!(sheet.contributor[0], "Editor One");
2270
2271                assert_eq!(sheet.language.len(), 2);
2272                assert_eq!(sheet.language[0], "en");
2273                assert_eq!(sheet.language[1], "zh-CN");
2274
2275                assert_eq!(sheet.subject.len(), 2);
2276                assert_eq!(sheet.subject[0], "Fiction");
2277                assert_eq!(sheet.subject[1], "Drama");
2278
2279                assert_eq!(sheet.relation.len(), 1);
2280                assert_eq!(sheet.relation[0], "prequel");
2281            }
2282
2283            #[test]
2284            fn test_metadata_sheet_append_date_and_identifier() {
2285                let mut sheet = MetadataSheet::new();
2286
2287                sheet
2288                    .append_date("2024-01-15", "publication")
2289                    .append_date("2024-01-10", "creation")
2290                    .append_identifier("book-id", "urn:isbn:1234567890")
2291                    .append_identifier("uuid-id", "urn:uuid:12345678-1234-1234-1234-123456789012");
2292
2293                assert_eq!(sheet.date.len(), 2);
2294                assert_eq!(
2295                    sheet.date.get("2024-01-15"),
2296                    Some(&"publication".to_string())
2297                );
2298                assert_eq!(sheet.date.get("2024-01-10"), Some(&"creation".to_string()));
2299
2300                assert_eq!(sheet.identifier.len(), 2);
2301                assert_eq!(
2302                    sheet.identifier.get("book-id"),
2303                    Some(&"urn:isbn:1234567890".to_string())
2304                );
2305                assert_eq!(
2306                    sheet.identifier.get("uuid-id"),
2307                    Some(&"urn:uuid:12345678-1234-1234-1234-123456789012".to_string())
2308                );
2309            }
2310
2311            #[test]
2312            fn test_metadata_sheet_with_string_fields() {
2313                let mut sheet = MetadataSheet::new();
2314
2315                sheet
2316                    .with_coverage("Spatial coverage")
2317                    .with_description("A test book description")
2318                    .with_format("application/epub+zip")
2319                    .with_publisher("Test Publisher")
2320                    .with_rights("Copyright 2024")
2321                    .with_source("Original source")
2322                    .with_epub_type("buku");
2323
2324                assert_eq!(sheet.coverage, "Spatial coverage");
2325                assert_eq!(sheet.description, "A test book description");
2326                assert_eq!(sheet.format, "application/epub+zip");
2327                assert_eq!(sheet.publisher, "Test Publisher");
2328                assert_eq!(sheet.rights, "Copyright 2024");
2329                assert_eq!(sheet.source, "Original source");
2330                assert_eq!(sheet.epub_type, "buku");
2331            }
2332
2333            #[test]
2334            fn test_metadata_sheet_builder_chaining() {
2335                let mut sheet = MetadataSheet::new();
2336
2337                sheet
2338                    .append_title("Chained Book")
2339                    .append_creator("Chained Author")
2340                    .append_date("2024-01-01", "")
2341                    .append_identifier("id-1", "test-id")
2342                    .with_publisher("Chained Publisher")
2343                    .with_description("Chained description");
2344
2345                assert_eq!(sheet.title.len(), 1);
2346                assert_eq!(sheet.title[0], "Chained Book");
2347
2348                assert_eq!(sheet.creator.len(), 1);
2349                assert_eq!(sheet.creator[0], "Chained Author");
2350
2351                assert_eq!(sheet.date.len(), 1);
2352                assert_eq!(sheet.identifier.len(), 1);
2353                assert_eq!(sheet.publisher, "Chained Publisher");
2354                assert_eq!(sheet.description, "Chained description");
2355            }
2356
2357            #[test]
2358            fn test_metadata_sheet_build() {
2359                let mut sheet = MetadataSheet::new();
2360                sheet
2361                    .append_title("Original Title")
2362                    .with_publisher("Original Publisher");
2363
2364                let built = sheet.build();
2365
2366                assert_eq!(built.title.len(), 1);
2367                assert_eq!(built.title[0], "Original Title");
2368                assert_eq!(built.publisher, "Original Publisher");
2369
2370                sheet.append_title("New Title");
2371                sheet.with_publisher("New Publisher");
2372
2373                assert_eq!(sheet.title.len(), 2);
2374                assert_eq!(built.title.len(), 1);
2375                assert_eq!(built.publisher, "Original Publisher");
2376            }
2377
2378            #[test]
2379            fn test_metadata_sheet_into_metadata_items() {
2380                let mut sheet = MetadataSheet::new();
2381                sheet
2382                    .append_title("Test Title")
2383                    .append_creator("Test Creator")
2384                    .with_description("Test Description")
2385                    .with_publisher("Test Publisher");
2386
2387                let items: Vec<MetadataItem> = sheet.into();
2388
2389                assert_eq!(items.len(), 4);
2390
2391                assert!(
2392                    items
2393                        .iter()
2394                        .any(|i| i.property == "title" && i.value == "Test Title")
2395                );
2396
2397                assert!(
2398                    items
2399                        .iter()
2400                        .any(|i| i.property == "creator" && i.value == "Test Creator")
2401                );
2402
2403                assert!(
2404                    items
2405                        .iter()
2406                        .any(|i| i.property == "description" && i.value == "Test Description")
2407                );
2408
2409                assert!(
2410                    items
2411                        .iter()
2412                        .any(|i| i.property == "publisher" && i.value == "Test Publisher")
2413                );
2414            }
2415
2416            #[test]
2417            fn test_metadata_sheet_into_metadata_items_with_date_and_identifier() {
2418                let mut sheet = MetadataSheet::new();
2419                sheet
2420                    .append_date("2024-01-15", "publication")
2421                    .append_identifier("book-id", "urn:isbn:9876543210");
2422
2423                let items: Vec<MetadataItem> = sheet.into();
2424
2425                assert_eq!(items.len(), 2);
2426
2427                let date_item = items.iter().find(|i| i.property == "date").unwrap();
2428
2429                assert_eq!(date_item.value, "2024-01-15");
2430                assert!(date_item.id.is_some());
2431                assert_eq!(date_item.refined.len(), 1);
2432                assert_eq!(date_item.refined[0].property, "event");
2433                assert_eq!(date_item.refined[0].value, "publication");
2434
2435                let id_item = items.iter().find(|i| i.property == "identifier").unwrap();
2436
2437                assert_eq!(id_item.value, "urn:isbn:9876543210");
2438                assert_eq!(id_item.id, Some("book-id".to_string()));
2439            }
2440
2441            #[test]
2442            fn test_metadata_sheet_into_metadata_items_ignores_empty_fields() {
2443                let mut sheet = MetadataSheet::new();
2444                sheet.append_title("Valid Title").with_description(""); // Empty string should be ignored
2445
2446                let items: Vec<MetadataItem> = sheet.into();
2447
2448                assert_eq!(items.len(), 1);
2449                assert_eq!(items[0].property, "title");
2450            }
2451        }
2452
2453        mod navpoint {
2454
2455            use std::path::PathBuf;
2456
2457            use crate::types::NavPoint;
2458
2459            #[test]
2460            fn test_navpoint_new() {
2461                let navpoint = NavPoint::new("Test Chapter");
2462
2463                assert_eq!(navpoint.label, "Test Chapter");
2464                assert_eq!(navpoint.content, None);
2465                assert_eq!(navpoint.children.len(), 0);
2466            }
2467
2468            #[test]
2469            fn test_navpoint_with_content() {
2470                let mut navpoint = NavPoint::new("Test Chapter");
2471                navpoint.with_content("chapter1.html");
2472
2473                assert_eq!(navpoint.label, "Test Chapter");
2474                assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
2475                assert_eq!(navpoint.children.len(), 0);
2476            }
2477
2478            #[test]
2479            fn test_navpoint_append_child() {
2480                let mut parent = NavPoint::new("Parent Chapter");
2481
2482                let mut child1 = NavPoint::new("Child Section 1");
2483                child1.with_content("section1.html");
2484
2485                let mut child2 = NavPoint::new("Child Section 2");
2486                child2.with_content("section2.html");
2487
2488                parent.append_child(child1.build());
2489                parent.append_child(child2.build());
2490
2491                assert_eq!(parent.children.len(), 2);
2492                assert_eq!(parent.children[0].label, "Child Section 1");
2493                assert_eq!(parent.children[1].label, "Child Section 2");
2494            }
2495
2496            #[test]
2497            fn test_navpoint_set_children() {
2498                let mut navpoint = NavPoint::new("Main Chapter");
2499                let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
2500
2501                navpoint.set_children(children);
2502
2503                assert_eq!(navpoint.children.len(), 2);
2504                assert_eq!(navpoint.children[0].label, "Section 1");
2505                assert_eq!(navpoint.children[1].label, "Section 2");
2506            }
2507
2508            #[test]
2509            fn test_navpoint_build() {
2510                let mut navpoint = NavPoint::new("Complete Chapter");
2511                navpoint.with_content("complete.html");
2512
2513                let child = NavPoint::new("Sub Section");
2514                navpoint.append_child(child.build());
2515
2516                let built = navpoint.build();
2517
2518                assert_eq!(built.label, "Complete Chapter");
2519                assert_eq!(built.content, Some(PathBuf::from("complete.html")));
2520                assert_eq!(built.children.len(), 1);
2521                assert_eq!(built.children[0].label, "Sub Section");
2522            }
2523
2524            #[test]
2525            fn test_navpoint_builder_chaining() {
2526                let mut navpoint = NavPoint::new("Chained Chapter");
2527
2528                navpoint
2529                    .with_content("chained.html")
2530                    .append_child(NavPoint::new("Child 1").build())
2531                    .append_child(NavPoint::new("Child 2").build());
2532
2533                let built = navpoint.build();
2534
2535                assert_eq!(built.label, "Chained Chapter");
2536                assert_eq!(built.content, Some(PathBuf::from("chained.html")));
2537                assert_eq!(built.children.len(), 2);
2538            }
2539
2540            #[test]
2541            fn test_navpoint_empty_children() {
2542                let navpoint = NavPoint::new("No Children Chapter");
2543                let built = navpoint.build();
2544
2545                assert_eq!(built.children.len(), 0);
2546            }
2547
2548            #[test]
2549            fn test_navpoint_complex_hierarchy() {
2550                let mut root = NavPoint::new("Book");
2551
2552                let mut chapter1 = NavPoint::new("Chapter 1");
2553                chapter1
2554                    .with_content("chapter1.html")
2555                    .append_child(
2556                        NavPoint::new("Section 1.1")
2557                            .with_content("sec1_1.html")
2558                            .build(),
2559                    )
2560                    .append_child(
2561                        NavPoint::new("Section 1.2")
2562                            .with_content("sec1_2.html")
2563                            .build(),
2564                    );
2565
2566                let mut chapter2 = NavPoint::new("Chapter 2");
2567                chapter2.with_content("chapter2.html").append_child(
2568                    NavPoint::new("Section 2.1")
2569                        .with_content("sec2_1.html")
2570                        .build(),
2571                );
2572
2573                root.append_child(chapter1.build())
2574                    .append_child(chapter2.build());
2575
2576                let book = root.build();
2577
2578                assert_eq!(book.label, "Book");
2579                assert_eq!(book.children.len(), 2);
2580
2581                let ch1 = &book.children[0];
2582                assert_eq!(ch1.label, "Chapter 1");
2583                assert_eq!(ch1.children.len(), 2);
2584
2585                let ch2 = &book.children[1];
2586                assert_eq!(ch2.label, "Chapter 2");
2587                assert_eq!(ch2.children.len(), 1);
2588            }
2589        }
2590    }
2591
2592    #[cfg(feature = "content-builder")]
2593    mod footnote_tests {
2594        use crate::types::Footnote;
2595
2596        #[test]
2597        fn test_footnote_basic_creation() {
2598            let footnote = Footnote {
2599                locate: 100,
2600                content: "Sample footnote".to_string(),
2601            };
2602
2603            assert_eq!(footnote.locate, 100);
2604            assert_eq!(footnote.content, "Sample footnote");
2605        }
2606
2607        #[test]
2608        fn test_footnote_equality() {
2609            let footnote1 = Footnote {
2610                locate: 100,
2611                content: "First note".to_string(),
2612            };
2613
2614            let footnote2 = Footnote {
2615                locate: 100,
2616                content: "First note".to_string(),
2617            };
2618
2619            let footnote3 = Footnote {
2620                locate: 100,
2621                content: "Different note".to_string(),
2622            };
2623
2624            let footnote4 = Footnote {
2625                locate: 200,
2626                content: "First note".to_string(),
2627            };
2628
2629            assert_eq!(footnote1, footnote2);
2630            assert_ne!(footnote1, footnote3);
2631            assert_ne!(footnote1, footnote4);
2632        }
2633
2634        #[test]
2635        fn test_footnote_ordering() {
2636            let footnote1 = Footnote {
2637                locate: 100,
2638                content: "First".to_string(),
2639            };
2640
2641            let footnote2 = Footnote {
2642                locate: 200,
2643                content: "Second".to_string(),
2644            };
2645
2646            let footnote3 = Footnote {
2647                locate: 150,
2648                content: "Middle".to_string(),
2649            };
2650
2651            assert!(footnote1 < footnote2);
2652            assert!(footnote2 > footnote1);
2653            assert!(footnote1 < footnote3);
2654            assert!(footnote3 < footnote2);
2655            assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
2656        }
2657
2658        #[test]
2659        fn test_footnote_sorting() {
2660            let mut footnotes = vec![
2661                Footnote {
2662                    locate: 300,
2663                    content: "Third note".to_string(),
2664                },
2665                Footnote {
2666                    locate: 100,
2667                    content: "First note".to_string(),
2668                },
2669                Footnote {
2670                    locate: 200,
2671                    content: "Second note".to_string(),
2672                },
2673            ];
2674
2675            footnotes.sort();
2676
2677            assert_eq!(footnotes[0].locate, 100);
2678            assert_eq!(footnotes[1].locate, 200);
2679            assert_eq!(footnotes[2].locate, 300);
2680
2681            assert_eq!(footnotes[0].content, "First note");
2682            assert_eq!(footnotes[1].content, "Second note");
2683            assert_eq!(footnotes[2].content, "Third note");
2684        }
2685    }
2686
2687    #[cfg(feature = "content-builder")]
2688    mod block_type_tests {
2689        use crate::types::BlockType;
2690
2691        #[test]
2692        fn test_block_type_variants() {
2693            let _ = BlockType::Text;
2694            let _ = BlockType::Quote;
2695            let _ = BlockType::Title;
2696            let _ = BlockType::Image;
2697            let _ = BlockType::Audio;
2698            let _ = BlockType::Video;
2699            let _ = BlockType::MathML;
2700        }
2701
2702        #[test]
2703        fn test_block_type_debug() {
2704            let text = format!("{:?}", BlockType::Text);
2705            assert_eq!(text, "Text");
2706
2707            let quote = format!("{:?}", BlockType::Quote);
2708            assert_eq!(quote, "Quote");
2709
2710            let image = format!("{:?}", BlockType::Image);
2711            assert_eq!(image, "Image");
2712        }
2713    }
2714
2715    #[cfg(feature = "content-builder")]
2716    mod style_options_tests {
2717        use crate::types::{ColorScheme, PageLayout, StyleOptions, TextAlign, TextStyle};
2718
2719        #[test]
2720        fn test_style_options_default() {
2721            let options = StyleOptions::default();
2722
2723            assert_eq!(options.text.font_size, 1.0);
2724            assert_eq!(options.text.line_height, 1.6);
2725            assert_eq!(
2726                options.text.font_family,
2727                "-apple-system, Roboto, sans-serif"
2728            );
2729            assert_eq!(options.text.font_weight, "normal");
2730            assert_eq!(options.text.font_style, "normal");
2731            assert_eq!(options.text.letter_spacing, "normal");
2732            assert_eq!(options.text.text_indent, 2.0);
2733
2734            assert_eq!(options.color_scheme.background, "#FFFFFF");
2735            assert_eq!(options.color_scheme.text, "#000000");
2736            assert_eq!(options.color_scheme.link, "#6f6f6f");
2737
2738            assert_eq!(options.layout.margin, 20);
2739            assert_eq!(options.layout.text_align, TextAlign::Left);
2740            assert_eq!(options.layout.paragraph_spacing, 16);
2741        }
2742
2743        #[test]
2744        fn test_style_options_custom_values() {
2745            let text = TextStyle {
2746                font_size: 1.5,
2747                line_height: 2.0,
2748                font_family: "Georgia, serif".to_string(),
2749                font_weight: "bold".to_string(),
2750                font_style: "italic".to_string(),
2751                letter_spacing: "0.1em".to_string(),
2752                text_indent: 3.0,
2753            };
2754
2755            let color_scheme = ColorScheme {
2756                background: "#F0F0F0".to_string(),
2757                text: "#333333".to_string(),
2758                link: "#0066CC".to_string(),
2759            };
2760
2761            let layout = PageLayout {
2762                margin: 30,
2763                text_align: TextAlign::Center,
2764                paragraph_spacing: 20,
2765            };
2766
2767            let options = StyleOptions { text, color_scheme, layout };
2768
2769            assert_eq!(options.text.font_size, 1.5);
2770            assert_eq!(options.text.font_weight, "bold");
2771            assert_eq!(options.color_scheme.background, "#F0F0F0");
2772            assert_eq!(options.layout.text_align, TextAlign::Center);
2773        }
2774
2775        #[test]
2776        fn test_text_style_default() {
2777            let style = TextStyle::default();
2778
2779            assert_eq!(style.font_size, 1.0);
2780            assert_eq!(style.line_height, 1.6);
2781            assert_eq!(style.font_family, "-apple-system, Roboto, sans-serif");
2782            assert_eq!(style.font_weight, "normal");
2783            assert_eq!(style.font_style, "normal");
2784            assert_eq!(style.letter_spacing, "normal");
2785            assert_eq!(style.text_indent, 2.0);
2786        }
2787
2788        #[test]
2789        fn test_text_style_custom_values() {
2790            let style = TextStyle {
2791                font_size: 2.0,
2792                line_height: 1.8,
2793                font_family: "Times New Roman".to_string(),
2794                font_weight: "bold".to_string(),
2795                font_style: "italic".to_string(),
2796                letter_spacing: "0.05em".to_string(),
2797                text_indent: 0.0,
2798            };
2799
2800            assert_eq!(style.font_size, 2.0);
2801            assert_eq!(style.line_height, 1.8);
2802            assert_eq!(style.font_family, "Times New Roman");
2803            assert_eq!(style.font_weight, "bold");
2804            assert_eq!(style.font_style, "italic");
2805            assert_eq!(style.letter_spacing, "0.05em");
2806            assert_eq!(style.text_indent, 0.0);
2807        }
2808
2809        #[test]
2810        fn test_text_style_debug() {
2811            let style = TextStyle::default();
2812            let debug_str = format!("{:?}", style);
2813            assert!(debug_str.contains("TextStyle"));
2814            assert!(debug_str.contains("font_size"));
2815        }
2816
2817        #[test]
2818        fn test_color_scheme_default() {
2819            let scheme = ColorScheme::default();
2820
2821            assert_eq!(scheme.background, "#FFFFFF");
2822            assert_eq!(scheme.text, "#000000");
2823            assert_eq!(scheme.link, "#6f6f6f");
2824        }
2825
2826        #[test]
2827        fn test_color_scheme_custom_values() {
2828            let scheme = ColorScheme {
2829                background: "#000000".to_string(),
2830                text: "#FFFFFF".to_string(),
2831                link: "#00FF00".to_string(),
2832            };
2833
2834            assert_eq!(scheme.background, "#000000");
2835            assert_eq!(scheme.text, "#FFFFFF");
2836            assert_eq!(scheme.link, "#00FF00");
2837        }
2838
2839        #[test]
2840        fn test_color_scheme_debug() {
2841            let scheme = ColorScheme::default();
2842            let debug_str = format!("{:?}", scheme);
2843            assert!(debug_str.contains("ColorScheme"));
2844            assert!(debug_str.contains("background"));
2845        }
2846
2847        #[test]
2848        fn test_page_layout_default() {
2849            let layout = PageLayout::default();
2850
2851            assert_eq!(layout.margin, 20);
2852            assert_eq!(layout.text_align, TextAlign::Left);
2853            assert_eq!(layout.paragraph_spacing, 16);
2854        }
2855
2856        #[test]
2857        fn test_page_layout_custom_values() {
2858            let layout = PageLayout {
2859                margin: 40,
2860                text_align: TextAlign::Justify,
2861                paragraph_spacing: 24,
2862            };
2863
2864            assert_eq!(layout.margin, 40);
2865            assert_eq!(layout.text_align, TextAlign::Justify);
2866            assert_eq!(layout.paragraph_spacing, 24);
2867        }
2868
2869        #[test]
2870        fn test_page_layout_debug() {
2871            let layout = PageLayout::default();
2872            let debug_str = format!("{:?}", layout);
2873            assert!(debug_str.contains("PageLayout"));
2874            assert!(debug_str.contains("margin"));
2875        }
2876
2877        #[test]
2878        fn test_text_align_default() {
2879            let align = TextAlign::default();
2880            assert_eq!(align, TextAlign::Left);
2881        }
2882
2883        #[test]
2884        fn test_text_align_display() {
2885            assert_eq!(TextAlign::Left.to_string(), "left");
2886            assert_eq!(TextAlign::Right.to_string(), "right");
2887            assert_eq!(TextAlign::Justify.to_string(), "justify");
2888            assert_eq!(TextAlign::Center.to_string(), "center");
2889        }
2890
2891        #[test]
2892        fn test_text_align_all_variants() {
2893            let left = TextAlign::Left;
2894            let right = TextAlign::Right;
2895            let justify = TextAlign::Justify;
2896            let center = TextAlign::Center;
2897
2898            assert!(matches!(left, TextAlign::Left));
2899            assert!(matches!(right, TextAlign::Right));
2900            assert!(matches!(justify, TextAlign::Justify));
2901            assert!(matches!(center, TextAlign::Center));
2902        }
2903
2904        #[test]
2905        fn test_text_align_debug() {
2906            assert_eq!(format!("{:?}", TextAlign::Left), "Left");
2907            assert_eq!(format!("{:?}", TextAlign::Right), "Right");
2908            assert_eq!(format!("{:?}", TextAlign::Justify), "Justify");
2909            assert_eq!(format!("{:?}", TextAlign::Center), "Center");
2910        }
2911
2912        #[test]
2913        fn test_style_options_builder_new() {
2914            let options = StyleOptions::new();
2915            assert_eq!(options.text.font_size, 1.0);
2916            assert_eq!(options.color_scheme.background, "#FFFFFF");
2917            assert_eq!(options.layout.margin, 20);
2918        }
2919
2920        #[test]
2921        fn test_style_options_builder_with_text() {
2922            let mut options = StyleOptions::new();
2923            let text_style = TextStyle::new()
2924                .with_font_size(2.0)
2925                .with_font_weight("bold")
2926                .build();
2927            options.with_text(text_style);
2928
2929            assert_eq!(options.text.font_size, 2.0);
2930            assert_eq!(options.text.font_weight, "bold");
2931        }
2932
2933        #[test]
2934        fn test_style_options_builder_with_color_scheme() {
2935            let mut options = StyleOptions::new();
2936            let color = ColorScheme::new()
2937                .with_background("#000000")
2938                .with_text("#FFFFFF")
2939                .build();
2940            options.with_color_scheme(color);
2941
2942            assert_eq!(options.color_scheme.background, "#000000");
2943            assert_eq!(options.color_scheme.text, "#FFFFFF");
2944        }
2945
2946        #[test]
2947        fn test_style_options_builder_with_layout() {
2948            let mut options = StyleOptions::new();
2949            let layout = PageLayout::new()
2950                .with_margin(40)
2951                .with_text_align(TextAlign::Justify)
2952                .with_paragraph_spacing(24)
2953                .build();
2954            options.with_layout(layout);
2955
2956            assert_eq!(options.layout.margin, 40);
2957            assert_eq!(options.layout.text_align, TextAlign::Justify);
2958            assert_eq!(options.layout.paragraph_spacing, 24);
2959        }
2960
2961        #[test]
2962        fn test_style_options_builder_build() {
2963            let options = StyleOptions::new()
2964                .with_text(TextStyle::new().with_font_size(1.5).build())
2965                .with_color_scheme(ColorScheme::new().with_link("#FF0000").build())
2966                .with_layout(PageLayout::new().with_margin(30).build())
2967                .build();
2968
2969            assert_eq!(options.text.font_size, 1.5);
2970            assert_eq!(options.color_scheme.link, "#FF0000");
2971            assert_eq!(options.layout.margin, 30);
2972        }
2973
2974        #[test]
2975        fn test_style_options_builder_chaining() {
2976            let options = StyleOptions::new()
2977                .with_text(
2978                    TextStyle::new()
2979                        .with_font_size(1.5)
2980                        .with_line_height(2.0)
2981                        .with_font_family("Arial")
2982                        .with_font_weight("bold")
2983                        .with_font_style("italic")
2984                        .with_letter_spacing("0.1em")
2985                        .with_text_indent(1.5)
2986                        .build(),
2987                )
2988                .with_color_scheme(
2989                    ColorScheme::new()
2990                        .with_background("#CCCCCC")
2991                        .with_text("#111111")
2992                        .with_link("#0000FF")
2993                        .build(),
2994                )
2995                .with_layout(
2996                    PageLayout::new()
2997                        .with_margin(25)
2998                        .with_text_align(TextAlign::Right)
2999                        .with_paragraph_spacing(20)
3000                        .build(),
3001                )
3002                .build();
3003
3004            assert_eq!(options.text.font_size, 1.5);
3005            assert_eq!(options.text.line_height, 2.0);
3006            assert_eq!(options.text.font_family, "Arial");
3007            assert_eq!(options.text.font_weight, "bold");
3008            assert_eq!(options.text.font_style, "italic");
3009            assert_eq!(options.text.letter_spacing, "0.1em");
3010            assert_eq!(options.text.text_indent, 1.5);
3011
3012            assert_eq!(options.color_scheme.background, "#CCCCCC");
3013            assert_eq!(options.color_scheme.text, "#111111");
3014            assert_eq!(options.color_scheme.link, "#0000FF");
3015
3016            assert_eq!(options.layout.margin, 25);
3017            assert_eq!(options.layout.text_align, TextAlign::Right);
3018            assert_eq!(options.layout.paragraph_spacing, 20);
3019        }
3020
3021        #[test]
3022        fn test_text_style_builder_new() {
3023            let style = TextStyle::new();
3024            assert_eq!(style.font_size, 1.0);
3025            assert_eq!(style.line_height, 1.6);
3026        }
3027
3028        #[test]
3029        fn test_text_style_builder_with_font_size() {
3030            let mut style = TextStyle::new();
3031            style.with_font_size(2.5);
3032            assert_eq!(style.font_size, 2.5);
3033        }
3034
3035        #[test]
3036        fn test_text_style_builder_with_line_height() {
3037            let mut style = TextStyle::new();
3038            style.with_line_height(2.0);
3039            assert_eq!(style.line_height, 2.0);
3040        }
3041
3042        #[test]
3043        fn test_text_style_builder_with_font_family() {
3044            let mut style = TextStyle::new();
3045            style.with_font_family("Helvetica, Arial");
3046            assert_eq!(style.font_family, "Helvetica, Arial");
3047        }
3048
3049        #[test]
3050        fn test_text_style_builder_with_font_weight() {
3051            let mut style = TextStyle::new();
3052            style.with_font_weight("bold");
3053            assert_eq!(style.font_weight, "bold");
3054        }
3055
3056        #[test]
3057        fn test_text_style_builder_with_font_style() {
3058            let mut style = TextStyle::new();
3059            style.with_font_style("italic");
3060            assert_eq!(style.font_style, "italic");
3061        }
3062
3063        #[test]
3064        fn test_text_style_builder_with_letter_spacing() {
3065            let mut style = TextStyle::new();
3066            style.with_letter_spacing("0.05em");
3067            assert_eq!(style.letter_spacing, "0.05em");
3068        }
3069
3070        #[test]
3071        fn test_text_style_builder_with_text_indent() {
3072            let mut style = TextStyle::new();
3073            style.with_text_indent(3.0);
3074            assert_eq!(style.text_indent, 3.0);
3075        }
3076
3077        #[test]
3078        fn test_text_style_builder_build() {
3079            let style = TextStyle::new()
3080                .with_font_size(1.8)
3081                .with_line_height(1.9)
3082                .build();
3083
3084            assert_eq!(style.font_size, 1.8);
3085            assert_eq!(style.line_height, 1.9);
3086        }
3087
3088        #[test]
3089        fn test_text_style_builder_chaining() {
3090            let style = TextStyle::new()
3091                .with_font_size(2.0)
3092                .with_line_height(1.8)
3093                .with_font_family("Georgia")
3094                .with_font_weight("bold")
3095                .with_font_style("italic")
3096                .with_letter_spacing("0.1em")
3097                .with_text_indent(0.5)
3098                .build();
3099
3100            assert_eq!(style.font_size, 2.0);
3101            assert_eq!(style.line_height, 1.8);
3102            assert_eq!(style.font_family, "Georgia");
3103            assert_eq!(style.font_weight, "bold");
3104            assert_eq!(style.font_style, "italic");
3105            assert_eq!(style.letter_spacing, "0.1em");
3106            assert_eq!(style.text_indent, 0.5);
3107        }
3108
3109        #[test]
3110        fn test_color_scheme_builder_new() {
3111            let scheme = ColorScheme::new();
3112            assert_eq!(scheme.background, "#FFFFFF");
3113            assert_eq!(scheme.text, "#000000");
3114        }
3115
3116        #[test]
3117        fn test_color_scheme_builder_with_background() {
3118            let mut scheme = ColorScheme::new();
3119            scheme.with_background("#FF0000");
3120            assert_eq!(scheme.background, "#FF0000");
3121        }
3122
3123        #[test]
3124        fn test_color_scheme_builder_with_text() {
3125            let mut scheme = ColorScheme::new();
3126            scheme.with_text("#333333");
3127            assert_eq!(scheme.text, "#333333");
3128        }
3129
3130        #[test]
3131        fn test_color_scheme_builder_with_link() {
3132            let mut scheme = ColorScheme::new();
3133            scheme.with_link("#0000FF");
3134            assert_eq!(scheme.link, "#0000FF");
3135        }
3136
3137        #[test]
3138        fn test_color_scheme_builder_build() {
3139            let scheme = ColorScheme::new().with_background("#123456").build();
3140
3141            assert_eq!(scheme.background, "#123456");
3142            assert_eq!(scheme.text, "#000000");
3143        }
3144
3145        #[test]
3146        fn test_color_scheme_builder_chaining() {
3147            let scheme = ColorScheme::new()
3148                .with_background("#AABBCC")
3149                .with_text("#DDEEFF")
3150                .with_link("#112233")
3151                .build();
3152
3153            assert_eq!(scheme.background, "#AABBCC");
3154            assert_eq!(scheme.text, "#DDEEFF");
3155            assert_eq!(scheme.link, "#112233");
3156        }
3157
3158        #[test]
3159        fn test_page_layout_builder_new() {
3160            let layout = PageLayout::new();
3161            assert_eq!(layout.margin, 20);
3162            assert_eq!(layout.text_align, TextAlign::Left);
3163            assert_eq!(layout.paragraph_spacing, 16);
3164        }
3165
3166        #[test]
3167        fn test_page_layout_builder_with_margin() {
3168            let mut layout = PageLayout::new();
3169            layout.with_margin(50);
3170            assert_eq!(layout.margin, 50);
3171        }
3172
3173        #[test]
3174        fn test_page_layout_builder_with_text_align() {
3175            let mut layout = PageLayout::new();
3176            layout.with_text_align(TextAlign::Center);
3177            assert_eq!(layout.text_align, TextAlign::Center);
3178        }
3179
3180        #[test]
3181        fn test_page_layout_builder_with_paragraph_spacing() {
3182            let mut layout = PageLayout::new();
3183            layout.with_paragraph_spacing(30);
3184            assert_eq!(layout.paragraph_spacing, 30);
3185        }
3186
3187        #[test]
3188        fn test_page_layout_builder_build() {
3189            let layout = PageLayout::new().with_margin(35).build();
3190
3191            assert_eq!(layout.margin, 35);
3192            assert_eq!(layout.text_align, TextAlign::Left);
3193        }
3194
3195        #[test]
3196        fn test_page_layout_builder_chaining() {
3197            let layout = PageLayout::new()
3198                .with_margin(45)
3199                .with_text_align(TextAlign::Justify)
3200                .with_paragraph_spacing(28)
3201                .build();
3202
3203            assert_eq!(layout.margin, 45);
3204            assert_eq!(layout.text_align, TextAlign::Justify);
3205            assert_eq!(layout.paragraph_spacing, 28);
3206        }
3207
3208        #[test]
3209        fn test_page_layout_builder_all_text_align_variants() {
3210            let left = PageLayout::new().with_text_align(TextAlign::Left).build();
3211            assert_eq!(left.text_align, TextAlign::Left);
3212
3213            let right = PageLayout::new().with_text_align(TextAlign::Right).build();
3214            assert_eq!(right.text_align, TextAlign::Right);
3215
3216            let center = PageLayout::new().with_text_align(TextAlign::Center).build();
3217            assert_eq!(center.text_align, TextAlign::Center);
3218
3219            let justify = PageLayout::new()
3220                .with_text_align(TextAlign::Justify)
3221                .build();
3222            assert_eq!(justify.text_align, TextAlign::Justify);
3223        }
3224    }
3225}