Skip to main content

lib_epub/
types.rs

1//! Types and data structures for EPUB processing
2//!
3//! This module defines all the core data structures used throughout the EPUB library.
4//! These structures represent the various components of an EPUB publication according to
5//! the EPUB specification, including metadata, manifest items, spine items, navigation points,
6//! and encryption information.
7//!
8//! The types in this module are designed to be compatible with both EPUB 2 and EPUB 3
9//! specifications, providing a unified interface for working with different versions
10//! of EPUB publications.
11//!
12//! ## Builder Pattern
13//!
14//! Many of these types implement a builder pattern for easier construction when the
15//! `builder` feature is enabled. See individual type documentation for details.
16
17use std::path::PathBuf;
18
19#[cfg(feature = "builder")]
20use crate::{
21    error::{EpubBuilderError, EpubError},
22    utils::ELEMENT_IN_DC_NAMESPACE,
23};
24
25/// Represents the EPUB version
26///
27/// This enum is used to distinguish between different versions of the EPUB specification.
28#[derive(Debug, PartialEq, Eq)]
29pub enum EpubVersion {
30    Version2_0,
31    Version3_0,
32}
33
34/// Represents a metadata item in the EPUB publication
35///
36/// The `MetadataItem` structure represents a single piece of metadata from the EPUB publication.
37/// Metadata items contain information about the publication such as title, author, identifier,
38/// language, and other descriptive information.
39///
40/// In EPUB 3.0, metadata items can have refinements that provide additional details about
41/// the main metadata item. For example, a title metadata item might have refinements that
42/// specify it is the main title of the publication.
43///
44/// ## Builder Methods
45///
46/// When the `builder` feature is enabled, this struct provides convenient builder methods:
47///
48/// ```rust
49/// # #[cfg(feature = "builder")] {
50/// use lib_epub::types::MetadataItem;
51///
52/// let metadata = MetadataItem::new("title", "Sample Book")
53///     .with_id("title-1")
54///     .with_lang("en")
55///     .build();
56/// # }
57/// ```
58#[derive(Debug, Clone)]
59pub struct MetadataItem {
60    /// Optional unique identifier for this metadata item
61    ///
62    /// Used to reference this metadata item from other elements or refinements.
63    /// In EPUB 3.0, this ID is particularly important for linking with metadata refinements.
64    pub id: Option<String>,
65
66    /// The metadata property name
67    ///
68    /// This field specifies the type of metadata this item represents. Common properties
69    /// include "title", "creator", "identifier", "language", "publisher", etc.
70    /// These typically correspond to Dublin Core metadata terms.
71    pub property: String,
72
73    /// The metadata value
74    pub value: String,
75
76    /// Optional language code for this metadata item
77    pub lang: Option<String>,
78
79    /// Refinements of this metadata item
80    ///
81    /// In EPUB 3.x, metadata items can have associated refinements that provide additional
82    /// information about the main metadata item. For example, a creator metadata item might
83    /// have refinements specifying the creator's role (author, illustrator, etc.) or file-as.
84    ///
85    /// In EPUB 2.x, metadata items may contain custom attributes, which will also be parsed as refinement.
86    pub refined: Vec<MetadataRefinement>,
87}
88
89#[cfg(feature = "builder")]
90impl MetadataItem {
91    /// Creates a new metadata item with the given property and value
92    ///
93    /// Requires the `builder` feature.
94    ///
95    /// ## Parameters
96    /// - `property` - The metadata property name (e.g., "title", "creator")
97    /// - `value` - The metadata value
98    pub fn new(property: &str, value: &str) -> Self {
99        Self {
100            id: None,
101            property: property.to_string(),
102            value: value.to_string(),
103            lang: None,
104            refined: vec![],
105        }
106    }
107
108    /// Sets the ID of the metadata item
109    ///
110    /// Requires the `builder` feature.
111    ///
112    /// ## Parameters
113    /// - `id` - The ID to assign to this metadata item
114    pub fn with_id(&mut self, id: &str) -> &mut Self {
115        self.id = Some(id.to_string());
116        self
117    }
118
119    /// Sets the language of the metadata item
120    ///
121    /// Requires the `builder` feature.
122    ///
123    /// ## Parameters
124    /// - `lang` - The language code (e.g., "en", "fr", "zh-CN")
125    pub fn with_lang(&mut self, lang: &str) -> &mut Self {
126        self.lang = Some(lang.to_string());
127        self
128    }
129
130    /// Adds a refinement to this metadata item
131    ///
132    /// Requires the `builder` feature.
133    ///
134    /// ## Parameters
135    /// - `refine` - The refinement to add
136    ///
137    /// ## Notes
138    /// - The metadata item must have an ID for refinements to be added.
139    pub fn append_refinement(&mut self, refine: MetadataRefinement) -> &mut Self {
140        if self.id.is_some() {
141            self.refined.push(refine);
142        } else {
143            // TODO: alert warning
144        }
145
146        self
147    }
148
149    /// Builds the final metadata item
150    ///
151    /// Requires the `builder` feature.
152    pub fn build(&self) -> Self {
153        Self { ..self.clone() }
154    }
155
156    /// Gets the XML attributes for this metadata item
157    pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
158        let mut attributes = Vec::new();
159
160        if !ELEMENT_IN_DC_NAMESPACE.contains(&self.property.as_str()) {
161            attributes.push(("property", self.property.as_str()));
162        }
163
164        if let Some(id) = &self.id {
165            attributes.push(("id", id.as_str()));
166        };
167
168        if let Some(lang) = &self.lang {
169            attributes.push(("lang", lang.as_str()));
170        };
171
172        attributes
173    }
174}
175
176/// Represents a refinement of a metadata item in an EPUB 3.0 publication
177///
178/// The `MetadataRefinement` structure provides additional details about a parent metadata item.
179/// Refinements are used in EPUB 3.0 to add granular metadata information that would be difficult
180/// to express with the basic metadata structure alone.
181///
182/// For example, a creator metadata item might have refinements specifying the creator's role
183/// or the scheme used for an identifier.
184///
185/// ## Builder Methods
186///
187/// When the `builder` feature is enabled, this struct provides convenient builder methods:
188///
189/// ```rust
190/// # #[cfg(feature = "builder")] {
191/// use lib_epub::types::MetadataRefinement;
192///
193/// let refinement = MetadataRefinement::new("creator-1", "role", "author")
194///     .with_lang("en")
195///     .with_scheme("marc:relators")
196///     .build();
197/// # }
198/// ```
199#[derive(Debug, Clone)]
200pub struct MetadataRefinement {
201    pub refines: String,
202
203    /// The refinement property name
204    ///
205    /// Specifies what aspect of the parent metadata item this refinement describes.
206    /// Common refinement properties include "role", "file-as", "alternate-script", etc.
207    pub property: String,
208
209    /// The refinement value
210    pub value: String,
211
212    /// Optional language code for this refinement
213    pub lang: Option<String>,
214
215    /// Optional scheme identifier for this refinement
216    ///
217    /// Specifies the vocabulary or scheme used for the refinement value. For example,
218    /// "marc:relators" for MARC relator codes, or "onix:codelist5" for ONIX roles.
219    pub scheme: Option<String>,
220}
221
222#[cfg(feature = "builder")]
223impl MetadataRefinement {
224    /// Creates a new metadata refinement
225    ///
226    /// Requires the `builder` feature.
227    ///
228    /// ## Parameters
229    /// - `refines` - The ID of the metadata item being refined
230    /// - `property` - The refinement property name
231    /// - `value` - The refinement value
232    pub fn new(refines: &str, property: &str, value: &str) -> Self {
233        Self {
234            refines: refines.to_string(),
235            property: property.to_string(),
236            value: value.to_string(),
237            lang: None,
238            scheme: None,
239        }
240    }
241
242    /// Sets the language of the refinement
243    ///
244    /// Requires the `builder` feature.
245    ///
246    /// ## Parameters
247    /// - `lang` - The language code
248    pub fn with_lang(&mut self, lang: &str) -> &mut Self {
249        self.lang = Some(lang.to_string());
250        self
251    }
252
253    /// Sets the scheme of the refinement
254    ///
255    /// Requires the `builder` feature.
256    ///
257    /// ## Parameters
258    /// - `scheme` - The scheme identifier
259    pub fn with_scheme(&mut self, scheme: &str) -> &mut Self {
260        self.scheme = Some(scheme.to_string());
261        self
262    }
263
264    /// Builds the final metadata refinement
265    ///
266    /// Requires the `builder` feature.
267    pub fn build(&self) -> Self {
268        Self { ..self.clone() }
269    }
270
271    /// Gets the XML attributes for this refinement
272    pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
273        let mut attributes = Vec::new();
274
275        attributes.push(("refines", self.refines.as_str()));
276        attributes.push(("property", self.property.as_str()));
277
278        if let Some(lang) = &self.lang {
279            attributes.push(("lang", lang.as_str()));
280        };
281
282        if let Some(scheme) = &self.scheme {
283            attributes.push(("scheme", scheme.as_str()));
284        };
285
286        attributes
287    }
288}
289
290/// Represents a metadata link item in an EPUB publication
291///
292/// The `MetadataLinkItem` structure represents a link from the publication's metadata to
293/// external resources. These links are typically used to associate the publication with
294/// external records, alternate editions, or related resources.
295///
296/// Link metadata items are defined in the OPF file using `<link>` elements in the metadata
297/// section and follow the EPUB 3.0 metadata link specification.
298#[derive(Debug)]
299pub struct MetadataLinkItem {
300    /// The URI of the linked resource
301    pub href: String,
302
303    /// The relationship between this publication and the linked resource
304    pub rel: String,
305
306    /// Optional language of the linked resource
307    pub hreflang: Option<String>,
308
309    /// Optional unique identifier for this link item
310    ///
311    /// Provides an ID that can be used to reference this link from other elements.
312    pub id: Option<String>,
313
314    /// Optional MIME type of the linked resource
315    pub mime: Option<String>,
316
317    /// Optional properties of this link
318    ///
319    /// Contains space-separated property values that describe characteristics of the link
320    /// or the linked resource. For example, "onix-3.0" to indicate an ONIX 3.0 record.
321    pub properties: Option<String>,
322
323    /// Optional reference to another metadata item
324    ///
325    /// In EPUB 3.0, links can refine other metadata items. This field contains the ID
326    /// of the metadata item that this link refines, prefixed with "#".
327    pub refines: Option<String>,
328}
329
330/// Represents a resource item declared in the EPUB manifest
331///
332/// The `ManifestItem` structure represents a single resource file declared in the EPUB
333/// publication's manifest. Each manifest item describes a resource that is part of the
334/// publication, including its location, media type, and optional properties or fallback
335/// relationships.
336///
337/// The manifest serves as a comprehensive inventory of all resources in an EPUB publication.
338/// Every resource that is part of the publication must be declared in the manifest, and
339/// resources not listed in the manifest should not be accessed by reading systems.
340///
341/// Manifest items support the fallback mechanism, allowing alternative versions of a resource
342/// to be specified. This is particularly important for foreign resources (resources with
343/// non-core media types) that may not be supported by all reading systems.
344///
345/// ## Builder Methods
346///
347/// When the `builder` feature is enabled, this struct provides convenient builder methods:
348///
349/// ```
350/// # #[cfg(feature = "builder")] {
351/// use lib_epub::types::ManifestItem;
352///
353/// let manifest_item = ManifestItem::new("cover", "images/cover.jpg")
354///     .unwrap()
355///     .append_property("cover-image")
356///     .with_fallback("cover-fallback")
357///     .build();
358/// # }
359/// ```
360#[derive(Debug, Clone)]
361pub struct ManifestItem {
362    /// The unique identifier for this resource item
363    pub id: String,
364
365    /// The path to the resource file within the EPUB container
366    ///
367    /// This field contains the normalized path to the resource file relative to the
368    /// root of the EPUB container. The path is processed during parsing to handle
369    /// various EPUB path conventions (absolute paths, relative paths, etc.).
370    pub path: PathBuf,
371
372    /// The media type of the resource
373    pub mime: String,
374
375    /// Optional properties associated with this resource
376    ///
377    /// This field contains a space-separated list of properties that apply to this
378    /// resource. Properties provide additional information about how the resource
379    /// should be treated.
380    pub properties: Option<String>,
381
382    /// Optional fallback resource identifier
383    ///
384    /// This field specifies the ID of another manifest item that serves as a fallback
385    /// for this resource. Fallbacks are used when a reading system does not support
386    /// the media type of the primary resource. The fallback chain allows publications
387    /// to include foreign resources while maintaining compatibility with older or
388    /// simpler reading systems.
389    ///
390    /// The value is the ID of another manifest item, which must exist in the manifest.
391    /// If `None`, this resource has no fallback.
392    pub fallback: Option<String>,
393}
394
395#[cfg(feature = "builder")]
396impl ManifestItem {
397    /// Creates a new manifest item
398    ///
399    /// Requires the `builder` feature.
400    ///
401    /// ## Parameters
402    /// - `id` - The unique identifier for this resource
403    /// - `path` - The path to the resource file
404    ///
405    /// ## Errors
406    /// Returns an error if the path starts with "../" which is not allowed.
407    pub fn new(id: &str, path: &str) -> Result<Self, EpubError> {
408        if path.starts_with("../") {
409            return Err(
410                EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() }.into(),
411            );
412        }
413
414        Ok(Self {
415            id: id.to_string(),
416            path: PathBuf::from(path),
417            mime: String::new(),
418            properties: None,
419            fallback: None,
420        })
421    }
422
423    /// Sets the MIME type of the manifest item
424    pub(crate) fn set_mime(self, mime: &str) -> Self {
425        Self {
426            id: self.id,
427            path: self.path,
428            mime: mime.to_string(),
429            properties: self.properties,
430            fallback: self.fallback,
431        }
432    }
433
434    /// Appends a property to the manifest item
435    ///
436    /// Requires the `builder` feature.
437    ///
438    /// ## Parameters
439    /// - `property` - The property to add
440    pub fn append_property(&mut self, property: &str) -> &mut Self {
441        let new_properties = if let Some(properties) = &self.properties {
442            format!("{} {}", properties, property)
443        } else {
444            property.to_string()
445        };
446
447        self.properties = Some(new_properties);
448        self
449    }
450
451    /// Sets the fallback for this manifest item
452    ///
453    /// Requires the `builder` feature.
454    ///
455    /// ## Parameters
456    /// - `fallback` - The ID of the fallback manifest item
457    pub fn with_fallback(&mut self, fallback: &str) -> &mut Self {
458        self.fallback = Some(fallback.to_string());
459        self
460    }
461
462    /// Builds the final manifest item
463    ///
464    /// Requires the `builder` feature.
465    pub fn build(&self) -> Self {
466        Self { ..self.clone() }
467    }
468
469    /// Gets the XML attributes for this manifest item
470    pub fn attributes(&self) -> Vec<(&str, &str)> {
471        let mut attributes = Vec::new();
472
473        attributes.push(("id", self.id.as_str()));
474        attributes.push(("href", self.path.to_str().unwrap()));
475        attributes.push(("media-type", self.mime.as_str()));
476
477        if let Some(properties) = &self.properties {
478            attributes.push(("properties", properties.as_str()));
479        }
480
481        if let Some(fallback) = &self.fallback {
482            attributes.push(("fallback", fallback.as_str()));
483        }
484
485        attributes
486    }
487}
488
489/// Represents an item in the EPUB spine, defining the reading order of the publication
490///
491/// The `SpineItem` structure represents a single item in the EPUB spine, which defines
492/// the linear reading order of the publication's content documents. Each spine item
493/// references a resource declared in the manifest and indicates whether it should be
494/// included in the linear reading sequence.
495///
496/// The spine is a crucial component of an EPUB publication as it determines the recommended
497/// reading order of content documents. Items can be marked as linear (part of the main reading
498/// flow) or non-linear (supplementary content that may be accessed out of sequence).
499///
500/// ## Builder Methods
501///
502/// When the `builder` feature is enabled, this struct provides convenient builder methods:
503///
504/// ```
505/// # #[cfg(feature = "builder")] {
506/// use lib_epub::types::SpineItem;
507///
508/// let spine_item = SpineItem::new("content-1")
509///     .with_id("spine-1")
510///     .append_property("page-spread-right")
511///     .set_linear(false)
512///     .build();
513/// # }
514/// ```
515#[derive(Debug, Clone)]
516pub struct SpineItem {
517    /// The ID reference to a manifest item
518    ///
519    /// This field contains the ID of the manifest item that this spine item references.
520    /// It establishes the connection between the reading order (spine) and the actual
521    /// content resources (manifest). The referenced ID must exist in the manifest.
522    pub idref: String,
523
524    /// Optional identifier for this spine item
525    pub id: Option<String>,
526
527    /// Optional properties associated with this spine item
528    ///
529    /// This field contains a space-separated list of properties that apply to this
530    /// spine item. These properties can indicate special handling requirements,
531    /// layout preferences, or other characteristics.
532    pub properties: Option<String>,
533
534    /// Indicates whether this item is part of the linear reading order
535    ///
536    /// When `true`, this spine item is part of the main linear reading sequence.
537    /// When `false`, this item represents supplementary content that may be accessed
538    /// out of the normal reading order (e.g., through hyperlinks).
539    ///
540    /// Non-linear items are typically used for content like footnotes, endnotes,
541    /// appendices, or other supplementary materials that readers might access
542    /// on-demand rather than sequentially.
543    pub linear: bool,
544}
545
546#[cfg(feature = "builder")]
547impl SpineItem {
548    /// Creates a new spine item referencing a manifest item
549    ///
550    /// Requires the `builder` feature.
551    ///
552    /// By default, spine items are linear.
553    ///
554    /// ## Parameters
555    /// - `idref` - The ID of the manifest item this spine item references
556    pub fn new(idref: &str) -> Self {
557        Self {
558            idref: idref.to_string(),
559            id: None,
560            properties: None,
561            linear: true,
562        }
563    }
564
565    /// Sets the ID of the spine item
566    ///
567    /// Requires the `builder` feature.
568    ///
569    /// ## Parameters
570    /// - `id` - The ID to assign to this spine item
571    pub fn with_id(&mut self, id: &str) -> &mut Self {
572        self.id = Some(id.to_string());
573        self
574    }
575
576    /// Appends a property to the spine item
577    ///
578    /// Requires the `builder` feature.
579    ///
580    /// ## Parameters
581    /// - `property` - The property to add
582    pub fn append_property(&mut self, property: &str) -> &mut Self {
583        let new_properties = if let Some(properties) = &self.properties {
584            format!("{} {}", properties, property)
585        } else {
586            property.to_string()
587        };
588
589        self.properties = Some(new_properties);
590        self
591    }
592
593    /// Sets whether this spine item is part of the linear reading order
594    ///
595    /// Requires the `builder` feature.
596    ///
597    /// ## Parameters
598    /// - `linear` - `true` if the item is part of the linear reading order, `false` otherwise
599    pub fn set_linear(&mut self, linear: bool) -> &mut Self {
600        self.linear = linear;
601        self
602    }
603
604    /// Builds the final spine item
605    ///
606    /// Requires the `builder` feature.
607    pub fn build(&self) -> Self {
608        Self { ..self.clone() }
609    }
610
611    /// Gets the XML attributes for this spine item
612    pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
613        let mut attributes = Vec::new();
614
615        attributes.push(("idref", self.idref.as_str()));
616        attributes.push(("linear", if self.linear { "yes" } else { "no" }));
617
618        if let Some(id) = &self.id {
619            attributes.push(("id", id.as_str()));
620        }
621
622        if let Some(properties) = &self.properties {
623            attributes.push(("properties", properties.as_str()));
624        }
625
626        attributes
627    }
628}
629
630/// Represents encryption information for EPUB resources
631///
632/// This structure holds information about encrypted resources in an EPUB publication,
633/// as defined in the META-INF/encryption.xml file according to the EPUB specification.
634/// It describes which resources are encrypted and what encryption method was used.
635#[derive(Debug, Clone)]
636pub struct EncryptionData {
637    /// The encryption algorithm URI
638    ///
639    /// This field specifies the encryption method used for the resource.
640    /// Supported encryption methods:
641    /// - IDPF font obfuscation: <http://www.idpf.org/2008/embedding>
642    /// - Adobe font obfuscation: <http://ns.adobe.com/pdf/enc#RC>
643    pub method: String,
644
645    /// The URI of the encrypted resource
646    ///
647    /// This field contains the path/URI to the encrypted resource within the EPUB container.
648    /// The path is relative to the root of the EPUB container.
649    pub data: String,
650}
651
652/// Represents a navigation point in an EPUB document's table of contents
653///
654/// The `NavPoint` structure represents a single entry in the hierarchical table of contents
655/// of an EPUB publication. Each navigation point corresponds to a section or chapter in
656/// the publication and may contain nested child navigation points to represent sub-sections.
657///
658/// ## Builder Methods
659///
660/// When the `builder` feature is enabled, this struct provides convenient builder methods:
661///
662/// ```
663/// # #[cfg(feature = "builder")] {
664/// use lib_epub::types::NavPoint;
665///
666/// let nav_point = NavPoint::new("Chapter 1")
667///     .with_content("chapter1.xhtml")
668///     .append_child(
669///         NavPoint::new("Section 1.1")
670///             .with_content("section1_1.xhtml")
671///             .build()
672///     )
673///     .build();
674/// # }
675/// ```
676#[derive(Debug, Eq, Clone)]
677pub struct NavPoint {
678    /// The display label/title of this navigation point
679    ///
680    /// This is the text that should be displayed to users in the table of contents.
681    pub label: String,
682
683    /// The content document path this navigation point references
684    ///
685    /// Can be `None` for navigation points that no relevant information was
686    /// provided in the original data.
687    pub content: Option<PathBuf>,
688
689    /// Child navigation points (sub-sections)
690    pub children: Vec<NavPoint>,
691
692    /// The reading order position of this navigation point
693    ///
694    /// It can be `None` for navigation points that no relevant information was
695    /// provided in the original data.
696    pub play_order: Option<usize>,
697}
698
699#[cfg(feature = "builder")]
700impl NavPoint {
701    /// Creates a new navigation point with the given label
702    ///
703    /// Requires the `builder` feature.
704    ///
705    /// ## Parameters
706    /// - `label` - The display label for this navigation point
707    pub fn new(label: &str) -> Self {
708        Self {
709            label: label.to_string(),
710            content: None,
711            children: vec![],
712            play_order: None,
713        }
714    }
715
716    /// Sets the content path for this navigation point
717    ///
718    /// Requires the `builder` feature.
719    ///
720    /// ## Parameters
721    /// - `content` - The path to the content document
722    pub fn with_content(&mut self, content: &str) -> &mut Self {
723        self.content = Some(PathBuf::from(content));
724        self
725    }
726
727    /// Appends a child navigation point
728    ///
729    /// Requires the `builder` feature.
730    ///
731    /// ## Parameters
732    /// - `child` - The child navigation point to add
733    pub fn append_child(&mut self, child: NavPoint) -> &mut Self {
734        self.children.push(child);
735        self
736    }
737
738    /// Sets all child navigation points
739    ///
740    /// Requires the `builder` feature.
741    ///
742    /// ## Parameters
743    /// - `children` - Vector of child navigation points
744    pub fn set_children(&mut self, children: Vec<NavPoint>) -> &mut Self {
745        self.children = children;
746        self
747    }
748
749    /// Builds the final navigation point
750    ///
751    /// Requires the `builder` feature.
752    pub fn build(&self) -> Self {
753        Self { ..self.clone() }
754    }
755}
756
757impl Ord for NavPoint {
758    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
759        self.play_order.cmp(&other.play_order)
760    }
761}
762
763impl PartialOrd for NavPoint {
764    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
765        Some(self.cmp(other))
766    }
767}
768
769impl PartialEq for NavPoint {
770    fn eq(&self, other: &Self) -> bool {
771        self.play_order == other.play_order
772    }
773}
774
775/// Represents a footnote in an EPUB content document
776///
777/// This structure represents a footnote in an EPUB content document.
778/// It contains the location within the content document and the content of the footnote.
779#[cfg(feature = "content-builder")]
780#[derive(Debug, Clone, Eq, PartialEq)]
781pub struct Footnote {
782    /// The position/location of the footnote reference in the content
783    pub locate: usize,
784
785    /// The text content of the footnote
786    pub content: String,
787}
788
789#[cfg(feature = "content-builder")]
790impl Ord for Footnote {
791    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
792        self.locate.cmp(&other.locate)
793    }
794}
795
796#[cfg(feature = "content-builder")]
797impl PartialOrd for Footnote {
798    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
799        Some(self.cmp(other))
800    }
801}
802
803/// Represents the type of a block element in the content document
804#[cfg(feature = "content-builder")]
805#[derive(Debug, Copy, Clone)]
806pub enum BlockType {
807    /// A text paragraph block
808    ///
809    /// Standard paragraph content with text styling applied.
810    Text,
811
812    /// A quotation block
813    ///
814    /// Represents quoted or indented text content, typically rendered
815    /// with visual distinction from regular paragraphs.
816    Quote,
817
818    /// A title or heading block
819    ///
820    /// Represents chapter or section titles with appropriate heading styling.
821    Title,
822
823    /// An image block
824    ///
825    /// Contains embedded image content with optional caption support.
826    Image,
827
828    /// An audio block
829    ///
830    /// Contains audio content for playback within the document.
831    Audio,
832
833    /// A video block
834    ///
835    /// Contains video content for playback within the document.
836    Video,
837
838    /// A MathML block
839    ///
840    /// Contains mathematical notation using MathML markup for
841    /// proper mathematical typesetting.
842    MathML,
843}
844
845#[cfg(feature = "content-builder")]
846impl std::fmt::Display for BlockType {
847    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
848        match self {
849            BlockType::Text => write!(f, "Text"),
850            BlockType::Quote => write!(f, "Quote"),
851            BlockType::Title => write!(f, "Title"),
852            BlockType::Image => write!(f, "Image"),
853            BlockType::Audio => write!(f, "Audio"),
854            BlockType::Video => write!(f, "Video"),
855            BlockType::MathML => write!(f, "MathML"),
856        }
857    }
858}
859
860/// Configuration options for document styling
861///
862/// This struct aggregates all style-related configuration for an EPUB document,
863/// including text appearance, color scheme, and page layout settings.
864#[cfg(feature = "content-builder")]
865#[derive(Debug, Default, Clone)]
866pub struct StyleOptions {
867    /// Text styling configuration
868    pub text: TextStyle,
869
870    /// Color scheme configuration
871    ///
872    /// Defines the background, text, and link colors for the document.
873    pub color_scheme: ColorScheme,
874
875    /// Page layout configuration
876    ///
877    /// Controls margins, text alignment, and paragraph spacing.
878    pub layout: PageLayout,
879}
880
881#[cfg(feature = "content-builder")]
882#[cfg(feature = "builder")]
883impl StyleOptions {
884    /// Creates a new style options with default values
885    pub fn new() -> Self {
886        Self::default()
887    }
888
889    /// Sets the text style configuration
890    pub fn with_text(&mut self, text: TextStyle) -> &mut Self {
891        self.text = text;
892        self
893    }
894
895    /// Sets the color scheme configuration
896    pub fn with_color_scheme(&mut self, color_scheme: ColorScheme) -> &mut Self {
897        self.color_scheme = color_scheme;
898        self
899    }
900
901    /// Sets the page layout configuration
902    pub fn with_layout(&mut self, layout: PageLayout) -> &mut Self {
903        self.layout = layout;
904        self
905    }
906
907    /// Builds the final style options
908    pub fn build(&self) -> Self {
909        Self { ..self.clone() }
910    }
911}
912
913/// Text styling configuration
914///
915/// Defines the visual appearance of text content in the document,
916/// including font properties, sizing, and spacing.
917#[cfg(feature = "content-builder")]
918#[derive(Debug, Clone)]
919pub struct TextStyle {
920    /// The base font size (default: 1.0, unit: rem)
921    ///
922    /// Relative to the root element, providing consistent sizing
923    /// across different viewing contexts.
924    pub font_size: f32,
925
926    /// The line height (default: 1.6, unit: em)
927    ///
928    /// Controls the vertical spacing between lines of text.
929    /// Values greater than 1.0 increase spacing, while values
930    /// less than 1.0 compress the text.
931    pub line_height: f32,
932
933    /// The font family stack (default: "-apple-system, Roboto, sans-serif")
934    ///
935    /// A comma-separated list of font families to use, with
936    /// fallback fonts specified for compatibility.
937    pub font_family: String,
938
939    /// The font weight (default: "normal")
940    ///
941    /// Controls the thickness of the font strokes. Common values
942    /// include "normal" and "bold".
943    pub font_weight: String,
944
945    /// The font style (default: "normal")
946    ///
947    /// Controls whether the font is normal, italic, or oblique.
948    /// Common values include "normal" and "italic".
949    pub font_style: String,
950
951    /// The letter spacing (default: "normal")
952    ///
953    /// Controls the space between characters. Common values
954    /// include "normal" or specific lengths like "0.05em".
955    pub letter_spacing: String,
956
957    /// The text indent for paragraphs (default: 2.0, unit: em)
958    ///
959    /// Controls the indentation of the first line of paragraphs.
960    /// A value of 2.0 means the first line is indented by 2 ems.
961    pub text_indent: f32,
962}
963
964#[cfg(feature = "content-builder")]
965impl Default for TextStyle {
966    fn default() -> Self {
967        Self {
968            font_size: 1.0,
969            line_height: 1.6,
970            font_family: "-apple-system, Roboto, sans-serif".to_string(),
971            font_weight: "normal".to_string(),
972            font_style: "normal".to_string(),
973            letter_spacing: "normal".to_string(),
974            text_indent: 2.0,
975        }
976    }
977}
978
979#[cfg(feature = "content-builder")]
980impl TextStyle {
981    /// Creates a new text style with default values
982    pub fn new() -> Self {
983        Self::default()
984    }
985
986    /// Sets the font size
987    pub fn with_font_size(&mut self, font_size: f32) -> &mut Self {
988        self.font_size = font_size;
989        self
990    }
991
992    /// Sets the line height
993    pub fn with_line_height(&mut self, line_height: f32) -> &mut Self {
994        self.line_height = line_height;
995        self
996    }
997
998    /// Sets the font family
999    pub fn with_font_family(&mut self, font_family: &str) -> &mut Self {
1000        self.font_family = font_family.to_string();
1001        self
1002    }
1003
1004    /// Sets the font weight
1005    pub fn with_font_weight(&mut self, font_weight: &str) -> &mut Self {
1006        self.font_weight = font_weight.to_string();
1007        self
1008    }
1009
1010    /// Sets the font style
1011    pub fn with_font_style(&mut self, font_style: &str) -> &mut Self {
1012        self.font_style = font_style.to_string();
1013        self
1014    }
1015
1016    /// Sets the letter spacing
1017    pub fn with_letter_spacing(&mut self, letter_spacing: &str) -> &mut Self {
1018        self.letter_spacing = letter_spacing.to_string();
1019        self
1020    }
1021
1022    /// Sets the text indent
1023    pub fn with_text_indent(&mut self, text_indent: f32) -> &mut Self {
1024        self.text_indent = text_indent;
1025        self
1026    }
1027
1028    /// Builds the final text style
1029    pub fn build(&self) -> Self {
1030        Self { ..self.clone() }
1031    }
1032}
1033
1034/// Color scheme configuration
1035///
1036/// Defines the color palette for the document, including background,
1037/// text, and link colors.
1038#[cfg(feature = "content-builder")]
1039#[derive(Debug, Clone)]
1040pub struct ColorScheme {
1041    /// The background color (default: "#FFFFFF")
1042    ///
1043    /// The fill color for the document body. Specified as a hex color
1044    /// string (e.g., "#FFFFFF" for white).
1045    pub background: String,
1046
1047    /// The text color (default: "#000000")
1048    ///
1049    /// The primary color for text content. Specified as a hex color
1050    /// string (e.g., "#000000" for black).
1051    pub text: String,
1052
1053    /// The link color (default: "#6f6f6f")
1054    ///
1055    /// The color for hyperlinks in the document. Specified as a hex
1056    /// color string (e.g., "#6f6f6f" for gray).
1057    pub link: String,
1058}
1059
1060#[cfg(feature = "content-builder")]
1061impl Default for ColorScheme {
1062    fn default() -> Self {
1063        Self {
1064            background: "#FFFFFF".to_string(),
1065            text: "#000000".to_string(),
1066            link: "#6f6f6f".to_string(),
1067        }
1068    }
1069}
1070
1071#[cfg(feature = "content-builder")]
1072impl ColorScheme {
1073    /// Creates a new color scheme with default values
1074    pub fn new() -> Self {
1075        Self::default()
1076    }
1077
1078    /// Sets the background color
1079    pub fn with_background(&mut self, background: &str) -> &mut Self {
1080        self.background = background.to_string();
1081        self
1082    }
1083
1084    /// Sets the text color
1085    pub fn with_text(&mut self, text: &str) -> &mut Self {
1086        self.text = text.to_string();
1087        self
1088    }
1089
1090    /// Sets the link color
1091    pub fn with_link(&mut self, link: &str) -> &mut Self {
1092        self.link = link.to_string();
1093        self
1094    }
1095
1096    /// Builds the final color scheme
1097    pub fn build(&self) -> Self {
1098        Self { ..self.clone() }
1099    }
1100}
1101
1102/// Page layout configuration
1103///
1104/// Defines the layout properties for pages in the document, including
1105/// margins, text alignment, and paragraph spacing.
1106#[cfg(feature = "content-builder")]
1107#[derive(Debug, Clone)]
1108pub struct PageLayout {
1109    /// The page margin (default: 20, unit: pixels)
1110    ///
1111    /// Controls the space around the content area on each page.
1112    pub margin: usize,
1113
1114    /// The text alignment mode (default: TextAlign::Left)
1115    ///
1116    /// Controls how text is aligned within the content area.
1117    pub text_align: TextAlign,
1118
1119    /// The spacing between paragraphs (default: 16, unit: pixels)
1120    ///
1121    /// Controls the vertical space between block-level elements.
1122    pub paragraph_spacing: usize,
1123}
1124
1125#[cfg(feature = "content-builder")]
1126impl Default for PageLayout {
1127    fn default() -> Self {
1128        Self {
1129            margin: 20,
1130            text_align: Default::default(),
1131            paragraph_spacing: 16,
1132        }
1133    }
1134}
1135
1136#[cfg(feature = "content-builder")]
1137impl PageLayout {
1138    /// Creates a new page layout with default values
1139    pub fn new() -> Self {
1140        Self::default()
1141    }
1142
1143    /// Sets the page margin
1144    pub fn with_margin(&mut self, margin: usize) -> &mut Self {
1145        self.margin = margin;
1146        self
1147    }
1148
1149    /// Sets the text alignment
1150    pub fn with_text_align(&mut self, text_align: TextAlign) -> &mut Self {
1151        self.text_align = text_align;
1152        self
1153    }
1154
1155    /// Sets the paragraph spacing
1156    pub fn with_paragraph_spacing(&mut self, paragraph_spacing: usize) -> &mut Self {
1157        self.paragraph_spacing = paragraph_spacing;
1158        self
1159    }
1160
1161    /// Builds the final page layout
1162    pub fn build(&self) -> Self {
1163        Self { ..self.clone() }
1164    }
1165}
1166
1167/// Text alignment options
1168///
1169/// Defines the available text alignment modes for content in the document.
1170#[cfg(feature = "content-builder")]
1171#[derive(Debug, Default, Clone, Copy, PartialEq)]
1172pub enum TextAlign {
1173    /// Left-aligned text
1174    ///
1175    /// Text is aligned to the left margin, with the right edge ragged.
1176    #[default]
1177    Left,
1178
1179    /// Right-aligned text
1180    ///
1181    /// Text is aligned to the right margin, with the left edge ragged.
1182    Right,
1183
1184    /// Justified text
1185    ///
1186    /// Text is aligned to both margins by adjusting the spacing between
1187    /// words. The left and right edges are both straight.
1188    Justify,
1189
1190    /// Centered text
1191    ///
1192    /// Text is centered within the content area, with both edges ragged.
1193    Center,
1194}
1195
1196#[cfg(feature = "content-builder")]
1197impl std::fmt::Display for TextAlign {
1198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1199        match self {
1200            TextAlign::Left => write!(f, "left"),
1201            TextAlign::Right => write!(f, "right"),
1202            TextAlign::Justify => write!(f, "justify"),
1203            TextAlign::Center => write!(f, "center"),
1204        }
1205    }
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210    mod navpoint_tests {
1211        use std::path::PathBuf;
1212
1213        use crate::types::NavPoint;
1214
1215        /// Testing the equality comparison of NavPoint
1216        #[test]
1217        fn test_navpoint_partial_eq() {
1218            let nav1 = NavPoint {
1219                label: "Chapter 1".to_string(),
1220                content: Some(PathBuf::from("chapter1.html")),
1221                children: vec![],
1222                play_order: Some(1),
1223            };
1224
1225            let nav2 = NavPoint {
1226                label: "Chapter 1".to_string(),
1227                content: Some(PathBuf::from("chapter2.html")),
1228                children: vec![],
1229                play_order: Some(1),
1230            };
1231
1232            let nav3 = NavPoint {
1233                label: "Chapter 2".to_string(),
1234                content: Some(PathBuf::from("chapter1.html")),
1235                children: vec![],
1236                play_order: Some(2),
1237            };
1238
1239            assert_eq!(nav1, nav2); // Same play_order, different contents, should be equal
1240            assert_ne!(nav1, nav3); // Different play_order, Same contents, should be unequal
1241        }
1242
1243        /// Test NavPoint sorting comparison
1244        #[test]
1245        fn test_navpoint_ord() {
1246            let nav1 = NavPoint {
1247                label: "Chapter 1".to_string(),
1248                content: Some(PathBuf::from("chapter1.html")),
1249                children: vec![],
1250                play_order: Some(1),
1251            };
1252
1253            let nav2 = NavPoint {
1254                label: "Chapter 2".to_string(),
1255                content: Some(PathBuf::from("chapter2.html")),
1256                children: vec![],
1257                play_order: Some(2),
1258            };
1259
1260            let nav3 = NavPoint {
1261                label: "Chapter 3".to_string(),
1262                content: Some(PathBuf::from("chapter3.html")),
1263                children: vec![],
1264                play_order: Some(3),
1265            };
1266
1267            // Test function cmp
1268            assert!(nav1 < nav2);
1269            assert!(nav2 > nav1);
1270            assert!(nav1 == nav1);
1271
1272            // Test function partial_cmp
1273            assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
1274            assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
1275            assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
1276
1277            // Test function sort
1278            let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
1279            nav_points.sort();
1280            assert_eq!(nav_points, vec![nav1, nav2, nav3]);
1281        }
1282
1283        /// Test the case of None play_order
1284        #[test]
1285        fn test_navpoint_ord_with_none_play_order() {
1286            let nav_with_order = NavPoint {
1287                label: "Chapter 1".to_string(),
1288                content: Some(PathBuf::from("chapter1.html")),
1289                children: vec![],
1290                play_order: Some(1),
1291            };
1292
1293            let nav_without_order = NavPoint {
1294                label: "Preface".to_string(),
1295                content: Some(PathBuf::from("preface.html")),
1296                children: vec![],
1297                play_order: None,
1298            };
1299
1300            assert!(nav_without_order < nav_with_order);
1301            assert!(nav_with_order > nav_without_order);
1302
1303            let nav_without_order2 = NavPoint {
1304                label: "Introduction".to_string(),
1305                content: Some(PathBuf::from("intro.html")),
1306                children: vec![],
1307                play_order: None,
1308            };
1309
1310            assert!(nav_without_order == nav_without_order2);
1311        }
1312
1313        /// Test NavPoint containing child nodes
1314        #[test]
1315        fn test_navpoint_with_children() {
1316            let child1 = NavPoint {
1317                label: "Section 1.1".to_string(),
1318                content: Some(PathBuf::from("section1_1.html")),
1319                children: vec![],
1320                play_order: Some(1),
1321            };
1322
1323            let child2 = NavPoint {
1324                label: "Section 1.2".to_string(),
1325                content: Some(PathBuf::from("section1_2.html")),
1326                children: vec![],
1327                play_order: Some(2),
1328            };
1329
1330            let parent1 = NavPoint {
1331                label: "Chapter 1".to_string(),
1332                content: Some(PathBuf::from("chapter1.html")),
1333                children: vec![child1.clone(), child2.clone()],
1334                play_order: Some(1),
1335            };
1336
1337            let parent2 = NavPoint {
1338                label: "Chapter 1".to_string(),
1339                content: Some(PathBuf::from("chapter1.html")),
1340                children: vec![child1.clone(), child2.clone()],
1341                play_order: Some(1),
1342            };
1343
1344            assert!(parent1 == parent2);
1345
1346            let parent3 = NavPoint {
1347                label: "Chapter 2".to_string(),
1348                content: Some(PathBuf::from("chapter2.html")),
1349                children: vec![child1.clone(), child2.clone()],
1350                play_order: Some(2),
1351            };
1352
1353            assert!(parent1 != parent3);
1354            assert!(parent1 < parent3);
1355        }
1356
1357        /// Test the case where content is None
1358        #[test]
1359        fn test_navpoint_with_none_content() {
1360            let nav1 = NavPoint {
1361                label: "Chapter 1".to_string(),
1362                content: None,
1363                children: vec![],
1364                play_order: Some(1),
1365            };
1366
1367            let nav2 = NavPoint {
1368                label: "Chapter 1".to_string(),
1369                content: None,
1370                children: vec![],
1371                play_order: Some(1),
1372            };
1373
1374            assert!(nav1 == nav2);
1375        }
1376    }
1377
1378    #[cfg(feature = "builder")]
1379    mod builder_tests {
1380        mod metadata_item {
1381            use crate::types::{MetadataItem, MetadataRefinement};
1382
1383            #[test]
1384            fn test_metadata_item_new() {
1385                let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1386
1387                assert_eq!(metadata_item.property, "title");
1388                assert_eq!(metadata_item.value, "EPUB Test Book");
1389                assert_eq!(metadata_item.id, None);
1390                assert_eq!(metadata_item.lang, None);
1391                assert_eq!(metadata_item.refined.len(), 0);
1392            }
1393
1394            #[test]
1395            fn test_metadata_item_with_id() {
1396                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1397                metadata_item.with_id("creator-1");
1398
1399                assert_eq!(metadata_item.property, "creator");
1400                assert_eq!(metadata_item.value, "John Doe");
1401                assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1402                assert_eq!(metadata_item.lang, None);
1403                assert_eq!(metadata_item.refined.len(), 0);
1404            }
1405
1406            #[test]
1407            fn test_metadata_item_with_lang() {
1408                let mut metadata_item = MetadataItem::new("title", "测试书籍");
1409                metadata_item.with_lang("zh-CN");
1410
1411                assert_eq!(metadata_item.property, "title");
1412                assert_eq!(metadata_item.value, "测试书籍");
1413                assert_eq!(metadata_item.id, None);
1414                assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1415                assert_eq!(metadata_item.refined.len(), 0);
1416            }
1417
1418            #[test]
1419            fn test_metadata_item_append_refinement() {
1420                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1421                metadata_item.with_id("creator-1"); // ID is required for refinements
1422
1423                let refinement = MetadataRefinement::new("creator-1", "role", "author");
1424                metadata_item.append_refinement(refinement);
1425
1426                assert_eq!(metadata_item.refined.len(), 1);
1427                assert_eq!(metadata_item.refined[0].refines, "creator-1");
1428                assert_eq!(metadata_item.refined[0].property, "role");
1429                assert_eq!(metadata_item.refined[0].value, "author");
1430            }
1431
1432            #[test]
1433            fn test_metadata_item_append_refinement_without_id() {
1434                let mut metadata_item = MetadataItem::new("title", "Test Book");
1435                // No ID set
1436
1437                let refinement = MetadataRefinement::new("title", "title-type", "main");
1438                metadata_item.append_refinement(refinement);
1439
1440                // Refinement should not be added because metadata item has no ID
1441                assert_eq!(metadata_item.refined.len(), 0);
1442            }
1443
1444            #[test]
1445            fn test_metadata_item_build() {
1446                let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1447                metadata_item.with_id("pub-id").with_lang("en");
1448
1449                let built = metadata_item.build();
1450
1451                assert_eq!(built.property, "identifier");
1452                assert_eq!(built.value, "urn:isbn:1234567890");
1453                assert_eq!(built.id, Some("pub-id".to_string()));
1454                assert_eq!(built.lang, Some("en".to_string()));
1455                assert_eq!(built.refined.len(), 0);
1456            }
1457
1458            #[test]
1459            fn test_metadata_item_builder_chaining() {
1460                let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1461                metadata_item.with_id("title").with_lang("en");
1462
1463                let refinement = MetadataRefinement::new("title", "title-type", "main");
1464                metadata_item.append_refinement(refinement);
1465
1466                let built = metadata_item.build();
1467
1468                assert_eq!(built.property, "title");
1469                assert_eq!(built.value, "EPUB 3.3 Guide");
1470                assert_eq!(built.id, Some("title".to_string()));
1471                assert_eq!(built.lang, Some("en".to_string()));
1472                assert_eq!(built.refined.len(), 1);
1473            }
1474
1475            #[test]
1476            fn test_metadata_item_attributes_dc_namespace() {
1477                let mut metadata_item = MetadataItem::new("title", "Test Book");
1478                metadata_item.with_id("title-id");
1479
1480                let attributes = metadata_item.attributes();
1481
1482                // For DC namespace properties, no "property" attribute should be added
1483                assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1484                assert!(
1485                    attributes
1486                        .iter()
1487                        .any(|(k, v)| k == &"id" && v == &"title-id")
1488                );
1489            }
1490
1491            #[test]
1492            fn test_metadata_item_attributes_non_dc_namespace() {
1493                let mut metadata_item = MetadataItem::new("meta", "value");
1494                metadata_item.with_id("meta-id");
1495
1496                let attributes = metadata_item.attributes();
1497
1498                // For non-DC namespace properties, "property" attribute should be added
1499                assert!(attributes.iter().any(|(k, _)| k == &"property"));
1500                assert!(
1501                    attributes
1502                        .iter()
1503                        .any(|(k, v)| k == &"id" && v == &"meta-id")
1504                );
1505            }
1506
1507            #[test]
1508            fn test_metadata_item_attributes_with_lang() {
1509                let mut metadata_item = MetadataItem::new("title", "Test Book");
1510                metadata_item.with_id("title-id").with_lang("en");
1511
1512                let attributes = metadata_item.attributes();
1513
1514                assert!(
1515                    attributes
1516                        .iter()
1517                        .any(|(k, v)| k == &"id" && v == &"title-id")
1518                );
1519                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1520            }
1521        }
1522
1523        mod metadata_refinement {
1524            use crate::types::MetadataRefinement;
1525
1526            #[test]
1527            fn test_metadata_refinement_new() {
1528                let refinement = MetadataRefinement::new("title", "title-type", "main");
1529
1530                assert_eq!(refinement.refines, "title");
1531                assert_eq!(refinement.property, "title-type");
1532                assert_eq!(refinement.value, "main");
1533                assert_eq!(refinement.lang, None);
1534                assert_eq!(refinement.scheme, None);
1535            }
1536
1537            #[test]
1538            fn test_metadata_refinement_with_lang() {
1539                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1540                refinement.with_lang("en");
1541
1542                assert_eq!(refinement.refines, "creator");
1543                assert_eq!(refinement.property, "role");
1544                assert_eq!(refinement.value, "author");
1545                assert_eq!(refinement.lang, Some("en".to_string()));
1546                assert_eq!(refinement.scheme, None);
1547            }
1548
1549            #[test]
1550            fn test_metadata_refinement_with_scheme() {
1551                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1552                refinement.with_scheme("marc:relators");
1553
1554                assert_eq!(refinement.refines, "creator");
1555                assert_eq!(refinement.property, "role");
1556                assert_eq!(refinement.value, "author");
1557                assert_eq!(refinement.lang, None);
1558                assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1559            }
1560
1561            #[test]
1562            fn test_metadata_refinement_build() {
1563                let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1564                refinement.with_lang("ja").with_scheme("iso-15924");
1565
1566                let built = refinement.build();
1567
1568                assert_eq!(built.refines, "title");
1569                assert_eq!(built.property, "alternate-script");
1570                assert_eq!(built.value, "テスト");
1571                assert_eq!(built.lang, Some("ja".to_string()));
1572                assert_eq!(built.scheme, Some("iso-15924".to_string()));
1573            }
1574
1575            #[test]
1576            fn test_metadata_refinement_builder_chaining() {
1577                let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1578                refinement.with_lang("en").with_scheme("dcterms");
1579
1580                let built = refinement.build();
1581
1582                assert_eq!(built.refines, "creator");
1583                assert_eq!(built.property, "file-as");
1584                assert_eq!(built.value, "Doe, John");
1585                assert_eq!(built.lang, Some("en".to_string()));
1586                assert_eq!(built.scheme, Some("dcterms".to_string()));
1587            }
1588
1589            #[test]
1590            fn test_metadata_refinement_attributes() {
1591                let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1592                refinement.with_lang("en").with_scheme("onix:codelist5");
1593
1594                let attributes = refinement.attributes();
1595
1596                assert!(
1597                    attributes
1598                        .iter()
1599                        .any(|(k, v)| k == &"refines" && v == &"title")
1600                );
1601                assert!(
1602                    attributes
1603                        .iter()
1604                        .any(|(k, v)| k == &"property" && v == &"title-type")
1605                );
1606                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1607                assert!(
1608                    attributes
1609                        .iter()
1610                        .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1611                );
1612            }
1613
1614            #[test]
1615            fn test_metadata_refinement_attributes_optional_fields() {
1616                let refinement = MetadataRefinement::new("creator", "role", "author");
1617                let attributes = refinement.attributes();
1618
1619                assert!(
1620                    attributes
1621                        .iter()
1622                        .any(|(k, v)| k == &"refines" && v == &"creator")
1623                );
1624                assert!(
1625                    attributes
1626                        .iter()
1627                        .any(|(k, v)| k == &"property" && v == &"role")
1628                );
1629
1630                // Should not contain optional attributes when they are None
1631                assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1632                assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1633            }
1634        }
1635
1636        mod manifest_item {
1637            use std::path::PathBuf;
1638
1639            use crate::types::ManifestItem;
1640
1641            #[test]
1642            fn test_manifest_item_new() {
1643                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1644                assert!(manifest_item.is_ok());
1645
1646                let manifest_item = manifest_item.unwrap();
1647                assert_eq!(manifest_item.id, "cover");
1648                assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1649                assert_eq!(manifest_item.mime, "");
1650                assert_eq!(manifest_item.properties, None);
1651                assert_eq!(manifest_item.fallback, None);
1652            }
1653
1654            #[test]
1655            fn test_manifest_item_append_property() {
1656                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1657                assert!(manifest_item.is_ok());
1658
1659                let mut manifest_item = manifest_item.unwrap();
1660                manifest_item.append_property("nav");
1661
1662                assert_eq!(manifest_item.id, "nav");
1663                assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1664                assert_eq!(manifest_item.mime, "");
1665                assert_eq!(manifest_item.properties, Some("nav".to_string()));
1666                assert_eq!(manifest_item.fallback, None);
1667            }
1668
1669            #[test]
1670            fn test_manifest_item_append_multiple_properties() {
1671                let manifest_item = ManifestItem::new("content", "content.xhtml");
1672                assert!(manifest_item.is_ok());
1673
1674                let mut manifest_item = manifest_item.unwrap();
1675                manifest_item
1676                    .append_property("nav")
1677                    .append_property("scripted")
1678                    .append_property("svg");
1679
1680                assert_eq!(
1681                    manifest_item.properties,
1682                    Some("nav scripted svg".to_string())
1683                );
1684            }
1685
1686            #[test]
1687            fn test_manifest_item_with_fallback() {
1688                let manifest_item = ManifestItem::new("image", "image.tiff");
1689                assert!(manifest_item.is_ok());
1690
1691                let mut manifest_item = manifest_item.unwrap();
1692                manifest_item.with_fallback("image-fallback");
1693
1694                assert_eq!(manifest_item.id, "image");
1695                assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1696                assert_eq!(manifest_item.mime, "");
1697                assert_eq!(manifest_item.properties, None);
1698                assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1699            }
1700
1701            #[test]
1702            fn test_manifest_item_set_mime() {
1703                let manifest_item = ManifestItem::new("style", "style.css");
1704                assert!(manifest_item.is_ok());
1705
1706                let manifest_item = manifest_item.unwrap();
1707                let updated_item = manifest_item.set_mime("text/css");
1708
1709                assert_eq!(updated_item.id, "style");
1710                assert_eq!(updated_item.path, PathBuf::from("style.css"));
1711                assert_eq!(updated_item.mime, "text/css");
1712                assert_eq!(updated_item.properties, None);
1713                assert_eq!(updated_item.fallback, None);
1714            }
1715
1716            #[test]
1717            fn test_manifest_item_build() {
1718                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1719                assert!(manifest_item.is_ok());
1720
1721                let mut manifest_item = manifest_item.unwrap();
1722                manifest_item
1723                    .append_property("cover-image")
1724                    .with_fallback("cover-fallback");
1725
1726                let built = manifest_item.build();
1727
1728                assert_eq!(built.id, "cover");
1729                assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
1730                assert_eq!(built.mime, "");
1731                assert_eq!(built.properties, Some("cover-image".to_string()));
1732                assert_eq!(built.fallback, Some("cover-fallback".to_string()));
1733            }
1734
1735            #[test]
1736            fn test_manifest_item_builder_chaining() {
1737                let manifest_item = ManifestItem::new("content", "content.xhtml");
1738                assert!(manifest_item.is_ok());
1739
1740                let mut manifest_item = manifest_item.unwrap();
1741                manifest_item
1742                    .append_property("scripted")
1743                    .append_property("svg")
1744                    .with_fallback("fallback-content");
1745
1746                let built = manifest_item.build();
1747
1748                assert_eq!(built.id, "content");
1749                assert_eq!(built.path, PathBuf::from("content.xhtml"));
1750                assert_eq!(built.mime, "");
1751                assert_eq!(built.properties, Some("scripted svg".to_string()));
1752                assert_eq!(built.fallback, Some("fallback-content".to_string()));
1753            }
1754
1755            #[test]
1756            fn test_manifest_item_attributes() {
1757                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1758                assert!(manifest_item.is_ok());
1759
1760                let mut manifest_item = manifest_item.unwrap();
1761                manifest_item
1762                    .append_property("nav")
1763                    .with_fallback("fallback-nav");
1764
1765                // Manually set mime type for testing
1766                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1767                let attributes = manifest_item.attributes();
1768
1769                // Check that all expected attributes are present
1770                assert!(attributes.contains(&("id", "nav")));
1771                assert!(attributes.contains(&("href", "nav.xhtml")));
1772                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1773                assert!(attributes.contains(&("properties", "nav")));
1774                assert!(attributes.contains(&("fallback", "fallback-nav")));
1775            }
1776
1777            #[test]
1778            fn test_manifest_item_attributes_optional_fields() {
1779                let manifest_item = ManifestItem::new("simple", "simple.xhtml");
1780                assert!(manifest_item.is_ok());
1781
1782                let manifest_item = manifest_item.unwrap();
1783                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1784                let attributes = manifest_item.attributes();
1785
1786                // Should contain required attributes
1787                assert!(attributes.contains(&("id", "simple")));
1788                assert!(attributes.contains(&("href", "simple.xhtml")));
1789                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1790
1791                // Should not contain optional attributes when they are None
1792                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1793                assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
1794            }
1795
1796            #[test]
1797            fn test_manifest_item_path_handling() {
1798                let manifest_item = ManifestItem::new("test", "../images/test.png");
1799                assert!(manifest_item.is_err());
1800
1801                let err = manifest_item.unwrap_err();
1802                assert_eq!(
1803                    err.to_string(),
1804                    "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
1805                );
1806            }
1807        }
1808
1809        mod spine_item {
1810            use crate::types::SpineItem;
1811
1812            #[test]
1813            fn test_spine_item_new() {
1814                let spine_item = SpineItem::new("content_001");
1815
1816                assert_eq!(spine_item.idref, "content_001");
1817                assert_eq!(spine_item.id, None);
1818                assert_eq!(spine_item.properties, None);
1819                assert_eq!(spine_item.linear, true);
1820            }
1821
1822            #[test]
1823            fn test_spine_item_with_id() {
1824                let mut spine_item = SpineItem::new("content_001");
1825                spine_item.with_id("spine1");
1826
1827                assert_eq!(spine_item.idref, "content_001");
1828                assert_eq!(spine_item.id, Some("spine1".to_string()));
1829                assert_eq!(spine_item.properties, None);
1830                assert_eq!(spine_item.linear, true);
1831            }
1832
1833            #[test]
1834            fn test_spine_item_append_property() {
1835                let mut spine_item = SpineItem::new("content_001");
1836                spine_item.append_property("page-spread-left");
1837
1838                assert_eq!(spine_item.idref, "content_001");
1839                assert_eq!(spine_item.id, None);
1840                assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
1841                assert_eq!(spine_item.linear, true);
1842            }
1843
1844            #[test]
1845            fn test_spine_item_append_multiple_properties() {
1846                let mut spine_item = SpineItem::new("content_001");
1847                spine_item
1848                    .append_property("page-spread-left")
1849                    .append_property("rendition:layout-pre-paginated");
1850
1851                assert_eq!(
1852                    spine_item.properties,
1853                    Some("page-spread-left rendition:layout-pre-paginated".to_string())
1854                );
1855            }
1856
1857            #[test]
1858            fn test_spine_item_set_linear() {
1859                let mut spine_item = SpineItem::new("content_001");
1860                spine_item.set_linear(false);
1861
1862                assert_eq!(spine_item.idref, "content_001");
1863                assert_eq!(spine_item.id, None);
1864                assert_eq!(spine_item.properties, None);
1865                assert_eq!(spine_item.linear, false);
1866            }
1867
1868            #[test]
1869            fn test_spine_item_build() {
1870                let mut spine_item = SpineItem::new("content_001");
1871                spine_item
1872                    .with_id("spine1")
1873                    .append_property("page-spread-left")
1874                    .set_linear(false);
1875
1876                let built = spine_item.build();
1877
1878                assert_eq!(built.idref, "content_001");
1879                assert_eq!(built.id, Some("spine1".to_string()));
1880                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1881                assert_eq!(built.linear, false);
1882            }
1883
1884            #[test]
1885            fn test_spine_item_builder_chaining() {
1886                let mut spine_item = SpineItem::new("content_001");
1887                spine_item
1888                    .with_id("spine1")
1889                    .append_property("page-spread-left")
1890                    .set_linear(false);
1891
1892                let built = spine_item.build();
1893
1894                assert_eq!(built.idref, "content_001");
1895                assert_eq!(built.id, Some("spine1".to_string()));
1896                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1897                assert_eq!(built.linear, false);
1898            }
1899
1900            #[test]
1901            fn test_spine_item_attributes() {
1902                let mut spine_item = SpineItem::new("content_001");
1903                spine_item
1904                    .with_id("spine1")
1905                    .append_property("page-spread-left")
1906                    .set_linear(false);
1907
1908                let attributes = spine_item.attributes();
1909
1910                // Check that all expected attributes are present
1911                assert!(attributes.contains(&("idref", "content_001")));
1912                assert!(attributes.contains(&("id", "spine1")));
1913                assert!(attributes.contains(&("properties", "page-spread-left")));
1914                assert!(attributes.contains(&("linear", "no"))); // false should become "no"
1915            }
1916
1917            #[test]
1918            fn test_spine_item_attributes_linear_yes() {
1919                let spine_item = SpineItem::new("content_001");
1920                let attributes = spine_item.attributes();
1921
1922                // Linear true should become "yes"
1923                assert!(attributes.contains(&("linear", "yes")));
1924            }
1925
1926            #[test]
1927            fn test_spine_item_attributes_optional_fields() {
1928                let spine_item = SpineItem::new("content_001");
1929                let attributes = spine_item.attributes();
1930
1931                // Should only contain required attributes when optional fields are None
1932                assert!(attributes.contains(&("idref", "content_001")));
1933                assert!(attributes.contains(&("linear", "yes")));
1934
1935                // Should not contain optional attributes when they are None
1936                assert!(!attributes.iter().any(|(k, _)| k == &"id"));
1937                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1938            }
1939        }
1940
1941        mod navpoint {
1942
1943            use std::path::PathBuf;
1944
1945            use crate::types::NavPoint;
1946
1947            #[test]
1948            fn test_navpoint_new() {
1949                let navpoint = NavPoint::new("Test Chapter");
1950
1951                assert_eq!(navpoint.label, "Test Chapter");
1952                assert_eq!(navpoint.content, None);
1953                assert_eq!(navpoint.children.len(), 0);
1954            }
1955
1956            #[test]
1957            fn test_navpoint_with_content() {
1958                let mut navpoint = NavPoint::new("Test Chapter");
1959                navpoint.with_content("chapter1.html");
1960
1961                assert_eq!(navpoint.label, "Test Chapter");
1962                assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
1963                assert_eq!(navpoint.children.len(), 0);
1964            }
1965
1966            #[test]
1967            fn test_navpoint_append_child() {
1968                let mut parent = NavPoint::new("Parent Chapter");
1969
1970                let mut child1 = NavPoint::new("Child Section 1");
1971                child1.with_content("section1.html");
1972
1973                let mut child2 = NavPoint::new("Child Section 2");
1974                child2.with_content("section2.html");
1975
1976                parent.append_child(child1.build());
1977                parent.append_child(child2.build());
1978
1979                assert_eq!(parent.children.len(), 2);
1980                assert_eq!(parent.children[0].label, "Child Section 1");
1981                assert_eq!(parent.children[1].label, "Child Section 2");
1982            }
1983
1984            #[test]
1985            fn test_navpoint_set_children() {
1986                let mut navpoint = NavPoint::new("Main Chapter");
1987                let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
1988
1989                navpoint.set_children(children);
1990
1991                assert_eq!(navpoint.children.len(), 2);
1992                assert_eq!(navpoint.children[0].label, "Section 1");
1993                assert_eq!(navpoint.children[1].label, "Section 2");
1994            }
1995
1996            #[test]
1997            fn test_navpoint_build() {
1998                let mut navpoint = NavPoint::new("Complete Chapter");
1999                navpoint.with_content("complete.html");
2000
2001                let child = NavPoint::new("Sub Section");
2002                navpoint.append_child(child.build());
2003
2004                let built = navpoint.build();
2005
2006                assert_eq!(built.label, "Complete Chapter");
2007                assert_eq!(built.content, Some(PathBuf::from("complete.html")));
2008                assert_eq!(built.children.len(), 1);
2009                assert_eq!(built.children[0].label, "Sub Section");
2010            }
2011
2012            #[test]
2013            fn test_navpoint_builder_chaining() {
2014                let mut navpoint = NavPoint::new("Chained Chapter");
2015
2016                navpoint
2017                    .with_content("chained.html")
2018                    .append_child(NavPoint::new("Child 1").build())
2019                    .append_child(NavPoint::new("Child 2").build());
2020
2021                let built = navpoint.build();
2022
2023                assert_eq!(built.label, "Chained Chapter");
2024                assert_eq!(built.content, Some(PathBuf::from("chained.html")));
2025                assert_eq!(built.children.len(), 2);
2026            }
2027
2028            #[test]
2029            fn test_navpoint_empty_children() {
2030                let navpoint = NavPoint::new("No Children Chapter");
2031                let built = navpoint.build();
2032
2033                assert_eq!(built.children.len(), 0);
2034            }
2035
2036            #[test]
2037            fn test_navpoint_complex_hierarchy() {
2038                let mut root = NavPoint::new("Book");
2039
2040                let mut chapter1 = NavPoint::new("Chapter 1");
2041                chapter1
2042                    .with_content("chapter1.html")
2043                    .append_child(
2044                        NavPoint::new("Section 1.1")
2045                            .with_content("sec1_1.html")
2046                            .build(),
2047                    )
2048                    .append_child(
2049                        NavPoint::new("Section 1.2")
2050                            .with_content("sec1_2.html")
2051                            .build(),
2052                    );
2053
2054                let mut chapter2 = NavPoint::new("Chapter 2");
2055                chapter2.with_content("chapter2.html").append_child(
2056                    NavPoint::new("Section 2.1")
2057                        .with_content("sec2_1.html")
2058                        .build(),
2059                );
2060
2061                root.append_child(chapter1.build())
2062                    .append_child(chapter2.build());
2063
2064                let book = root.build();
2065
2066                assert_eq!(book.label, "Book");
2067                assert_eq!(book.children.len(), 2);
2068
2069                let ch1 = &book.children[0];
2070                assert_eq!(ch1.label, "Chapter 1");
2071                assert_eq!(ch1.children.len(), 2);
2072
2073                let ch2 = &book.children[1];
2074                assert_eq!(ch2.label, "Chapter 2");
2075                assert_eq!(ch2.children.len(), 1);
2076            }
2077        }
2078    }
2079
2080    #[cfg(feature = "content-builder")]
2081    mod footnote_tests {
2082        use crate::types::Footnote;
2083
2084        #[test]
2085        fn test_footnote_basic_creation() {
2086            let footnote = Footnote {
2087                locate: 100,
2088                content: "Sample footnote".to_string(),
2089            };
2090
2091            assert_eq!(footnote.locate, 100);
2092            assert_eq!(footnote.content, "Sample footnote");
2093        }
2094
2095        #[test]
2096        fn test_footnote_equality() {
2097            let footnote1 = Footnote {
2098                locate: 100,
2099                content: "First note".to_string(),
2100            };
2101
2102            let footnote2 = Footnote {
2103                locate: 100,
2104                content: "First note".to_string(),
2105            };
2106
2107            let footnote3 = Footnote {
2108                locate: 100,
2109                content: "Different note".to_string(),
2110            };
2111
2112            let footnote4 = Footnote {
2113                locate: 200,
2114                content: "First note".to_string(),
2115            };
2116
2117            assert_eq!(footnote1, footnote2);
2118            assert_ne!(footnote1, footnote3);
2119            assert_ne!(footnote1, footnote4);
2120        }
2121
2122        #[test]
2123        fn test_footnote_ordering() {
2124            let footnote1 = Footnote {
2125                locate: 100,
2126                content: "First".to_string(),
2127            };
2128
2129            let footnote2 = Footnote {
2130                locate: 200,
2131                content: "Second".to_string(),
2132            };
2133
2134            let footnote3 = Footnote {
2135                locate: 150,
2136                content: "Middle".to_string(),
2137            };
2138
2139            assert!(footnote1 < footnote2);
2140            assert!(footnote2 > footnote1);
2141            assert!(footnote1 < footnote3);
2142            assert!(footnote3 < footnote2);
2143            assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
2144        }
2145
2146        #[test]
2147        fn test_footnote_sorting() {
2148            let mut footnotes = vec![
2149                Footnote {
2150                    locate: 300,
2151                    content: "Third note".to_string(),
2152                },
2153                Footnote {
2154                    locate: 100,
2155                    content: "First note".to_string(),
2156                },
2157                Footnote {
2158                    locate: 200,
2159                    content: "Second note".to_string(),
2160                },
2161            ];
2162
2163            footnotes.sort();
2164
2165            assert_eq!(footnotes[0].locate, 100);
2166            assert_eq!(footnotes[1].locate, 200);
2167            assert_eq!(footnotes[2].locate, 300);
2168
2169            assert_eq!(footnotes[0].content, "First note");
2170            assert_eq!(footnotes[1].content, "Second note");
2171            assert_eq!(footnotes[2].content, "Third note");
2172        }
2173    }
2174
2175    #[cfg(feature = "content-builder")]
2176    mod block_type_tests {
2177        use crate::types::BlockType;
2178
2179        #[test]
2180        fn test_block_type_variants() {
2181            let _ = BlockType::Text;
2182            let _ = BlockType::Quote;
2183            let _ = BlockType::Title;
2184            let _ = BlockType::Image;
2185            let _ = BlockType::Audio;
2186            let _ = BlockType::Video;
2187            let _ = BlockType::MathML;
2188        }
2189
2190        #[test]
2191        fn test_block_type_debug() {
2192            let text = format!("{:?}", BlockType::Text);
2193            assert_eq!(text, "Text");
2194
2195            let quote = format!("{:?}", BlockType::Quote);
2196            assert_eq!(quote, "Quote");
2197
2198            let image = format!("{:?}", BlockType::Image);
2199            assert_eq!(image, "Image");
2200        }
2201    }
2202
2203    #[cfg(feature = "content-builder")]
2204    mod style_options_tests {
2205        use crate::types::{ColorScheme, PageLayout, StyleOptions, TextAlign, TextStyle};
2206
2207        #[test]
2208        fn test_style_options_default() {
2209            let options = StyleOptions::default();
2210
2211            assert_eq!(options.text.font_size, 1.0);
2212            assert_eq!(options.text.line_height, 1.6);
2213            assert_eq!(
2214                options.text.font_family,
2215                "-apple-system, Roboto, sans-serif"
2216            );
2217            assert_eq!(options.text.font_weight, "normal");
2218            assert_eq!(options.text.font_style, "normal");
2219            assert_eq!(options.text.letter_spacing, "normal");
2220            assert_eq!(options.text.text_indent, 2.0);
2221
2222            assert_eq!(options.color_scheme.background, "#FFFFFF");
2223            assert_eq!(options.color_scheme.text, "#000000");
2224            assert_eq!(options.color_scheme.link, "#6f6f6f");
2225
2226            assert_eq!(options.layout.margin, 20);
2227            assert_eq!(options.layout.text_align, TextAlign::Left);
2228            assert_eq!(options.layout.paragraph_spacing, 16);
2229        }
2230
2231        #[test]
2232        fn test_style_options_custom_values() {
2233            let text = TextStyle {
2234                font_size: 1.5,
2235                line_height: 2.0,
2236                font_family: "Georgia, serif".to_string(),
2237                font_weight: "bold".to_string(),
2238                font_style: "italic".to_string(),
2239                letter_spacing: "0.1em".to_string(),
2240                text_indent: 3.0,
2241            };
2242
2243            let color_scheme = ColorScheme {
2244                background: "#F0F0F0".to_string(),
2245                text: "#333333".to_string(),
2246                link: "#0066CC".to_string(),
2247            };
2248
2249            let layout = PageLayout {
2250                margin: 30,
2251                text_align: TextAlign::Center,
2252                paragraph_spacing: 20,
2253            };
2254
2255            let options = StyleOptions { text, color_scheme, layout };
2256
2257            assert_eq!(options.text.font_size, 1.5);
2258            assert_eq!(options.text.font_weight, "bold");
2259            assert_eq!(options.color_scheme.background, "#F0F0F0");
2260            assert_eq!(options.layout.text_align, TextAlign::Center);
2261        }
2262
2263        #[test]
2264        fn test_text_style_default() {
2265            let style = TextStyle::default();
2266
2267            assert_eq!(style.font_size, 1.0);
2268            assert_eq!(style.line_height, 1.6);
2269            assert_eq!(style.font_family, "-apple-system, Roboto, sans-serif");
2270            assert_eq!(style.font_weight, "normal");
2271            assert_eq!(style.font_style, "normal");
2272            assert_eq!(style.letter_spacing, "normal");
2273            assert_eq!(style.text_indent, 2.0);
2274        }
2275
2276        #[test]
2277        fn test_text_style_custom_values() {
2278            let style = TextStyle {
2279                font_size: 2.0,
2280                line_height: 1.8,
2281                font_family: "Times New Roman".to_string(),
2282                font_weight: "bold".to_string(),
2283                font_style: "italic".to_string(),
2284                letter_spacing: "0.05em".to_string(),
2285                text_indent: 0.0,
2286            };
2287
2288            assert_eq!(style.font_size, 2.0);
2289            assert_eq!(style.line_height, 1.8);
2290            assert_eq!(style.font_family, "Times New Roman");
2291            assert_eq!(style.font_weight, "bold");
2292            assert_eq!(style.font_style, "italic");
2293            assert_eq!(style.letter_spacing, "0.05em");
2294            assert_eq!(style.text_indent, 0.0);
2295        }
2296
2297        #[test]
2298        fn test_text_style_debug() {
2299            let style = TextStyle::default();
2300            let debug_str = format!("{:?}", style);
2301            assert!(debug_str.contains("TextStyle"));
2302            assert!(debug_str.contains("font_size"));
2303        }
2304
2305        #[test]
2306        fn test_color_scheme_default() {
2307            let scheme = ColorScheme::default();
2308
2309            assert_eq!(scheme.background, "#FFFFFF");
2310            assert_eq!(scheme.text, "#000000");
2311            assert_eq!(scheme.link, "#6f6f6f");
2312        }
2313
2314        #[test]
2315        fn test_color_scheme_custom_values() {
2316            let scheme = ColorScheme {
2317                background: "#000000".to_string(),
2318                text: "#FFFFFF".to_string(),
2319                link: "#00FF00".to_string(),
2320            };
2321
2322            assert_eq!(scheme.background, "#000000");
2323            assert_eq!(scheme.text, "#FFFFFF");
2324            assert_eq!(scheme.link, "#00FF00");
2325        }
2326
2327        #[test]
2328        fn test_color_scheme_debug() {
2329            let scheme = ColorScheme::default();
2330            let debug_str = format!("{:?}", scheme);
2331            assert!(debug_str.contains("ColorScheme"));
2332            assert!(debug_str.contains("background"));
2333        }
2334
2335        #[test]
2336        fn test_page_layout_default() {
2337            let layout = PageLayout::default();
2338
2339            assert_eq!(layout.margin, 20);
2340            assert_eq!(layout.text_align, TextAlign::Left);
2341            assert_eq!(layout.paragraph_spacing, 16);
2342        }
2343
2344        #[test]
2345        fn test_page_layout_custom_values() {
2346            let layout = PageLayout {
2347                margin: 40,
2348                text_align: TextAlign::Justify,
2349                paragraph_spacing: 24,
2350            };
2351
2352            assert_eq!(layout.margin, 40);
2353            assert_eq!(layout.text_align, TextAlign::Justify);
2354            assert_eq!(layout.paragraph_spacing, 24);
2355        }
2356
2357        #[test]
2358        fn test_page_layout_debug() {
2359            let layout = PageLayout::default();
2360            let debug_str = format!("{:?}", layout);
2361            assert!(debug_str.contains("PageLayout"));
2362            assert!(debug_str.contains("margin"));
2363        }
2364
2365        #[test]
2366        fn test_text_align_default() {
2367            let align = TextAlign::default();
2368            assert_eq!(align, TextAlign::Left);
2369        }
2370
2371        #[test]
2372        fn test_text_align_display() {
2373            assert_eq!(TextAlign::Left.to_string(), "left");
2374            assert_eq!(TextAlign::Right.to_string(), "right");
2375            assert_eq!(TextAlign::Justify.to_string(), "justify");
2376            assert_eq!(TextAlign::Center.to_string(), "center");
2377        }
2378
2379        #[test]
2380        fn test_text_align_all_variants() {
2381            let left = TextAlign::Left;
2382            let right = TextAlign::Right;
2383            let justify = TextAlign::Justify;
2384            let center = TextAlign::Center;
2385
2386            assert!(matches!(left, TextAlign::Left));
2387            assert!(matches!(right, TextAlign::Right));
2388            assert!(matches!(justify, TextAlign::Justify));
2389            assert!(matches!(center, TextAlign::Center));
2390        }
2391
2392        #[test]
2393        fn test_text_align_debug() {
2394            assert_eq!(format!("{:?}", TextAlign::Left), "Left");
2395            assert_eq!(format!("{:?}", TextAlign::Right), "Right");
2396            assert_eq!(format!("{:?}", TextAlign::Justify), "Justify");
2397            assert_eq!(format!("{:?}", TextAlign::Center), "Center");
2398        }
2399
2400        #[test]
2401        fn test_style_options_builder_new() {
2402            let options = StyleOptions::new();
2403            assert_eq!(options.text.font_size, 1.0);
2404            assert_eq!(options.color_scheme.background, "#FFFFFF");
2405            assert_eq!(options.layout.margin, 20);
2406        }
2407
2408        #[test]
2409        fn test_style_options_builder_with_text() {
2410            let mut options = StyleOptions::new();
2411            let text_style = TextStyle::new()
2412                .with_font_size(2.0)
2413                .with_font_weight("bold")
2414                .build();
2415            options.with_text(text_style);
2416
2417            assert_eq!(options.text.font_size, 2.0);
2418            assert_eq!(options.text.font_weight, "bold");
2419        }
2420
2421        #[test]
2422        fn test_style_options_builder_with_color_scheme() {
2423            let mut options = StyleOptions::new();
2424            let color = ColorScheme::new()
2425                .with_background("#000000")
2426                .with_text("#FFFFFF")
2427                .build();
2428            options.with_color_scheme(color);
2429
2430            assert_eq!(options.color_scheme.background, "#000000");
2431            assert_eq!(options.color_scheme.text, "#FFFFFF");
2432        }
2433
2434        #[test]
2435        fn test_style_options_builder_with_layout() {
2436            let mut options = StyleOptions::new();
2437            let layout = PageLayout::new()
2438                .with_margin(40)
2439                .with_text_align(TextAlign::Justify)
2440                .with_paragraph_spacing(24)
2441                .build();
2442            options.with_layout(layout);
2443
2444            assert_eq!(options.layout.margin, 40);
2445            assert_eq!(options.layout.text_align, TextAlign::Justify);
2446            assert_eq!(options.layout.paragraph_spacing, 24);
2447        }
2448
2449        #[test]
2450        fn test_style_options_builder_build() {
2451            let options = StyleOptions::new()
2452                .with_text(TextStyle::new().with_font_size(1.5).build())
2453                .with_color_scheme(ColorScheme::new().with_link("#FF0000").build())
2454                .with_layout(PageLayout::new().with_margin(30).build())
2455                .build();
2456
2457            assert_eq!(options.text.font_size, 1.5);
2458            assert_eq!(options.color_scheme.link, "#FF0000");
2459            assert_eq!(options.layout.margin, 30);
2460        }
2461
2462        #[test]
2463        fn test_style_options_builder_chaining() {
2464            let options = StyleOptions::new()
2465                .with_text(
2466                    TextStyle::new()
2467                        .with_font_size(1.5)
2468                        .with_line_height(2.0)
2469                        .with_font_family("Arial")
2470                        .with_font_weight("bold")
2471                        .with_font_style("italic")
2472                        .with_letter_spacing("0.1em")
2473                        .with_text_indent(1.5)
2474                        .build(),
2475                )
2476                .with_color_scheme(
2477                    ColorScheme::new()
2478                        .with_background("#CCCCCC")
2479                        .with_text("#111111")
2480                        .with_link("#0000FF")
2481                        .build(),
2482                )
2483                .with_layout(
2484                    PageLayout::new()
2485                        .with_margin(25)
2486                        .with_text_align(TextAlign::Right)
2487                        .with_paragraph_spacing(20)
2488                        .build(),
2489                )
2490                .build();
2491
2492            assert_eq!(options.text.font_size, 1.5);
2493            assert_eq!(options.text.line_height, 2.0);
2494            assert_eq!(options.text.font_family, "Arial");
2495            assert_eq!(options.text.font_weight, "bold");
2496            assert_eq!(options.text.font_style, "italic");
2497            assert_eq!(options.text.letter_spacing, "0.1em");
2498            assert_eq!(options.text.text_indent, 1.5);
2499
2500            assert_eq!(options.color_scheme.background, "#CCCCCC");
2501            assert_eq!(options.color_scheme.text, "#111111");
2502            assert_eq!(options.color_scheme.link, "#0000FF");
2503
2504            assert_eq!(options.layout.margin, 25);
2505            assert_eq!(options.layout.text_align, TextAlign::Right);
2506            assert_eq!(options.layout.paragraph_spacing, 20);
2507        }
2508
2509        #[test]
2510        fn test_text_style_builder_new() {
2511            let style = TextStyle::new();
2512            assert_eq!(style.font_size, 1.0);
2513            assert_eq!(style.line_height, 1.6);
2514        }
2515
2516        #[test]
2517        fn test_text_style_builder_with_font_size() {
2518            let mut style = TextStyle::new();
2519            style.with_font_size(2.5);
2520            assert_eq!(style.font_size, 2.5);
2521        }
2522
2523        #[test]
2524        fn test_text_style_builder_with_line_height() {
2525            let mut style = TextStyle::new();
2526            style.with_line_height(2.0);
2527            assert_eq!(style.line_height, 2.0);
2528        }
2529
2530        #[test]
2531        fn test_text_style_builder_with_font_family() {
2532            let mut style = TextStyle::new();
2533            style.with_font_family("Helvetica, Arial");
2534            assert_eq!(style.font_family, "Helvetica, Arial");
2535        }
2536
2537        #[test]
2538        fn test_text_style_builder_with_font_weight() {
2539            let mut style = TextStyle::new();
2540            style.with_font_weight("bold");
2541            assert_eq!(style.font_weight, "bold");
2542        }
2543
2544        #[test]
2545        fn test_text_style_builder_with_font_style() {
2546            let mut style = TextStyle::new();
2547            style.with_font_style("italic");
2548            assert_eq!(style.font_style, "italic");
2549        }
2550
2551        #[test]
2552        fn test_text_style_builder_with_letter_spacing() {
2553            let mut style = TextStyle::new();
2554            style.with_letter_spacing("0.05em");
2555            assert_eq!(style.letter_spacing, "0.05em");
2556        }
2557
2558        #[test]
2559        fn test_text_style_builder_with_text_indent() {
2560            let mut style = TextStyle::new();
2561            style.with_text_indent(3.0);
2562            assert_eq!(style.text_indent, 3.0);
2563        }
2564
2565        #[test]
2566        fn test_text_style_builder_build() {
2567            let style = TextStyle::new()
2568                .with_font_size(1.8)
2569                .with_line_height(1.9)
2570                .build();
2571
2572            assert_eq!(style.font_size, 1.8);
2573            assert_eq!(style.line_height, 1.9);
2574        }
2575
2576        #[test]
2577        fn test_text_style_builder_chaining() {
2578            let style = TextStyle::new()
2579                .with_font_size(2.0)
2580                .with_line_height(1.8)
2581                .with_font_family("Georgia")
2582                .with_font_weight("bold")
2583                .with_font_style("italic")
2584                .with_letter_spacing("0.1em")
2585                .with_text_indent(0.5)
2586                .build();
2587
2588            assert_eq!(style.font_size, 2.0);
2589            assert_eq!(style.line_height, 1.8);
2590            assert_eq!(style.font_family, "Georgia");
2591            assert_eq!(style.font_weight, "bold");
2592            assert_eq!(style.font_style, "italic");
2593            assert_eq!(style.letter_spacing, "0.1em");
2594            assert_eq!(style.text_indent, 0.5);
2595        }
2596
2597        #[test]
2598        fn test_color_scheme_builder_new() {
2599            let scheme = ColorScheme::new();
2600            assert_eq!(scheme.background, "#FFFFFF");
2601            assert_eq!(scheme.text, "#000000");
2602        }
2603
2604        #[test]
2605        fn test_color_scheme_builder_with_background() {
2606            let mut scheme = ColorScheme::new();
2607            scheme.with_background("#FF0000");
2608            assert_eq!(scheme.background, "#FF0000");
2609        }
2610
2611        #[test]
2612        fn test_color_scheme_builder_with_text() {
2613            let mut scheme = ColorScheme::new();
2614            scheme.with_text("#333333");
2615            assert_eq!(scheme.text, "#333333");
2616        }
2617
2618        #[test]
2619        fn test_color_scheme_builder_with_link() {
2620            let mut scheme = ColorScheme::new();
2621            scheme.with_link("#0000FF");
2622            assert_eq!(scheme.link, "#0000FF");
2623        }
2624
2625        #[test]
2626        fn test_color_scheme_builder_build() {
2627            let scheme = ColorScheme::new().with_background("#123456").build();
2628
2629            assert_eq!(scheme.background, "#123456");
2630            assert_eq!(scheme.text, "#000000");
2631        }
2632
2633        #[test]
2634        fn test_color_scheme_builder_chaining() {
2635            let scheme = ColorScheme::new()
2636                .with_background("#AABBCC")
2637                .with_text("#DDEEFF")
2638                .with_link("#112233")
2639                .build();
2640
2641            assert_eq!(scheme.background, "#AABBCC");
2642            assert_eq!(scheme.text, "#DDEEFF");
2643            assert_eq!(scheme.link, "#112233");
2644        }
2645
2646        #[test]
2647        fn test_page_layout_builder_new() {
2648            let layout = PageLayout::new();
2649            assert_eq!(layout.margin, 20);
2650            assert_eq!(layout.text_align, TextAlign::Left);
2651            assert_eq!(layout.paragraph_spacing, 16);
2652        }
2653
2654        #[test]
2655        fn test_page_layout_builder_with_margin() {
2656            let mut layout = PageLayout::new();
2657            layout.with_margin(50);
2658            assert_eq!(layout.margin, 50);
2659        }
2660
2661        #[test]
2662        fn test_page_layout_builder_with_text_align() {
2663            let mut layout = PageLayout::new();
2664            layout.with_text_align(TextAlign::Center);
2665            assert_eq!(layout.text_align, TextAlign::Center);
2666        }
2667
2668        #[test]
2669        fn test_page_layout_builder_with_paragraph_spacing() {
2670            let mut layout = PageLayout::new();
2671            layout.with_paragraph_spacing(30);
2672            assert_eq!(layout.paragraph_spacing, 30);
2673        }
2674
2675        #[test]
2676        fn test_page_layout_builder_build() {
2677            let layout = PageLayout::new().with_margin(35).build();
2678
2679            assert_eq!(layout.margin, 35);
2680            assert_eq!(layout.text_align, TextAlign::Left);
2681        }
2682
2683        #[test]
2684        fn test_page_layout_builder_chaining() {
2685            let layout = PageLayout::new()
2686                .with_margin(45)
2687                .with_text_align(TextAlign::Justify)
2688                .with_paragraph_spacing(28)
2689                .build();
2690
2691            assert_eq!(layout.margin, 45);
2692            assert_eq!(layout.text_align, TextAlign::Justify);
2693            assert_eq!(layout.paragraph_spacing, 28);
2694        }
2695
2696        #[test]
2697        fn test_page_layout_builder_all_text_align_variants() {
2698            let left = PageLayout::new().with_text_align(TextAlign::Left).build();
2699            assert_eq!(left.text_align, TextAlign::Left);
2700
2701            let right = PageLayout::new().with_text_align(TextAlign::Right).build();
2702            assert_eq!(right.text_align, TextAlign::Right);
2703
2704            let center = PageLayout::new().with_text_align(TextAlign::Center).build();
2705            assert_eq!(center.text_align, TextAlign::Center);
2706
2707            let justify = PageLayout::new()
2708                .with_text_align(TextAlign::Justify)
2709                .build();
2710            assert_eq!(justify.text_align, TextAlign::Justify);
2711        }
2712    }
2713}