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)]
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/// Configuration options for document styling
846///
847/// This struct aggregates all style-related configuration for an EPUB document,
848/// including text appearance, color scheme, and page layout settings.
849#[cfg(feature = "content-builder")]
850#[derive(Debug, Default, Clone)]
851pub struct StyleOptions {
852    /// Text styling configuration
853    pub text: TextStyle,
854
855    /// Color scheme configuration
856    ///
857    /// Defines the background, text, and link colors for the document.
858    pub color_scheme: ColorScheme,
859
860    /// Page layout configuration
861    ///
862    /// Controls margins, text alignment, and paragraph spacing.
863    pub layout: PageLayout,
864}
865
866#[cfg(feature = "content-builder")]
867#[cfg(feature = "builder")]
868impl StyleOptions {
869    /// Creates a new style options with default values
870    pub fn new() -> Self {
871        Self::default()
872    }
873
874    /// Sets the text style configuration
875    pub fn with_text(&mut self, text: TextStyle) -> &mut Self {
876        self.text = text;
877        self
878    }
879
880    /// Sets the color scheme configuration
881    pub fn with_color_scheme(&mut self, color_scheme: ColorScheme) -> &mut Self {
882        self.color_scheme = color_scheme;
883        self
884    }
885
886    /// Sets the page layout configuration
887    pub fn with_layout(&mut self, layout: PageLayout) -> &mut Self {
888        self.layout = layout;
889        self
890    }
891
892    /// Builds the final style options
893    pub fn build(&self) -> Self {
894        Self { ..self.clone() }
895    }
896}
897
898/// Text styling configuration
899///
900/// Defines the visual appearance of text content in the document,
901/// including font properties, sizing, and spacing.
902#[cfg(feature = "content-builder")]
903#[derive(Debug, Clone)]
904pub struct TextStyle {
905    /// The base font size (default: 1.0, unit: rem)
906    ///
907    /// Relative to the root element, providing consistent sizing
908    /// across different viewing contexts.
909    pub font_size: f32,
910
911    /// The line height (default: 1.6, unit: em)
912    ///
913    /// Controls the vertical spacing between lines of text.
914    /// Values greater than 1.0 increase spacing, while values
915    /// less than 1.0 compress the text.
916    pub line_height: f32,
917
918    /// The font family stack (default: "-apple-system, Roboto, sans-serif")
919    ///
920    /// A comma-separated list of font families to use, with
921    /// fallback fonts specified for compatibility.
922    pub font_family: String,
923
924    /// The font weight (default: "normal")
925    ///
926    /// Controls the thickness of the font strokes. Common values
927    /// include "normal" and "bold".
928    pub font_weight: String,
929
930    /// The font style (default: "normal")
931    ///
932    /// Controls whether the font is normal, italic, or oblique.
933    /// Common values include "normal" and "italic".
934    pub font_style: String,
935
936    /// The letter spacing (default: "normal")
937    ///
938    /// Controls the space between characters. Common values
939    /// include "normal" or specific lengths like "0.05em".
940    pub letter_spacing: String,
941
942    /// The text indent for paragraphs (default: 2.0, unit: em)
943    ///
944    /// Controls the indentation of the first line of paragraphs.
945    /// A value of 2.0 means the first line is indented by 2 ems.
946    pub text_indent: f32,
947}
948
949#[cfg(feature = "content-builder")]
950impl Default for TextStyle {
951    fn default() -> Self {
952        Self {
953            font_size: 1.0,
954            line_height: 1.6,
955            font_family: "-apple-system, Roboto, sans-serif".to_string(),
956            font_weight: "normal".to_string(),
957            font_style: "normal".to_string(),
958            letter_spacing: "normal".to_string(),
959            text_indent: 2.0,
960        }
961    }
962}
963
964#[cfg(feature = "content-builder")]
965impl TextStyle {
966    /// Creates a new text style with default values
967    pub fn new() -> Self {
968        Self::default()
969    }
970
971    /// Sets the font size
972    pub fn with_font_size(&mut self, font_size: f32) -> &mut Self {
973        self.font_size = font_size;
974        self
975    }
976
977    /// Sets the line height
978    pub fn with_line_height(&mut self, line_height: f32) -> &mut Self {
979        self.line_height = line_height;
980        self
981    }
982
983    /// Sets the font family
984    pub fn with_font_family(&mut self, font_family: &str) -> &mut Self {
985        self.font_family = font_family.to_string();
986        self
987    }
988
989    /// Sets the font weight
990    pub fn with_font_weight(&mut self, font_weight: &str) -> &mut Self {
991        self.font_weight = font_weight.to_string();
992        self
993    }
994
995    /// Sets the font style
996    pub fn with_font_style(&mut self, font_style: &str) -> &mut Self {
997        self.font_style = font_style.to_string();
998        self
999    }
1000
1001    /// Sets the letter spacing
1002    pub fn with_letter_spacing(&mut self, letter_spacing: &str) -> &mut Self {
1003        self.letter_spacing = letter_spacing.to_string();
1004        self
1005    }
1006
1007    /// Sets the text indent
1008    pub fn with_text_indent(&mut self, text_indent: f32) -> &mut Self {
1009        self.text_indent = text_indent;
1010        self
1011    }
1012
1013    /// Builds the final text style
1014    pub fn build(&self) -> Self {
1015        Self { ..self.clone() }
1016    }
1017}
1018
1019/// Color scheme configuration
1020///
1021/// Defines the color palette for the document, including background,
1022/// text, and link colors.
1023#[cfg(feature = "content-builder")]
1024#[derive(Debug, Clone)]
1025pub struct ColorScheme {
1026    /// The background color (default: "#FFFFFF")
1027    ///
1028    /// The fill color for the document body. Specified as a hex color
1029    /// string (e.g., "#FFFFFF" for white).
1030    pub background: String,
1031
1032    /// The text color (default: "#000000")
1033    ///
1034    /// The primary color for text content. Specified as a hex color
1035    /// string (e.g., "#000000" for black).
1036    pub text: String,
1037
1038    /// The link color (default: "#6f6f6f")
1039    ///
1040    /// The color for hyperlinks in the document. Specified as a hex
1041    /// color string (e.g., "#6f6f6f" for gray).
1042    pub link: String,
1043}
1044
1045#[cfg(feature = "content-builder")]
1046impl Default for ColorScheme {
1047    fn default() -> Self {
1048        Self {
1049            background: "#FFFFFF".to_string(),
1050            text: "#000000".to_string(),
1051            link: "#6f6f6f".to_string(),
1052        }
1053    }
1054}
1055
1056#[cfg(feature = "content-builder")]
1057impl ColorScheme {
1058    /// Creates a new color scheme with default values
1059    pub fn new() -> Self {
1060        Self::default()
1061    }
1062
1063    /// Sets the background color
1064    pub fn with_background(&mut self, background: &str) -> &mut Self {
1065        self.background = background.to_string();
1066        self
1067    }
1068
1069    /// Sets the text color
1070    pub fn with_text(&mut self, text: &str) -> &mut Self {
1071        self.text = text.to_string();
1072        self
1073    }
1074
1075    /// Sets the link color
1076    pub fn with_link(&mut self, link: &str) -> &mut Self {
1077        self.link = link.to_string();
1078        self
1079    }
1080
1081    /// Builds the final color scheme
1082    pub fn build(&self) -> Self {
1083        Self { ..self.clone() }
1084    }
1085}
1086
1087/// Page layout configuration
1088///
1089/// Defines the layout properties for pages in the document, including
1090/// margins, text alignment, and paragraph spacing.
1091#[cfg(feature = "content-builder")]
1092#[derive(Debug, Clone)]
1093pub struct PageLayout {
1094    /// The page margin (default: 20, unit: pixels)
1095    ///
1096    /// Controls the space around the content area on each page.
1097    pub margin: usize,
1098
1099    /// The text alignment mode (default: TextAlign::Left)
1100    ///
1101    /// Controls how text is aligned within the content area.
1102    pub text_align: TextAlign,
1103
1104    /// The spacing between paragraphs (default: 16, unit: pixels)
1105    ///
1106    /// Controls the vertical space between block-level elements.
1107    pub paragraph_spacing: usize,
1108}
1109
1110#[cfg(feature = "content-builder")]
1111impl Default for PageLayout {
1112    fn default() -> Self {
1113        Self {
1114            margin: 20,
1115            text_align: Default::default(),
1116            paragraph_spacing: 16,
1117        }
1118    }
1119}
1120
1121#[cfg(feature = "content-builder")]
1122impl PageLayout {
1123    /// Creates a new page layout with default values
1124    pub fn new() -> Self {
1125        Self::default()
1126    }
1127
1128    /// Sets the page margin
1129    pub fn with_margin(&mut self, margin: usize) -> &mut Self {
1130        self.margin = margin;
1131        self
1132    }
1133
1134    /// Sets the text alignment
1135    pub fn with_text_align(&mut self, text_align: TextAlign) -> &mut Self {
1136        self.text_align = text_align;
1137        self
1138    }
1139
1140    /// Sets the paragraph spacing
1141    pub fn with_paragraph_spacing(&mut self, paragraph_spacing: usize) -> &mut Self {
1142        self.paragraph_spacing = paragraph_spacing;
1143        self
1144    }
1145
1146    /// Builds the final page layout
1147    pub fn build(&self) -> Self {
1148        Self { ..self.clone() }
1149    }
1150}
1151
1152/// Text alignment options
1153///
1154/// Defines the available text alignment modes for content in the document.
1155#[cfg(feature = "content-builder")]
1156#[derive(Debug, Default, Clone, Copy, PartialEq)]
1157pub enum TextAlign {
1158    /// Left-aligned text
1159    ///
1160    /// Text is aligned to the left margin, with the right edge ragged.
1161    #[default]
1162    Left,
1163
1164    /// Right-aligned text
1165    ///
1166    /// Text is aligned to the right margin, with the left edge ragged.
1167    Right,
1168
1169    /// Justified text
1170    ///
1171    /// Text is aligned to both margins by adjusting the spacing between
1172    /// words. The left and right edges are both straight.
1173    Justify,
1174
1175    /// Centered text
1176    ///
1177    /// Text is centered within the content area, with both edges ragged.
1178    Center,
1179}
1180
1181#[cfg(feature = "content-builder")]
1182impl std::fmt::Display for TextAlign {
1183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1184        match self {
1185            TextAlign::Left => write!(f, "left"),
1186            TextAlign::Right => write!(f, "right"),
1187            TextAlign::Justify => write!(f, "justify"),
1188            TextAlign::Center => write!(f, "center"),
1189        }
1190    }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195    mod navpoint_tests {
1196        use std::path::PathBuf;
1197
1198        use crate::types::NavPoint;
1199
1200        /// Testing the equality comparison of NavPoint
1201        #[test]
1202        fn test_navpoint_partial_eq() {
1203            let nav1 = NavPoint {
1204                label: "Chapter 1".to_string(),
1205                content: Some(PathBuf::from("chapter1.html")),
1206                children: vec![],
1207                play_order: Some(1),
1208            };
1209
1210            let nav2 = NavPoint {
1211                label: "Chapter 1".to_string(),
1212                content: Some(PathBuf::from("chapter2.html")),
1213                children: vec![],
1214                play_order: Some(1),
1215            };
1216
1217            let nav3 = NavPoint {
1218                label: "Chapter 2".to_string(),
1219                content: Some(PathBuf::from("chapter1.html")),
1220                children: vec![],
1221                play_order: Some(2),
1222            };
1223
1224            assert_eq!(nav1, nav2); // Same play_order, different contents, should be equal
1225            assert_ne!(nav1, nav3); // Different play_order, Same contents, should be unequal
1226        }
1227
1228        /// Test NavPoint sorting comparison
1229        #[test]
1230        fn test_navpoint_ord() {
1231            let nav1 = NavPoint {
1232                label: "Chapter 1".to_string(),
1233                content: Some(PathBuf::from("chapter1.html")),
1234                children: vec![],
1235                play_order: Some(1),
1236            };
1237
1238            let nav2 = NavPoint {
1239                label: "Chapter 2".to_string(),
1240                content: Some(PathBuf::from("chapter2.html")),
1241                children: vec![],
1242                play_order: Some(2),
1243            };
1244
1245            let nav3 = NavPoint {
1246                label: "Chapter 3".to_string(),
1247                content: Some(PathBuf::from("chapter3.html")),
1248                children: vec![],
1249                play_order: Some(3),
1250            };
1251
1252            // Test function cmp
1253            assert!(nav1 < nav2);
1254            assert!(nav2 > nav1);
1255            assert!(nav1 == nav1);
1256
1257            // Test function partial_cmp
1258            assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
1259            assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
1260            assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
1261
1262            // Test function sort
1263            let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
1264            nav_points.sort();
1265            assert_eq!(nav_points, vec![nav1, nav2, nav3]);
1266        }
1267
1268        /// Test the case of None play_order
1269        #[test]
1270        fn test_navpoint_ord_with_none_play_order() {
1271            let nav_with_order = NavPoint {
1272                label: "Chapter 1".to_string(),
1273                content: Some(PathBuf::from("chapter1.html")),
1274                children: vec![],
1275                play_order: Some(1),
1276            };
1277
1278            let nav_without_order = NavPoint {
1279                label: "Preface".to_string(),
1280                content: Some(PathBuf::from("preface.html")),
1281                children: vec![],
1282                play_order: None,
1283            };
1284
1285            assert!(nav_without_order < nav_with_order);
1286            assert!(nav_with_order > nav_without_order);
1287
1288            let nav_without_order2 = NavPoint {
1289                label: "Introduction".to_string(),
1290                content: Some(PathBuf::from("intro.html")),
1291                children: vec![],
1292                play_order: None,
1293            };
1294
1295            assert!(nav_without_order == nav_without_order2);
1296        }
1297
1298        /// Test NavPoint containing child nodes
1299        #[test]
1300        fn test_navpoint_with_children() {
1301            let child1 = NavPoint {
1302                label: "Section 1.1".to_string(),
1303                content: Some(PathBuf::from("section1_1.html")),
1304                children: vec![],
1305                play_order: Some(1),
1306            };
1307
1308            let child2 = NavPoint {
1309                label: "Section 1.2".to_string(),
1310                content: Some(PathBuf::from("section1_2.html")),
1311                children: vec![],
1312                play_order: Some(2),
1313            };
1314
1315            let parent1 = NavPoint {
1316                label: "Chapter 1".to_string(),
1317                content: Some(PathBuf::from("chapter1.html")),
1318                children: vec![child1.clone(), child2.clone()],
1319                play_order: Some(1),
1320            };
1321
1322            let parent2 = NavPoint {
1323                label: "Chapter 1".to_string(),
1324                content: Some(PathBuf::from("chapter1.html")),
1325                children: vec![child1.clone(), child2.clone()],
1326                play_order: Some(1),
1327            };
1328
1329            assert!(parent1 == parent2);
1330
1331            let parent3 = NavPoint {
1332                label: "Chapter 2".to_string(),
1333                content: Some(PathBuf::from("chapter2.html")),
1334                children: vec![child1.clone(), child2.clone()],
1335                play_order: Some(2),
1336            };
1337
1338            assert!(parent1 != parent3);
1339            assert!(parent1 < parent3);
1340        }
1341
1342        /// Test the case where content is None
1343        #[test]
1344        fn test_navpoint_with_none_content() {
1345            let nav1 = NavPoint {
1346                label: "Chapter 1".to_string(),
1347                content: None,
1348                children: vec![],
1349                play_order: Some(1),
1350            };
1351
1352            let nav2 = NavPoint {
1353                label: "Chapter 1".to_string(),
1354                content: None,
1355                children: vec![],
1356                play_order: Some(1),
1357            };
1358
1359            assert!(nav1 == nav2);
1360        }
1361    }
1362
1363    #[cfg(feature = "builder")]
1364    mod builder_tests {
1365        mod metadata_item {
1366            use crate::types::{MetadataItem, MetadataRefinement};
1367
1368            #[test]
1369            fn test_metadata_item_new() {
1370                let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1371
1372                assert_eq!(metadata_item.property, "title");
1373                assert_eq!(metadata_item.value, "EPUB Test Book");
1374                assert_eq!(metadata_item.id, None);
1375                assert_eq!(metadata_item.lang, None);
1376                assert_eq!(metadata_item.refined.len(), 0);
1377            }
1378
1379            #[test]
1380            fn test_metadata_item_with_id() {
1381                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1382                metadata_item.with_id("creator-1");
1383
1384                assert_eq!(metadata_item.property, "creator");
1385                assert_eq!(metadata_item.value, "John Doe");
1386                assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1387                assert_eq!(metadata_item.lang, None);
1388                assert_eq!(metadata_item.refined.len(), 0);
1389            }
1390
1391            #[test]
1392            fn test_metadata_item_with_lang() {
1393                let mut metadata_item = MetadataItem::new("title", "测试书籍");
1394                metadata_item.with_lang("zh-CN");
1395
1396                assert_eq!(metadata_item.property, "title");
1397                assert_eq!(metadata_item.value, "测试书籍");
1398                assert_eq!(metadata_item.id, None);
1399                assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1400                assert_eq!(metadata_item.refined.len(), 0);
1401            }
1402
1403            #[test]
1404            fn test_metadata_item_append_refinement() {
1405                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1406                metadata_item.with_id("creator-1"); // ID is required for refinements
1407
1408                let refinement = MetadataRefinement::new("creator-1", "role", "author");
1409                metadata_item.append_refinement(refinement);
1410
1411                assert_eq!(metadata_item.refined.len(), 1);
1412                assert_eq!(metadata_item.refined[0].refines, "creator-1");
1413                assert_eq!(metadata_item.refined[0].property, "role");
1414                assert_eq!(metadata_item.refined[0].value, "author");
1415            }
1416
1417            #[test]
1418            fn test_metadata_item_append_refinement_without_id() {
1419                let mut metadata_item = MetadataItem::new("title", "Test Book");
1420                // No ID set
1421
1422                let refinement = MetadataRefinement::new("title", "title-type", "main");
1423                metadata_item.append_refinement(refinement);
1424
1425                // Refinement should not be added because metadata item has no ID
1426                assert_eq!(metadata_item.refined.len(), 0);
1427            }
1428
1429            #[test]
1430            fn test_metadata_item_build() {
1431                let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1432                metadata_item.with_id("pub-id").with_lang("en");
1433
1434                let built = metadata_item.build();
1435
1436                assert_eq!(built.property, "identifier");
1437                assert_eq!(built.value, "urn:isbn:1234567890");
1438                assert_eq!(built.id, Some("pub-id".to_string()));
1439                assert_eq!(built.lang, Some("en".to_string()));
1440                assert_eq!(built.refined.len(), 0);
1441            }
1442
1443            #[test]
1444            fn test_metadata_item_builder_chaining() {
1445                let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1446                metadata_item.with_id("title").with_lang("en");
1447
1448                let refinement = MetadataRefinement::new("title", "title-type", "main");
1449                metadata_item.append_refinement(refinement);
1450
1451                let built = metadata_item.build();
1452
1453                assert_eq!(built.property, "title");
1454                assert_eq!(built.value, "EPUB 3.3 Guide");
1455                assert_eq!(built.id, Some("title".to_string()));
1456                assert_eq!(built.lang, Some("en".to_string()));
1457                assert_eq!(built.refined.len(), 1);
1458            }
1459
1460            #[test]
1461            fn test_metadata_item_attributes_dc_namespace() {
1462                let mut metadata_item = MetadataItem::new("title", "Test Book");
1463                metadata_item.with_id("title-id");
1464
1465                let attributes = metadata_item.attributes();
1466
1467                // For DC namespace properties, no "property" attribute should be added
1468                assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1469                assert!(
1470                    attributes
1471                        .iter()
1472                        .any(|(k, v)| k == &"id" && v == &"title-id")
1473                );
1474            }
1475
1476            #[test]
1477            fn test_metadata_item_attributes_non_dc_namespace() {
1478                let mut metadata_item = MetadataItem::new("meta", "value");
1479                metadata_item.with_id("meta-id");
1480
1481                let attributes = metadata_item.attributes();
1482
1483                // For non-DC namespace properties, "property" attribute should be added
1484                assert!(attributes.iter().any(|(k, _)| k == &"property"));
1485                assert!(
1486                    attributes
1487                        .iter()
1488                        .any(|(k, v)| k == &"id" && v == &"meta-id")
1489                );
1490            }
1491
1492            #[test]
1493            fn test_metadata_item_attributes_with_lang() {
1494                let mut metadata_item = MetadataItem::new("title", "Test Book");
1495                metadata_item.with_id("title-id").with_lang("en");
1496
1497                let attributes = metadata_item.attributes();
1498
1499                assert!(
1500                    attributes
1501                        .iter()
1502                        .any(|(k, v)| k == &"id" && v == &"title-id")
1503                );
1504                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1505            }
1506        }
1507
1508        mod metadata_refinement {
1509            use crate::types::MetadataRefinement;
1510
1511            #[test]
1512            fn test_metadata_refinement_new() {
1513                let refinement = MetadataRefinement::new("title", "title-type", "main");
1514
1515                assert_eq!(refinement.refines, "title");
1516                assert_eq!(refinement.property, "title-type");
1517                assert_eq!(refinement.value, "main");
1518                assert_eq!(refinement.lang, None);
1519                assert_eq!(refinement.scheme, None);
1520            }
1521
1522            #[test]
1523            fn test_metadata_refinement_with_lang() {
1524                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1525                refinement.with_lang("en");
1526
1527                assert_eq!(refinement.refines, "creator");
1528                assert_eq!(refinement.property, "role");
1529                assert_eq!(refinement.value, "author");
1530                assert_eq!(refinement.lang, Some("en".to_string()));
1531                assert_eq!(refinement.scheme, None);
1532            }
1533
1534            #[test]
1535            fn test_metadata_refinement_with_scheme() {
1536                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1537                refinement.with_scheme("marc:relators");
1538
1539                assert_eq!(refinement.refines, "creator");
1540                assert_eq!(refinement.property, "role");
1541                assert_eq!(refinement.value, "author");
1542                assert_eq!(refinement.lang, None);
1543                assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1544            }
1545
1546            #[test]
1547            fn test_metadata_refinement_build() {
1548                let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1549                refinement.with_lang("ja").with_scheme("iso-15924");
1550
1551                let built = refinement.build();
1552
1553                assert_eq!(built.refines, "title");
1554                assert_eq!(built.property, "alternate-script");
1555                assert_eq!(built.value, "テスト");
1556                assert_eq!(built.lang, Some("ja".to_string()));
1557                assert_eq!(built.scheme, Some("iso-15924".to_string()));
1558            }
1559
1560            #[test]
1561            fn test_metadata_refinement_builder_chaining() {
1562                let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1563                refinement.with_lang("en").with_scheme("dcterms");
1564
1565                let built = refinement.build();
1566
1567                assert_eq!(built.refines, "creator");
1568                assert_eq!(built.property, "file-as");
1569                assert_eq!(built.value, "Doe, John");
1570                assert_eq!(built.lang, Some("en".to_string()));
1571                assert_eq!(built.scheme, Some("dcterms".to_string()));
1572            }
1573
1574            #[test]
1575            fn test_metadata_refinement_attributes() {
1576                let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1577                refinement.with_lang("en").with_scheme("onix:codelist5");
1578
1579                let attributes = refinement.attributes();
1580
1581                assert!(
1582                    attributes
1583                        .iter()
1584                        .any(|(k, v)| k == &"refines" && v == &"title")
1585                );
1586                assert!(
1587                    attributes
1588                        .iter()
1589                        .any(|(k, v)| k == &"property" && v == &"title-type")
1590                );
1591                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1592                assert!(
1593                    attributes
1594                        .iter()
1595                        .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1596                );
1597            }
1598
1599            #[test]
1600            fn test_metadata_refinement_attributes_optional_fields() {
1601                let refinement = MetadataRefinement::new("creator", "role", "author");
1602                let attributes = refinement.attributes();
1603
1604                assert!(
1605                    attributes
1606                        .iter()
1607                        .any(|(k, v)| k == &"refines" && v == &"creator")
1608                );
1609                assert!(
1610                    attributes
1611                        .iter()
1612                        .any(|(k, v)| k == &"property" && v == &"role")
1613                );
1614
1615                // Should not contain optional attributes when they are None
1616                assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1617                assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1618            }
1619        }
1620
1621        mod manifest_item {
1622            use std::path::PathBuf;
1623
1624            use crate::types::ManifestItem;
1625
1626            #[test]
1627            fn test_manifest_item_new() {
1628                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1629                assert!(manifest_item.is_ok());
1630
1631                let manifest_item = manifest_item.unwrap();
1632                assert_eq!(manifest_item.id, "cover");
1633                assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1634                assert_eq!(manifest_item.mime, "");
1635                assert_eq!(manifest_item.properties, None);
1636                assert_eq!(manifest_item.fallback, None);
1637            }
1638
1639            #[test]
1640            fn test_manifest_item_append_property() {
1641                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1642                assert!(manifest_item.is_ok());
1643
1644                let mut manifest_item = manifest_item.unwrap();
1645                manifest_item.append_property("nav");
1646
1647                assert_eq!(manifest_item.id, "nav");
1648                assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1649                assert_eq!(manifest_item.mime, "");
1650                assert_eq!(manifest_item.properties, Some("nav".to_string()));
1651                assert_eq!(manifest_item.fallback, None);
1652            }
1653
1654            #[test]
1655            fn test_manifest_item_append_multiple_properties() {
1656                let manifest_item = ManifestItem::new("content", "content.xhtml");
1657                assert!(manifest_item.is_ok());
1658
1659                let mut manifest_item = manifest_item.unwrap();
1660                manifest_item
1661                    .append_property("nav")
1662                    .append_property("scripted")
1663                    .append_property("svg");
1664
1665                assert_eq!(
1666                    manifest_item.properties,
1667                    Some("nav scripted svg".to_string())
1668                );
1669            }
1670
1671            #[test]
1672            fn test_manifest_item_with_fallback() {
1673                let manifest_item = ManifestItem::new("image", "image.tiff");
1674                assert!(manifest_item.is_ok());
1675
1676                let mut manifest_item = manifest_item.unwrap();
1677                manifest_item.with_fallback("image-fallback");
1678
1679                assert_eq!(manifest_item.id, "image");
1680                assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1681                assert_eq!(manifest_item.mime, "");
1682                assert_eq!(manifest_item.properties, None);
1683                assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1684            }
1685
1686            #[test]
1687            fn test_manifest_item_set_mime() {
1688                let manifest_item = ManifestItem::new("style", "style.css");
1689                assert!(manifest_item.is_ok());
1690
1691                let manifest_item = manifest_item.unwrap();
1692                let updated_item = manifest_item.set_mime("text/css");
1693
1694                assert_eq!(updated_item.id, "style");
1695                assert_eq!(updated_item.path, PathBuf::from("style.css"));
1696                assert_eq!(updated_item.mime, "text/css");
1697                assert_eq!(updated_item.properties, None);
1698                assert_eq!(updated_item.fallback, None);
1699            }
1700
1701            #[test]
1702            fn test_manifest_item_build() {
1703                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1704                assert!(manifest_item.is_ok());
1705
1706                let mut manifest_item = manifest_item.unwrap();
1707                manifest_item
1708                    .append_property("cover-image")
1709                    .with_fallback("cover-fallback");
1710
1711                let built = manifest_item.build();
1712
1713                assert_eq!(built.id, "cover");
1714                assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
1715                assert_eq!(built.mime, "");
1716                assert_eq!(built.properties, Some("cover-image".to_string()));
1717                assert_eq!(built.fallback, Some("cover-fallback".to_string()));
1718            }
1719
1720            #[test]
1721            fn test_manifest_item_builder_chaining() {
1722                let manifest_item = ManifestItem::new("content", "content.xhtml");
1723                assert!(manifest_item.is_ok());
1724
1725                let mut manifest_item = manifest_item.unwrap();
1726                manifest_item
1727                    .append_property("scripted")
1728                    .append_property("svg")
1729                    .with_fallback("fallback-content");
1730
1731                let built = manifest_item.build();
1732
1733                assert_eq!(built.id, "content");
1734                assert_eq!(built.path, PathBuf::from("content.xhtml"));
1735                assert_eq!(built.mime, "");
1736                assert_eq!(built.properties, Some("scripted svg".to_string()));
1737                assert_eq!(built.fallback, Some("fallback-content".to_string()));
1738            }
1739
1740            #[test]
1741            fn test_manifest_item_attributes() {
1742                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1743                assert!(manifest_item.is_ok());
1744
1745                let mut manifest_item = manifest_item.unwrap();
1746                manifest_item
1747                    .append_property("nav")
1748                    .with_fallback("fallback-nav");
1749
1750                // Manually set mime type for testing
1751                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1752                let attributes = manifest_item.attributes();
1753
1754                // Check that all expected attributes are present
1755                assert!(attributes.contains(&("id", "nav")));
1756                assert!(attributes.contains(&("href", "nav.xhtml")));
1757                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1758                assert!(attributes.contains(&("properties", "nav")));
1759                assert!(attributes.contains(&("fallback", "fallback-nav")));
1760            }
1761
1762            #[test]
1763            fn test_manifest_item_attributes_optional_fields() {
1764                let manifest_item = ManifestItem::new("simple", "simple.xhtml");
1765                assert!(manifest_item.is_ok());
1766
1767                let manifest_item = manifest_item.unwrap();
1768                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1769                let attributes = manifest_item.attributes();
1770
1771                // Should contain required attributes
1772                assert!(attributes.contains(&("id", "simple")));
1773                assert!(attributes.contains(&("href", "simple.xhtml")));
1774                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1775
1776                // Should not contain optional attributes when they are None
1777                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1778                assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
1779            }
1780
1781            #[test]
1782            fn test_manifest_item_path_handling() {
1783                let manifest_item = ManifestItem::new("test", "../images/test.png");
1784                assert!(manifest_item.is_err());
1785
1786                let err = manifest_item.unwrap_err();
1787                assert_eq!(
1788                    err.to_string(),
1789                    "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
1790                );
1791            }
1792        }
1793
1794        mod spine_item {
1795            use crate::types::SpineItem;
1796
1797            #[test]
1798            fn test_spine_item_new() {
1799                let spine_item = SpineItem::new("content_001");
1800
1801                assert_eq!(spine_item.idref, "content_001");
1802                assert_eq!(spine_item.id, None);
1803                assert_eq!(spine_item.properties, None);
1804                assert_eq!(spine_item.linear, true);
1805            }
1806
1807            #[test]
1808            fn test_spine_item_with_id() {
1809                let mut spine_item = SpineItem::new("content_001");
1810                spine_item.with_id("spine1");
1811
1812                assert_eq!(spine_item.idref, "content_001");
1813                assert_eq!(spine_item.id, Some("spine1".to_string()));
1814                assert_eq!(spine_item.properties, None);
1815                assert_eq!(spine_item.linear, true);
1816            }
1817
1818            #[test]
1819            fn test_spine_item_append_property() {
1820                let mut spine_item = SpineItem::new("content_001");
1821                spine_item.append_property("page-spread-left");
1822
1823                assert_eq!(spine_item.idref, "content_001");
1824                assert_eq!(spine_item.id, None);
1825                assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
1826                assert_eq!(spine_item.linear, true);
1827            }
1828
1829            #[test]
1830            fn test_spine_item_append_multiple_properties() {
1831                let mut spine_item = SpineItem::new("content_001");
1832                spine_item
1833                    .append_property("page-spread-left")
1834                    .append_property("rendition:layout-pre-paginated");
1835
1836                assert_eq!(
1837                    spine_item.properties,
1838                    Some("page-spread-left rendition:layout-pre-paginated".to_string())
1839                );
1840            }
1841
1842            #[test]
1843            fn test_spine_item_set_linear() {
1844                let mut spine_item = SpineItem::new("content_001");
1845                spine_item.set_linear(false);
1846
1847                assert_eq!(spine_item.idref, "content_001");
1848                assert_eq!(spine_item.id, None);
1849                assert_eq!(spine_item.properties, None);
1850                assert_eq!(spine_item.linear, false);
1851            }
1852
1853            #[test]
1854            fn test_spine_item_build() {
1855                let mut spine_item = SpineItem::new("content_001");
1856                spine_item
1857                    .with_id("spine1")
1858                    .append_property("page-spread-left")
1859                    .set_linear(false);
1860
1861                let built = spine_item.build();
1862
1863                assert_eq!(built.idref, "content_001");
1864                assert_eq!(built.id, Some("spine1".to_string()));
1865                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1866                assert_eq!(built.linear, false);
1867            }
1868
1869            #[test]
1870            fn test_spine_item_builder_chaining() {
1871                let mut spine_item = SpineItem::new("content_001");
1872                spine_item
1873                    .with_id("spine1")
1874                    .append_property("page-spread-left")
1875                    .set_linear(false);
1876
1877                let built = spine_item.build();
1878
1879                assert_eq!(built.idref, "content_001");
1880                assert_eq!(built.id, Some("spine1".to_string()));
1881                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1882                assert_eq!(built.linear, false);
1883            }
1884
1885            #[test]
1886            fn test_spine_item_attributes() {
1887                let mut spine_item = SpineItem::new("content_001");
1888                spine_item
1889                    .with_id("spine1")
1890                    .append_property("page-spread-left")
1891                    .set_linear(false);
1892
1893                let attributes = spine_item.attributes();
1894
1895                // Check that all expected attributes are present
1896                assert!(attributes.contains(&("idref", "content_001")));
1897                assert!(attributes.contains(&("id", "spine1")));
1898                assert!(attributes.contains(&("properties", "page-spread-left")));
1899                assert!(attributes.contains(&("linear", "no"))); // false should become "no"
1900            }
1901
1902            #[test]
1903            fn test_spine_item_attributes_linear_yes() {
1904                let spine_item = SpineItem::new("content_001");
1905                let attributes = spine_item.attributes();
1906
1907                // Linear true should become "yes"
1908                assert!(attributes.contains(&("linear", "yes")));
1909            }
1910
1911            #[test]
1912            fn test_spine_item_attributes_optional_fields() {
1913                let spine_item = SpineItem::new("content_001");
1914                let attributes = spine_item.attributes();
1915
1916                // Should only contain required attributes when optional fields are None
1917                assert!(attributes.contains(&("idref", "content_001")));
1918                assert!(attributes.contains(&("linear", "yes")));
1919
1920                // Should not contain optional attributes when they are None
1921                assert!(!attributes.iter().any(|(k, _)| k == &"id"));
1922                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1923            }
1924        }
1925
1926        mod navpoint {
1927
1928            use std::path::PathBuf;
1929
1930            use crate::types::NavPoint;
1931
1932            #[test]
1933            fn test_navpoint_new() {
1934                let navpoint = NavPoint::new("Test Chapter");
1935
1936                assert_eq!(navpoint.label, "Test Chapter");
1937                assert_eq!(navpoint.content, None);
1938                assert_eq!(navpoint.children.len(), 0);
1939            }
1940
1941            #[test]
1942            fn test_navpoint_with_content() {
1943                let mut navpoint = NavPoint::new("Test Chapter");
1944                navpoint.with_content("chapter1.html");
1945
1946                assert_eq!(navpoint.label, "Test Chapter");
1947                assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
1948                assert_eq!(navpoint.children.len(), 0);
1949            }
1950
1951            #[test]
1952            fn test_navpoint_append_child() {
1953                let mut parent = NavPoint::new("Parent Chapter");
1954
1955                let mut child1 = NavPoint::new("Child Section 1");
1956                child1.with_content("section1.html");
1957
1958                let mut child2 = NavPoint::new("Child Section 2");
1959                child2.with_content("section2.html");
1960
1961                parent.append_child(child1.build());
1962                parent.append_child(child2.build());
1963
1964                assert_eq!(parent.children.len(), 2);
1965                assert_eq!(parent.children[0].label, "Child Section 1");
1966                assert_eq!(parent.children[1].label, "Child Section 2");
1967            }
1968
1969            #[test]
1970            fn test_navpoint_set_children() {
1971                let mut navpoint = NavPoint::new("Main Chapter");
1972                let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
1973
1974                navpoint.set_children(children);
1975
1976                assert_eq!(navpoint.children.len(), 2);
1977                assert_eq!(navpoint.children[0].label, "Section 1");
1978                assert_eq!(navpoint.children[1].label, "Section 2");
1979            }
1980
1981            #[test]
1982            fn test_navpoint_build() {
1983                let mut navpoint = NavPoint::new("Complete Chapter");
1984                navpoint.with_content("complete.html");
1985
1986                let child = NavPoint::new("Sub Section");
1987                navpoint.append_child(child.build());
1988
1989                let built = navpoint.build();
1990
1991                assert_eq!(built.label, "Complete Chapter");
1992                assert_eq!(built.content, Some(PathBuf::from("complete.html")));
1993                assert_eq!(built.children.len(), 1);
1994                assert_eq!(built.children[0].label, "Sub Section");
1995            }
1996
1997            #[test]
1998            fn test_navpoint_builder_chaining() {
1999                let mut navpoint = NavPoint::new("Chained Chapter");
2000
2001                navpoint
2002                    .with_content("chained.html")
2003                    .append_child(NavPoint::new("Child 1").build())
2004                    .append_child(NavPoint::new("Child 2").build());
2005
2006                let built = navpoint.build();
2007
2008                assert_eq!(built.label, "Chained Chapter");
2009                assert_eq!(built.content, Some(PathBuf::from("chained.html")));
2010                assert_eq!(built.children.len(), 2);
2011            }
2012
2013            #[test]
2014            fn test_navpoint_empty_children() {
2015                let navpoint = NavPoint::new("No Children Chapter");
2016                let built = navpoint.build();
2017
2018                assert_eq!(built.children.len(), 0);
2019            }
2020
2021            #[test]
2022            fn test_navpoint_complex_hierarchy() {
2023                let mut root = NavPoint::new("Book");
2024
2025                let mut chapter1 = NavPoint::new("Chapter 1");
2026                chapter1
2027                    .with_content("chapter1.html")
2028                    .append_child(
2029                        NavPoint::new("Section 1.1")
2030                            .with_content("sec1_1.html")
2031                            .build(),
2032                    )
2033                    .append_child(
2034                        NavPoint::new("Section 1.2")
2035                            .with_content("sec1_2.html")
2036                            .build(),
2037                    );
2038
2039                let mut chapter2 = NavPoint::new("Chapter 2");
2040                chapter2.with_content("chapter2.html").append_child(
2041                    NavPoint::new("Section 2.1")
2042                        .with_content("sec2_1.html")
2043                        .build(),
2044                );
2045
2046                root.append_child(chapter1.build())
2047                    .append_child(chapter2.build());
2048
2049                let book = root.build();
2050
2051                assert_eq!(book.label, "Book");
2052                assert_eq!(book.children.len(), 2);
2053
2054                let ch1 = &book.children[0];
2055                assert_eq!(ch1.label, "Chapter 1");
2056                assert_eq!(ch1.children.len(), 2);
2057
2058                let ch2 = &book.children[1];
2059                assert_eq!(ch2.label, "Chapter 2");
2060                assert_eq!(ch2.children.len(), 1);
2061            }
2062        }
2063    }
2064
2065    #[cfg(feature = "content-builder")]
2066    mod footnote_tests {
2067        use crate::types::Footnote;
2068
2069        #[test]
2070        fn test_footnote_basic_creation() {
2071            let footnote = Footnote {
2072                locate: 100,
2073                content: "Sample footnote".to_string(),
2074            };
2075
2076            assert_eq!(footnote.locate, 100);
2077            assert_eq!(footnote.content, "Sample footnote");
2078        }
2079
2080        #[test]
2081        fn test_footnote_equality() {
2082            let footnote1 = Footnote {
2083                locate: 100,
2084                content: "First note".to_string(),
2085            };
2086
2087            let footnote2 = Footnote {
2088                locate: 100,
2089                content: "First note".to_string(),
2090            };
2091
2092            let footnote3 = Footnote {
2093                locate: 100,
2094                content: "Different note".to_string(),
2095            };
2096
2097            let footnote4 = Footnote {
2098                locate: 200,
2099                content: "First note".to_string(),
2100            };
2101
2102            assert_eq!(footnote1, footnote2);
2103            assert_ne!(footnote1, footnote3);
2104            assert_ne!(footnote1, footnote4);
2105        }
2106
2107        #[test]
2108        fn test_footnote_ordering() {
2109            let footnote1 = Footnote {
2110                locate: 100,
2111                content: "First".to_string(),
2112            };
2113
2114            let footnote2 = Footnote {
2115                locate: 200,
2116                content: "Second".to_string(),
2117            };
2118
2119            let footnote3 = Footnote {
2120                locate: 150,
2121                content: "Middle".to_string(),
2122            };
2123
2124            assert!(footnote1 < footnote2);
2125            assert!(footnote2 > footnote1);
2126            assert!(footnote1 < footnote3);
2127            assert!(footnote3 < footnote2);
2128            assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
2129        }
2130
2131        #[test]
2132        fn test_footnote_sorting() {
2133            let mut footnotes = vec![
2134                Footnote {
2135                    locate: 300,
2136                    content: "Third note".to_string(),
2137                },
2138                Footnote {
2139                    locate: 100,
2140                    content: "First note".to_string(),
2141                },
2142                Footnote {
2143                    locate: 200,
2144                    content: "Second note".to_string(),
2145                },
2146            ];
2147
2148            footnotes.sort();
2149
2150            assert_eq!(footnotes[0].locate, 100);
2151            assert_eq!(footnotes[1].locate, 200);
2152            assert_eq!(footnotes[2].locate, 300);
2153
2154            assert_eq!(footnotes[0].content, "First note");
2155            assert_eq!(footnotes[1].content, "Second note");
2156            assert_eq!(footnotes[2].content, "Third note");
2157        }
2158    }
2159
2160    #[cfg(feature = "content-builder")]
2161    mod block_type_tests {
2162        use crate::types::BlockType;
2163
2164        #[test]
2165        fn test_block_type_variants() {
2166            let _ = BlockType::Text;
2167            let _ = BlockType::Quote;
2168            let _ = BlockType::Title;
2169            let _ = BlockType::Image;
2170            let _ = BlockType::Audio;
2171            let _ = BlockType::Video;
2172            let _ = BlockType::MathML;
2173        }
2174
2175        #[test]
2176        fn test_block_type_debug() {
2177            let text = format!("{:?}", BlockType::Text);
2178            assert_eq!(text, "Text");
2179
2180            let quote = format!("{:?}", BlockType::Quote);
2181            assert_eq!(quote, "Quote");
2182
2183            let image = format!("{:?}", BlockType::Image);
2184            assert_eq!(image, "Image");
2185        }
2186    }
2187
2188    #[cfg(feature = "content-builder")]
2189    mod style_options_tests {
2190        use crate::types::{ColorScheme, PageLayout, StyleOptions, TextAlign, TextStyle};
2191
2192        #[test]
2193        fn test_style_options_default() {
2194            let options = StyleOptions::default();
2195
2196            assert_eq!(options.text.font_size, 1.0);
2197            assert_eq!(options.text.line_height, 1.6);
2198            assert_eq!(
2199                options.text.font_family,
2200                "-apple-system, Roboto, sans-serif"
2201            );
2202            assert_eq!(options.text.font_weight, "normal");
2203            assert_eq!(options.text.font_style, "normal");
2204            assert_eq!(options.text.letter_spacing, "normal");
2205            assert_eq!(options.text.text_indent, 2.0);
2206
2207            assert_eq!(options.color_scheme.background, "#FFFFFF");
2208            assert_eq!(options.color_scheme.text, "#000000");
2209            assert_eq!(options.color_scheme.link, "#6f6f6f");
2210
2211            assert_eq!(options.layout.margin, 20);
2212            assert_eq!(options.layout.text_align, TextAlign::Left);
2213            assert_eq!(options.layout.paragraph_spacing, 16);
2214        }
2215
2216        #[test]
2217        fn test_style_options_custom_values() {
2218            let text = TextStyle {
2219                font_size: 1.5,
2220                line_height: 2.0,
2221                font_family: "Georgia, serif".to_string(),
2222                font_weight: "bold".to_string(),
2223                font_style: "italic".to_string(),
2224                letter_spacing: "0.1em".to_string(),
2225                text_indent: 3.0,
2226            };
2227
2228            let color_scheme = ColorScheme {
2229                background: "#F0F0F0".to_string(),
2230                text: "#333333".to_string(),
2231                link: "#0066CC".to_string(),
2232            };
2233
2234            let layout = PageLayout {
2235                margin: 30,
2236                text_align: TextAlign::Center,
2237                paragraph_spacing: 20,
2238            };
2239
2240            let options = StyleOptions { text, color_scheme, layout };
2241
2242            assert_eq!(options.text.font_size, 1.5);
2243            assert_eq!(options.text.font_weight, "bold");
2244            assert_eq!(options.color_scheme.background, "#F0F0F0");
2245            assert_eq!(options.layout.text_align, TextAlign::Center);
2246        }
2247
2248        #[test]
2249        fn test_text_style_default() {
2250            let style = TextStyle::default();
2251
2252            assert_eq!(style.font_size, 1.0);
2253            assert_eq!(style.line_height, 1.6);
2254            assert_eq!(style.font_family, "-apple-system, Roboto, sans-serif");
2255            assert_eq!(style.font_weight, "normal");
2256            assert_eq!(style.font_style, "normal");
2257            assert_eq!(style.letter_spacing, "normal");
2258            assert_eq!(style.text_indent, 2.0);
2259        }
2260
2261        #[test]
2262        fn test_text_style_custom_values() {
2263            let style = TextStyle {
2264                font_size: 2.0,
2265                line_height: 1.8,
2266                font_family: "Times New Roman".to_string(),
2267                font_weight: "bold".to_string(),
2268                font_style: "italic".to_string(),
2269                letter_spacing: "0.05em".to_string(),
2270                text_indent: 0.0,
2271            };
2272
2273            assert_eq!(style.font_size, 2.0);
2274            assert_eq!(style.line_height, 1.8);
2275            assert_eq!(style.font_family, "Times New Roman");
2276            assert_eq!(style.font_weight, "bold");
2277            assert_eq!(style.font_style, "italic");
2278            assert_eq!(style.letter_spacing, "0.05em");
2279            assert_eq!(style.text_indent, 0.0);
2280        }
2281
2282        #[test]
2283        fn test_text_style_debug() {
2284            let style = TextStyle::default();
2285            let debug_str = format!("{:?}", style);
2286            assert!(debug_str.contains("TextStyle"));
2287            assert!(debug_str.contains("font_size"));
2288        }
2289
2290        #[test]
2291        fn test_color_scheme_default() {
2292            let scheme = ColorScheme::default();
2293
2294            assert_eq!(scheme.background, "#FFFFFF");
2295            assert_eq!(scheme.text, "#000000");
2296            assert_eq!(scheme.link, "#6f6f6f");
2297        }
2298
2299        #[test]
2300        fn test_color_scheme_custom_values() {
2301            let scheme = ColorScheme {
2302                background: "#000000".to_string(),
2303                text: "#FFFFFF".to_string(),
2304                link: "#00FF00".to_string(),
2305            };
2306
2307            assert_eq!(scheme.background, "#000000");
2308            assert_eq!(scheme.text, "#FFFFFF");
2309            assert_eq!(scheme.link, "#00FF00");
2310        }
2311
2312        #[test]
2313        fn test_color_scheme_debug() {
2314            let scheme = ColorScheme::default();
2315            let debug_str = format!("{:?}", scheme);
2316            assert!(debug_str.contains("ColorScheme"));
2317            assert!(debug_str.contains("background"));
2318        }
2319
2320        #[test]
2321        fn test_page_layout_default() {
2322            let layout = PageLayout::default();
2323
2324            assert_eq!(layout.margin, 20);
2325            assert_eq!(layout.text_align, TextAlign::Left);
2326            assert_eq!(layout.paragraph_spacing, 16);
2327        }
2328
2329        #[test]
2330        fn test_page_layout_custom_values() {
2331            let layout = PageLayout {
2332                margin: 40,
2333                text_align: TextAlign::Justify,
2334                paragraph_spacing: 24,
2335            };
2336
2337            assert_eq!(layout.margin, 40);
2338            assert_eq!(layout.text_align, TextAlign::Justify);
2339            assert_eq!(layout.paragraph_spacing, 24);
2340        }
2341
2342        #[test]
2343        fn test_page_layout_debug() {
2344            let layout = PageLayout::default();
2345            let debug_str = format!("{:?}", layout);
2346            assert!(debug_str.contains("PageLayout"));
2347            assert!(debug_str.contains("margin"));
2348        }
2349
2350        #[test]
2351        fn test_text_align_default() {
2352            let align = TextAlign::default();
2353            assert_eq!(align, TextAlign::Left);
2354        }
2355
2356        #[test]
2357        fn test_text_align_display() {
2358            assert_eq!(TextAlign::Left.to_string(), "left");
2359            assert_eq!(TextAlign::Right.to_string(), "right");
2360            assert_eq!(TextAlign::Justify.to_string(), "justify");
2361            assert_eq!(TextAlign::Center.to_string(), "center");
2362        }
2363
2364        #[test]
2365        fn test_text_align_all_variants() {
2366            let left = TextAlign::Left;
2367            let right = TextAlign::Right;
2368            let justify = TextAlign::Justify;
2369            let center = TextAlign::Center;
2370
2371            assert!(matches!(left, TextAlign::Left));
2372            assert!(matches!(right, TextAlign::Right));
2373            assert!(matches!(justify, TextAlign::Justify));
2374            assert!(matches!(center, TextAlign::Center));
2375        }
2376
2377        #[test]
2378        fn test_text_align_debug() {
2379            assert_eq!(format!("{:?}", TextAlign::Left), "Left");
2380            assert_eq!(format!("{:?}", TextAlign::Right), "Right");
2381            assert_eq!(format!("{:?}", TextAlign::Justify), "Justify");
2382            assert_eq!(format!("{:?}", TextAlign::Center), "Center");
2383        }
2384
2385        #[test]
2386        fn test_style_options_builder_new() {
2387            let options = StyleOptions::new();
2388            assert_eq!(options.text.font_size, 1.0);
2389            assert_eq!(options.color_scheme.background, "#FFFFFF");
2390            assert_eq!(options.layout.margin, 20);
2391        }
2392
2393        #[test]
2394        fn test_style_options_builder_with_text() {
2395            let mut options = StyleOptions::new();
2396            let text_style = TextStyle::new()
2397                .with_font_size(2.0)
2398                .with_font_weight("bold")
2399                .build();
2400            options.with_text(text_style);
2401
2402            assert_eq!(options.text.font_size, 2.0);
2403            assert_eq!(options.text.font_weight, "bold");
2404        }
2405
2406        #[test]
2407        fn test_style_options_builder_with_color_scheme() {
2408            let mut options = StyleOptions::new();
2409            let color = ColorScheme::new()
2410                .with_background("#000000")
2411                .with_text("#FFFFFF")
2412                .build();
2413            options.with_color_scheme(color);
2414
2415            assert_eq!(options.color_scheme.background, "#000000");
2416            assert_eq!(options.color_scheme.text, "#FFFFFF");
2417        }
2418
2419        #[test]
2420        fn test_style_options_builder_with_layout() {
2421            let mut options = StyleOptions::new();
2422            let layout = PageLayout::new()
2423                .with_margin(40)
2424                .with_text_align(TextAlign::Justify)
2425                .with_paragraph_spacing(24)
2426                .build();
2427            options.with_layout(layout);
2428
2429            assert_eq!(options.layout.margin, 40);
2430            assert_eq!(options.layout.text_align, TextAlign::Justify);
2431            assert_eq!(options.layout.paragraph_spacing, 24);
2432        }
2433
2434        #[test]
2435        fn test_style_options_builder_build() {
2436            let options = StyleOptions::new()
2437                .with_text(TextStyle::new().with_font_size(1.5).build())
2438                .with_color_scheme(ColorScheme::new().with_link("#FF0000").build())
2439                .with_layout(PageLayout::new().with_margin(30).build())
2440                .build();
2441
2442            assert_eq!(options.text.font_size, 1.5);
2443            assert_eq!(options.color_scheme.link, "#FF0000");
2444            assert_eq!(options.layout.margin, 30);
2445        }
2446
2447        #[test]
2448        fn test_style_options_builder_chaining() {
2449            let options = StyleOptions::new()
2450                .with_text(
2451                    TextStyle::new()
2452                        .with_font_size(1.5)
2453                        .with_line_height(2.0)
2454                        .with_font_family("Arial")
2455                        .with_font_weight("bold")
2456                        .with_font_style("italic")
2457                        .with_letter_spacing("0.1em")
2458                        .with_text_indent(1.5)
2459                        .build(),
2460                )
2461                .with_color_scheme(
2462                    ColorScheme::new()
2463                        .with_background("#CCCCCC")
2464                        .with_text("#111111")
2465                        .with_link("#0000FF")
2466                        .build(),
2467                )
2468                .with_layout(
2469                    PageLayout::new()
2470                        .with_margin(25)
2471                        .with_text_align(TextAlign::Right)
2472                        .with_paragraph_spacing(20)
2473                        .build(),
2474                )
2475                .build();
2476
2477            assert_eq!(options.text.font_size, 1.5);
2478            assert_eq!(options.text.line_height, 2.0);
2479            assert_eq!(options.text.font_family, "Arial");
2480            assert_eq!(options.text.font_weight, "bold");
2481            assert_eq!(options.text.font_style, "italic");
2482            assert_eq!(options.text.letter_spacing, "0.1em");
2483            assert_eq!(options.text.text_indent, 1.5);
2484
2485            assert_eq!(options.color_scheme.background, "#CCCCCC");
2486            assert_eq!(options.color_scheme.text, "#111111");
2487            assert_eq!(options.color_scheme.link, "#0000FF");
2488
2489            assert_eq!(options.layout.margin, 25);
2490            assert_eq!(options.layout.text_align, TextAlign::Right);
2491            assert_eq!(options.layout.paragraph_spacing, 20);
2492        }
2493
2494        #[test]
2495        fn test_text_style_builder_new() {
2496            let style = TextStyle::new();
2497            assert_eq!(style.font_size, 1.0);
2498            assert_eq!(style.line_height, 1.6);
2499        }
2500
2501        #[test]
2502        fn test_text_style_builder_with_font_size() {
2503            let mut style = TextStyle::new();
2504            style.with_font_size(2.5);
2505            assert_eq!(style.font_size, 2.5);
2506        }
2507
2508        #[test]
2509        fn test_text_style_builder_with_line_height() {
2510            let mut style = TextStyle::new();
2511            style.with_line_height(2.0);
2512            assert_eq!(style.line_height, 2.0);
2513        }
2514
2515        #[test]
2516        fn test_text_style_builder_with_font_family() {
2517            let mut style = TextStyle::new();
2518            style.with_font_family("Helvetica, Arial");
2519            assert_eq!(style.font_family, "Helvetica, Arial");
2520        }
2521
2522        #[test]
2523        fn test_text_style_builder_with_font_weight() {
2524            let mut style = TextStyle::new();
2525            style.with_font_weight("bold");
2526            assert_eq!(style.font_weight, "bold");
2527        }
2528
2529        #[test]
2530        fn test_text_style_builder_with_font_style() {
2531            let mut style = TextStyle::new();
2532            style.with_font_style("italic");
2533            assert_eq!(style.font_style, "italic");
2534        }
2535
2536        #[test]
2537        fn test_text_style_builder_with_letter_spacing() {
2538            let mut style = TextStyle::new();
2539            style.with_letter_spacing("0.05em");
2540            assert_eq!(style.letter_spacing, "0.05em");
2541        }
2542
2543        #[test]
2544        fn test_text_style_builder_with_text_indent() {
2545            let mut style = TextStyle::new();
2546            style.with_text_indent(3.0);
2547            assert_eq!(style.text_indent, 3.0);
2548        }
2549
2550        #[test]
2551        fn test_text_style_builder_build() {
2552            let style = TextStyle::new()
2553                .with_font_size(1.8)
2554                .with_line_height(1.9)
2555                .build();
2556
2557            assert_eq!(style.font_size, 1.8);
2558            assert_eq!(style.line_height, 1.9);
2559        }
2560
2561        #[test]
2562        fn test_text_style_builder_chaining() {
2563            let style = TextStyle::new()
2564                .with_font_size(2.0)
2565                .with_line_height(1.8)
2566                .with_font_family("Georgia")
2567                .with_font_weight("bold")
2568                .with_font_style("italic")
2569                .with_letter_spacing("0.1em")
2570                .with_text_indent(0.5)
2571                .build();
2572
2573            assert_eq!(style.font_size, 2.0);
2574            assert_eq!(style.line_height, 1.8);
2575            assert_eq!(style.font_family, "Georgia");
2576            assert_eq!(style.font_weight, "bold");
2577            assert_eq!(style.font_style, "italic");
2578            assert_eq!(style.letter_spacing, "0.1em");
2579            assert_eq!(style.text_indent, 0.5);
2580        }
2581
2582        #[test]
2583        fn test_color_scheme_builder_new() {
2584            let scheme = ColorScheme::new();
2585            assert_eq!(scheme.background, "#FFFFFF");
2586            assert_eq!(scheme.text, "#000000");
2587        }
2588
2589        #[test]
2590        fn test_color_scheme_builder_with_background() {
2591            let mut scheme = ColorScheme::new();
2592            scheme.with_background("#FF0000");
2593            assert_eq!(scheme.background, "#FF0000");
2594        }
2595
2596        #[test]
2597        fn test_color_scheme_builder_with_text() {
2598            let mut scheme = ColorScheme::new();
2599            scheme.with_text("#333333");
2600            assert_eq!(scheme.text, "#333333");
2601        }
2602
2603        #[test]
2604        fn test_color_scheme_builder_with_link() {
2605            let mut scheme = ColorScheme::new();
2606            scheme.with_link("#0000FF");
2607            assert_eq!(scheme.link, "#0000FF");
2608        }
2609
2610        #[test]
2611        fn test_color_scheme_builder_build() {
2612            let scheme = ColorScheme::new().with_background("#123456").build();
2613
2614            assert_eq!(scheme.background, "#123456");
2615            assert_eq!(scheme.text, "#000000");
2616        }
2617
2618        #[test]
2619        fn test_color_scheme_builder_chaining() {
2620            let scheme = ColorScheme::new()
2621                .with_background("#AABBCC")
2622                .with_text("#DDEEFF")
2623                .with_link("#112233")
2624                .build();
2625
2626            assert_eq!(scheme.background, "#AABBCC");
2627            assert_eq!(scheme.text, "#DDEEFF");
2628            assert_eq!(scheme.link, "#112233");
2629        }
2630
2631        #[test]
2632        fn test_page_layout_builder_new() {
2633            let layout = PageLayout::new();
2634            assert_eq!(layout.margin, 20);
2635            assert_eq!(layout.text_align, TextAlign::Left);
2636            assert_eq!(layout.paragraph_spacing, 16);
2637        }
2638
2639        #[test]
2640        fn test_page_layout_builder_with_margin() {
2641            let mut layout = PageLayout::new();
2642            layout.with_margin(50);
2643            assert_eq!(layout.margin, 50);
2644        }
2645
2646        #[test]
2647        fn test_page_layout_builder_with_text_align() {
2648            let mut layout = PageLayout::new();
2649            layout.with_text_align(TextAlign::Center);
2650            assert_eq!(layout.text_align, TextAlign::Center);
2651        }
2652
2653        #[test]
2654        fn test_page_layout_builder_with_paragraph_spacing() {
2655            let mut layout = PageLayout::new();
2656            layout.with_paragraph_spacing(30);
2657            assert_eq!(layout.paragraph_spacing, 30);
2658        }
2659
2660        #[test]
2661        fn test_page_layout_builder_build() {
2662            let layout = PageLayout::new().with_margin(35).build();
2663
2664            assert_eq!(layout.margin, 35);
2665            assert_eq!(layout.text_align, TextAlign::Left);
2666        }
2667
2668        #[test]
2669        fn test_page_layout_builder_chaining() {
2670            let layout = PageLayout::new()
2671                .with_margin(45)
2672                .with_text_align(TextAlign::Justify)
2673                .with_paragraph_spacing(28)
2674                .build();
2675
2676            assert_eq!(layout.margin, 45);
2677            assert_eq!(layout.text_align, TextAlign::Justify);
2678            assert_eq!(layout.paragraph_spacing, 28);
2679        }
2680
2681        #[test]
2682        fn test_page_layout_builder_all_text_align_variants() {
2683            let left = PageLayout::new().with_text_align(TextAlign::Left).build();
2684            assert_eq!(left.text_align, TextAlign::Left);
2685
2686            let right = PageLayout::new().with_text_align(TextAlign::Right).build();
2687            assert_eq!(right.text_align, TextAlign::Right);
2688
2689            let center = PageLayout::new().with_text_align(TextAlign::Center).build();
2690            assert_eq!(center.text_align, TextAlign::Center);
2691
2692            let justify = PageLayout::new()
2693                .with_text_align(TextAlign::Justify)
2694                .build();
2695            assert_eq!(justify.text_align, TextAlign::Justify);
2696        }
2697    }
2698}