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#[derive(Debug, Default)]
850pub struct StyleOptions {
851    /// Text styling configuration
852    pub text: TextStyle,
853
854    /// Color scheme configuration
855    ///
856    /// Defines the background, text, and link colors for the document.
857    pub color_scheme: ColorScheme,
858
859    /// Page layout configuration
860    ///
861    /// Controls margins, text alignment, and paragraph spacing.
862    pub layout: PageLayout,
863}
864
865/// Text styling configuration
866///
867/// Defines the visual appearance of text content in the document,
868/// including font properties, sizing, and spacing.
869#[derive(Debug)]
870pub struct TextStyle {
871    /// The base font size (default: 1.0, unit: rem)
872    ///
873    /// Relative to the root element, providing consistent sizing
874    /// across different viewing contexts.
875    pub font_size: f32,
876
877    /// The line height (default: 1.6, unit: em)
878    ///
879    /// Controls the vertical spacing between lines of text.
880    /// Values greater than 1.0 increase spacing, while values
881    /// less than 1.0 compress the text.
882    pub line_height: f32,
883
884    /// The font family stack (default: "-apple-system, Roboto, sans-serif")
885    ///
886    /// A comma-separated list of font families to use, with
887    /// fallback fonts specified for compatibility.
888    pub font_family: String,
889
890    /// The font weight (default: "normal")
891    ///
892    /// Controls the thickness of the font strokes. Common values
893    /// include "normal" and "bold".
894    pub font_weight: String,
895
896    /// The font style (default: "normal")
897    ///
898    /// Controls whether the font is normal, italic, or oblique.
899    /// Common values include "normal" and "italic".
900    pub font_style: String,
901
902    /// The letter spacing (default: "normal")
903    ///
904    /// Controls the space between characters. Common values
905    /// include "normal" or specific lengths like "0.05em".
906    pub letter_spacing: String,
907
908    /// The text indent for paragraphs (default: 2.0, unit: em)
909    ///
910    /// Controls the indentation of the first line of paragraphs.
911    /// A value of 2.0 means the first line is indented by 2 ems.
912    pub text_indent: f32,
913}
914
915impl Default for TextStyle {
916    fn default() -> Self {
917        Self {
918            font_size: 1.0,
919            line_height: 1.6,
920            font_family: "-apple-system, Roboto, sans-serif".to_string(),
921            font_weight: "normal".to_string(),
922            font_style: "normal".to_string(),
923            letter_spacing: "normal".to_string(),
924            text_indent: 2.0,
925        }
926    }
927}
928
929/// Color scheme configuration
930///
931/// Defines the color palette for the document, including background,
932/// text, and link colors.
933#[derive(Debug)]
934pub struct ColorScheme {
935    /// The background color (default: "#FFFFFF")
936    ///
937    /// The fill color for the document body. Specified as a hex color
938    /// string (e.g., "#FFFFFF" for white).
939    pub background: String,
940
941    /// The text color (default: "#000000")
942    ///
943    /// The primary color for text content. Specified as a hex color
944    /// string (e.g., "#000000" for black).
945    pub text: String,
946
947    /// The link color (default: "#6f6f6f")
948    ///
949    /// The color for hyperlinks in the document. Specified as a hex
950    /// color string (e.g., "#6f6f6f" for gray).
951    pub link: String,
952}
953
954impl Default for ColorScheme {
955    fn default() -> Self {
956        Self {
957            background: "#FFFFFF".to_string(),
958            text: "#000000".to_string(),
959            link: "#6f6f6f".to_string(),
960        }
961    }
962}
963
964/// Page layout configuration
965///
966/// Defines the layout properties for pages in the document, including
967/// margins, text alignment, and paragraph spacing.
968#[derive(Debug)]
969pub struct PageLayout {
970    /// The page margin (default: 20, unit: pixels)
971    ///
972    /// Controls the space around the content area on each page.
973    pub margin: usize,
974
975    /// The text alignment mode (default: TextAlign::Left)
976    ///
977    /// Controls how text is aligned within the content area.
978    pub text_align: TextAlign,
979
980    /// The spacing between paragraphs (default: 16, unit: pixels)
981    ///
982    /// Controls the vertical space between block-level elements.
983    pub paragraph_spacing: usize,
984}
985
986impl Default for PageLayout {
987    fn default() -> Self {
988        Self {
989            margin: 20,
990            text_align: Default::default(),
991            paragraph_spacing: 16,
992        }
993    }
994}
995
996/// Text alignment options
997///
998/// Defines the available text alignment modes for content in the document.
999#[derive(Debug, Default, PartialEq)]
1000pub enum TextAlign {
1001    /// Left-aligned text
1002    ///
1003    /// Text is aligned to the left margin, with the right edge ragged.
1004    #[default]
1005    Left,
1006
1007    /// Right-aligned text
1008    ///
1009    /// Text is aligned to the right margin, with the left edge ragged.
1010    Right,
1011
1012    /// Justified text
1013    ///
1014    /// Text is aligned to both margins by adjusting the spacing between
1015    /// words. The left and right edges are both straight.
1016    Justify,
1017
1018    /// Centered text
1019    ///
1020    /// Text is centered within the content area, with both edges ragged.
1021    Center,
1022}
1023
1024impl std::fmt::Display for TextAlign {
1025    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1026        match self {
1027            TextAlign::Left => write!(f, "left"),
1028            TextAlign::Right => write!(f, "right"),
1029            TextAlign::Justify => write!(f, "justify"),
1030            TextAlign::Center => write!(f, "center"),
1031        }
1032    }
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037    mod navpoint_tests {
1038        use std::path::PathBuf;
1039
1040        use crate::types::NavPoint;
1041
1042        /// Testing the equality comparison of NavPoint
1043        #[test]
1044        fn test_navpoint_partial_eq() {
1045            let nav1 = NavPoint {
1046                label: "Chapter 1".to_string(),
1047                content: Some(PathBuf::from("chapter1.html")),
1048                children: vec![],
1049                play_order: Some(1),
1050            };
1051
1052            let nav2 = NavPoint {
1053                label: "Chapter 1".to_string(),
1054                content: Some(PathBuf::from("chapter2.html")),
1055                children: vec![],
1056                play_order: Some(1),
1057            };
1058
1059            let nav3 = NavPoint {
1060                label: "Chapter 2".to_string(),
1061                content: Some(PathBuf::from("chapter1.html")),
1062                children: vec![],
1063                play_order: Some(2),
1064            };
1065
1066            assert_eq!(nav1, nav2); // Same play_order, different contents, should be equal
1067            assert_ne!(nav1, nav3); // Different play_order, Same contents, should be unequal
1068        }
1069
1070        /// Test NavPoint sorting comparison
1071        #[test]
1072        fn test_navpoint_ord() {
1073            let nav1 = NavPoint {
1074                label: "Chapter 1".to_string(),
1075                content: Some(PathBuf::from("chapter1.html")),
1076                children: vec![],
1077                play_order: Some(1),
1078            };
1079
1080            let nav2 = NavPoint {
1081                label: "Chapter 2".to_string(),
1082                content: Some(PathBuf::from("chapter2.html")),
1083                children: vec![],
1084                play_order: Some(2),
1085            };
1086
1087            let nav3 = NavPoint {
1088                label: "Chapter 3".to_string(),
1089                content: Some(PathBuf::from("chapter3.html")),
1090                children: vec![],
1091                play_order: Some(3),
1092            };
1093
1094            // Test function cmp
1095            assert!(nav1 < nav2);
1096            assert!(nav2 > nav1);
1097            assert!(nav1 == nav1);
1098
1099            // Test function partial_cmp
1100            assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
1101            assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
1102            assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
1103
1104            // Test function sort
1105            let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
1106            nav_points.sort();
1107            assert_eq!(nav_points, vec![nav1, nav2, nav3]);
1108        }
1109
1110        /// Test the case of None play_order
1111        #[test]
1112        fn test_navpoint_ord_with_none_play_order() {
1113            let nav_with_order = NavPoint {
1114                label: "Chapter 1".to_string(),
1115                content: Some(PathBuf::from("chapter1.html")),
1116                children: vec![],
1117                play_order: Some(1),
1118            };
1119
1120            let nav_without_order = NavPoint {
1121                label: "Preface".to_string(),
1122                content: Some(PathBuf::from("preface.html")),
1123                children: vec![],
1124                play_order: None,
1125            };
1126
1127            assert!(nav_without_order < nav_with_order);
1128            assert!(nav_with_order > nav_without_order);
1129
1130            let nav_without_order2 = NavPoint {
1131                label: "Introduction".to_string(),
1132                content: Some(PathBuf::from("intro.html")),
1133                children: vec![],
1134                play_order: None,
1135            };
1136
1137            assert!(nav_without_order == nav_without_order2);
1138        }
1139
1140        /// Test NavPoint containing child nodes
1141        #[test]
1142        fn test_navpoint_with_children() {
1143            let child1 = NavPoint {
1144                label: "Section 1.1".to_string(),
1145                content: Some(PathBuf::from("section1_1.html")),
1146                children: vec![],
1147                play_order: Some(1),
1148            };
1149
1150            let child2 = NavPoint {
1151                label: "Section 1.2".to_string(),
1152                content: Some(PathBuf::from("section1_2.html")),
1153                children: vec![],
1154                play_order: Some(2),
1155            };
1156
1157            let parent1 = NavPoint {
1158                label: "Chapter 1".to_string(),
1159                content: Some(PathBuf::from("chapter1.html")),
1160                children: vec![child1.clone(), child2.clone()],
1161                play_order: Some(1),
1162            };
1163
1164            let parent2 = NavPoint {
1165                label: "Chapter 1".to_string(),
1166                content: Some(PathBuf::from("chapter1.html")),
1167                children: vec![child1.clone(), child2.clone()],
1168                play_order: Some(1),
1169            };
1170
1171            assert!(parent1 == parent2);
1172
1173            let parent3 = NavPoint {
1174                label: "Chapter 2".to_string(),
1175                content: Some(PathBuf::from("chapter2.html")),
1176                children: vec![child1.clone(), child2.clone()],
1177                play_order: Some(2),
1178            };
1179
1180            assert!(parent1 != parent3);
1181            assert!(parent1 < parent3);
1182        }
1183
1184        /// Test the case where content is None
1185        #[test]
1186        fn test_navpoint_with_none_content() {
1187            let nav1 = NavPoint {
1188                label: "Chapter 1".to_string(),
1189                content: None,
1190                children: vec![],
1191                play_order: Some(1),
1192            };
1193
1194            let nav2 = NavPoint {
1195                label: "Chapter 1".to_string(),
1196                content: None,
1197                children: vec![],
1198                play_order: Some(1),
1199            };
1200
1201            assert!(nav1 == nav2);
1202        }
1203    }
1204
1205    #[cfg(feature = "builder")]
1206    mod builder_tests {
1207        mod metadata_item {
1208            use crate::types::{MetadataItem, MetadataRefinement};
1209
1210            #[test]
1211            fn test_metadata_item_new() {
1212                let metadata_item = MetadataItem::new("title", "EPUB Test Book");
1213
1214                assert_eq!(metadata_item.property, "title");
1215                assert_eq!(metadata_item.value, "EPUB Test Book");
1216                assert_eq!(metadata_item.id, None);
1217                assert_eq!(metadata_item.lang, None);
1218                assert_eq!(metadata_item.refined.len(), 0);
1219            }
1220
1221            #[test]
1222            fn test_metadata_item_with_id() {
1223                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1224                metadata_item.with_id("creator-1");
1225
1226                assert_eq!(metadata_item.property, "creator");
1227                assert_eq!(metadata_item.value, "John Doe");
1228                assert_eq!(metadata_item.id, Some("creator-1".to_string()));
1229                assert_eq!(metadata_item.lang, None);
1230                assert_eq!(metadata_item.refined.len(), 0);
1231            }
1232
1233            #[test]
1234            fn test_metadata_item_with_lang() {
1235                let mut metadata_item = MetadataItem::new("title", "测试书籍");
1236                metadata_item.with_lang("zh-CN");
1237
1238                assert_eq!(metadata_item.property, "title");
1239                assert_eq!(metadata_item.value, "测试书籍");
1240                assert_eq!(metadata_item.id, None);
1241                assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
1242                assert_eq!(metadata_item.refined.len(), 0);
1243            }
1244
1245            #[test]
1246            fn test_metadata_item_append_refinement() {
1247                let mut metadata_item = MetadataItem::new("creator", "John Doe");
1248                metadata_item.with_id("creator-1"); // ID is required for refinements
1249
1250                let refinement = MetadataRefinement::new("creator-1", "role", "author");
1251                metadata_item.append_refinement(refinement);
1252
1253                assert_eq!(metadata_item.refined.len(), 1);
1254                assert_eq!(metadata_item.refined[0].refines, "creator-1");
1255                assert_eq!(metadata_item.refined[0].property, "role");
1256                assert_eq!(metadata_item.refined[0].value, "author");
1257            }
1258
1259            #[test]
1260            fn test_metadata_item_append_refinement_without_id() {
1261                let mut metadata_item = MetadataItem::new("title", "Test Book");
1262                // No ID set
1263
1264                let refinement = MetadataRefinement::new("title", "title-type", "main");
1265                metadata_item.append_refinement(refinement);
1266
1267                // Refinement should not be added because metadata item has no ID
1268                assert_eq!(metadata_item.refined.len(), 0);
1269            }
1270
1271            #[test]
1272            fn test_metadata_item_build() {
1273                let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
1274                metadata_item.with_id("pub-id").with_lang("en");
1275
1276                let built = metadata_item.build();
1277
1278                assert_eq!(built.property, "identifier");
1279                assert_eq!(built.value, "urn:isbn:1234567890");
1280                assert_eq!(built.id, Some("pub-id".to_string()));
1281                assert_eq!(built.lang, Some("en".to_string()));
1282                assert_eq!(built.refined.len(), 0);
1283            }
1284
1285            #[test]
1286            fn test_metadata_item_builder_chaining() {
1287                let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
1288                metadata_item.with_id("title").with_lang("en");
1289
1290                let refinement = MetadataRefinement::new("title", "title-type", "main");
1291                metadata_item.append_refinement(refinement);
1292
1293                let built = metadata_item.build();
1294
1295                assert_eq!(built.property, "title");
1296                assert_eq!(built.value, "EPUB 3.3 Guide");
1297                assert_eq!(built.id, Some("title".to_string()));
1298                assert_eq!(built.lang, Some("en".to_string()));
1299                assert_eq!(built.refined.len(), 1);
1300            }
1301
1302            #[test]
1303            fn test_metadata_item_attributes_dc_namespace() {
1304                let mut metadata_item = MetadataItem::new("title", "Test Book");
1305                metadata_item.with_id("title-id");
1306
1307                let attributes = metadata_item.attributes();
1308
1309                // For DC namespace properties, no "property" attribute should be added
1310                assert!(!attributes.iter().any(|(k, _)| k == &"property"));
1311                assert!(
1312                    attributes
1313                        .iter()
1314                        .any(|(k, v)| k == &"id" && v == &"title-id")
1315                );
1316            }
1317
1318            #[test]
1319            fn test_metadata_item_attributes_non_dc_namespace() {
1320                let mut metadata_item = MetadataItem::new("meta", "value");
1321                metadata_item.with_id("meta-id");
1322
1323                let attributes = metadata_item.attributes();
1324
1325                // For non-DC namespace properties, "property" attribute should be added
1326                assert!(attributes.iter().any(|(k, _)| k == &"property"));
1327                assert!(
1328                    attributes
1329                        .iter()
1330                        .any(|(k, v)| k == &"id" && v == &"meta-id")
1331                );
1332            }
1333
1334            #[test]
1335            fn test_metadata_item_attributes_with_lang() {
1336                let mut metadata_item = MetadataItem::new("title", "Test Book");
1337                metadata_item.with_id("title-id").with_lang("en");
1338
1339                let attributes = metadata_item.attributes();
1340
1341                assert!(
1342                    attributes
1343                        .iter()
1344                        .any(|(k, v)| k == &"id" && v == &"title-id")
1345                );
1346                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1347            }
1348        }
1349
1350        mod metadata_refinement {
1351            use crate::types::MetadataRefinement;
1352
1353            #[test]
1354            fn test_metadata_refinement_new() {
1355                let refinement = MetadataRefinement::new("title", "title-type", "main");
1356
1357                assert_eq!(refinement.refines, "title");
1358                assert_eq!(refinement.property, "title-type");
1359                assert_eq!(refinement.value, "main");
1360                assert_eq!(refinement.lang, None);
1361                assert_eq!(refinement.scheme, None);
1362            }
1363
1364            #[test]
1365            fn test_metadata_refinement_with_lang() {
1366                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1367                refinement.with_lang("en");
1368
1369                assert_eq!(refinement.refines, "creator");
1370                assert_eq!(refinement.property, "role");
1371                assert_eq!(refinement.value, "author");
1372                assert_eq!(refinement.lang, Some("en".to_string()));
1373                assert_eq!(refinement.scheme, None);
1374            }
1375
1376            #[test]
1377            fn test_metadata_refinement_with_scheme() {
1378                let mut refinement = MetadataRefinement::new("creator", "role", "author");
1379                refinement.with_scheme("marc:relators");
1380
1381                assert_eq!(refinement.refines, "creator");
1382                assert_eq!(refinement.property, "role");
1383                assert_eq!(refinement.value, "author");
1384                assert_eq!(refinement.lang, None);
1385                assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
1386            }
1387
1388            #[test]
1389            fn test_metadata_refinement_build() {
1390                let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
1391                refinement.with_lang("ja").with_scheme("iso-15924");
1392
1393                let built = refinement.build();
1394
1395                assert_eq!(built.refines, "title");
1396                assert_eq!(built.property, "alternate-script");
1397                assert_eq!(built.value, "テスト");
1398                assert_eq!(built.lang, Some("ja".to_string()));
1399                assert_eq!(built.scheme, Some("iso-15924".to_string()));
1400            }
1401
1402            #[test]
1403            fn test_metadata_refinement_builder_chaining() {
1404                let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
1405                refinement.with_lang("en").with_scheme("dcterms");
1406
1407                let built = refinement.build();
1408
1409                assert_eq!(built.refines, "creator");
1410                assert_eq!(built.property, "file-as");
1411                assert_eq!(built.value, "Doe, John");
1412                assert_eq!(built.lang, Some("en".to_string()));
1413                assert_eq!(built.scheme, Some("dcterms".to_string()));
1414            }
1415
1416            #[test]
1417            fn test_metadata_refinement_attributes() {
1418                let mut refinement = MetadataRefinement::new("title", "title-type", "main");
1419                refinement.with_lang("en").with_scheme("onix:codelist5");
1420
1421                let attributes = refinement.attributes();
1422
1423                assert!(
1424                    attributes
1425                        .iter()
1426                        .any(|(k, v)| k == &"refines" && v == &"title")
1427                );
1428                assert!(
1429                    attributes
1430                        .iter()
1431                        .any(|(k, v)| k == &"property" && v == &"title-type")
1432                );
1433                assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
1434                assert!(
1435                    attributes
1436                        .iter()
1437                        .any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
1438                );
1439            }
1440
1441            #[test]
1442            fn test_metadata_refinement_attributes_optional_fields() {
1443                let refinement = MetadataRefinement::new("creator", "role", "author");
1444                let attributes = refinement.attributes();
1445
1446                assert!(
1447                    attributes
1448                        .iter()
1449                        .any(|(k, v)| k == &"refines" && v == &"creator")
1450                );
1451                assert!(
1452                    attributes
1453                        .iter()
1454                        .any(|(k, v)| k == &"property" && v == &"role")
1455                );
1456
1457                // Should not contain optional attributes when they are None
1458                assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
1459                assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
1460            }
1461        }
1462
1463        mod manifest_item {
1464            use std::path::PathBuf;
1465
1466            use crate::types::ManifestItem;
1467
1468            #[test]
1469            fn test_manifest_item_new() {
1470                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1471                assert!(manifest_item.is_ok());
1472
1473                let manifest_item = manifest_item.unwrap();
1474                assert_eq!(manifest_item.id, "cover");
1475                assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
1476                assert_eq!(manifest_item.mime, "");
1477                assert_eq!(manifest_item.properties, None);
1478                assert_eq!(manifest_item.fallback, None);
1479            }
1480
1481            #[test]
1482            fn test_manifest_item_append_property() {
1483                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1484                assert!(manifest_item.is_ok());
1485
1486                let mut manifest_item = manifest_item.unwrap();
1487                manifest_item.append_property("nav");
1488
1489                assert_eq!(manifest_item.id, "nav");
1490                assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
1491                assert_eq!(manifest_item.mime, "");
1492                assert_eq!(manifest_item.properties, Some("nav".to_string()));
1493                assert_eq!(manifest_item.fallback, None);
1494            }
1495
1496            #[test]
1497            fn test_manifest_item_append_multiple_properties() {
1498                let manifest_item = ManifestItem::new("content", "content.xhtml");
1499                assert!(manifest_item.is_ok());
1500
1501                let mut manifest_item = manifest_item.unwrap();
1502                manifest_item
1503                    .append_property("nav")
1504                    .append_property("scripted")
1505                    .append_property("svg");
1506
1507                assert_eq!(
1508                    manifest_item.properties,
1509                    Some("nav scripted svg".to_string())
1510                );
1511            }
1512
1513            #[test]
1514            fn test_manifest_item_with_fallback() {
1515                let manifest_item = ManifestItem::new("image", "image.tiff");
1516                assert!(manifest_item.is_ok());
1517
1518                let mut manifest_item = manifest_item.unwrap();
1519                manifest_item.with_fallback("image-fallback");
1520
1521                assert_eq!(manifest_item.id, "image");
1522                assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
1523                assert_eq!(manifest_item.mime, "");
1524                assert_eq!(manifest_item.properties, None);
1525                assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
1526            }
1527
1528            #[test]
1529            fn test_manifest_item_set_mime() {
1530                let manifest_item = ManifestItem::new("style", "style.css");
1531                assert!(manifest_item.is_ok());
1532
1533                let manifest_item = manifest_item.unwrap();
1534                let updated_item = manifest_item.set_mime("text/css");
1535
1536                assert_eq!(updated_item.id, "style");
1537                assert_eq!(updated_item.path, PathBuf::from("style.css"));
1538                assert_eq!(updated_item.mime, "text/css");
1539                assert_eq!(updated_item.properties, None);
1540                assert_eq!(updated_item.fallback, None);
1541            }
1542
1543            #[test]
1544            fn test_manifest_item_build() {
1545                let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
1546                assert!(manifest_item.is_ok());
1547
1548                let mut manifest_item = manifest_item.unwrap();
1549                manifest_item
1550                    .append_property("cover-image")
1551                    .with_fallback("cover-fallback");
1552
1553                let built = manifest_item.build();
1554
1555                assert_eq!(built.id, "cover");
1556                assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
1557                assert_eq!(built.mime, "");
1558                assert_eq!(built.properties, Some("cover-image".to_string()));
1559                assert_eq!(built.fallback, Some("cover-fallback".to_string()));
1560            }
1561
1562            #[test]
1563            fn test_manifest_item_builder_chaining() {
1564                let manifest_item = ManifestItem::new("content", "content.xhtml");
1565                assert!(manifest_item.is_ok());
1566
1567                let mut manifest_item = manifest_item.unwrap();
1568                manifest_item
1569                    .append_property("scripted")
1570                    .append_property("svg")
1571                    .with_fallback("fallback-content");
1572
1573                let built = manifest_item.build();
1574
1575                assert_eq!(built.id, "content");
1576                assert_eq!(built.path, PathBuf::from("content.xhtml"));
1577                assert_eq!(built.mime, "");
1578                assert_eq!(built.properties, Some("scripted svg".to_string()));
1579                assert_eq!(built.fallback, Some("fallback-content".to_string()));
1580            }
1581
1582            #[test]
1583            fn test_manifest_item_attributes() {
1584                let manifest_item = ManifestItem::new("nav", "nav.xhtml");
1585                assert!(manifest_item.is_ok());
1586
1587                let mut manifest_item = manifest_item.unwrap();
1588                manifest_item
1589                    .append_property("nav")
1590                    .with_fallback("fallback-nav");
1591
1592                // Manually set mime type for testing
1593                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1594                let attributes = manifest_item.attributes();
1595
1596                // Check that all expected attributes are present
1597                assert!(attributes.contains(&("id", "nav")));
1598                assert!(attributes.contains(&("href", "nav.xhtml")));
1599                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1600                assert!(attributes.contains(&("properties", "nav")));
1601                assert!(attributes.contains(&("fallback", "fallback-nav")));
1602            }
1603
1604            #[test]
1605            fn test_manifest_item_attributes_optional_fields() {
1606                let manifest_item = ManifestItem::new("simple", "simple.xhtml");
1607                assert!(manifest_item.is_ok());
1608
1609                let manifest_item = manifest_item.unwrap();
1610                let manifest_item = manifest_item.set_mime("application/xhtml+xml");
1611                let attributes = manifest_item.attributes();
1612
1613                // Should contain required attributes
1614                assert!(attributes.contains(&("id", "simple")));
1615                assert!(attributes.contains(&("href", "simple.xhtml")));
1616                assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
1617
1618                // Should not contain optional attributes when they are None
1619                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1620                assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
1621            }
1622
1623            #[test]
1624            fn test_manifest_item_path_handling() {
1625                let manifest_item = ManifestItem::new("test", "../images/test.png");
1626                assert!(manifest_item.is_err());
1627
1628                let err = manifest_item.unwrap_err();
1629                assert_eq!(
1630                    err.to_string(),
1631                    "Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
1632                );
1633            }
1634        }
1635
1636        mod spine_item {
1637            use crate::types::SpineItem;
1638
1639            #[test]
1640            fn test_spine_item_new() {
1641                let spine_item = SpineItem::new("content_001");
1642
1643                assert_eq!(spine_item.idref, "content_001");
1644                assert_eq!(spine_item.id, None);
1645                assert_eq!(spine_item.properties, None);
1646                assert_eq!(spine_item.linear, true);
1647            }
1648
1649            #[test]
1650            fn test_spine_item_with_id() {
1651                let mut spine_item = SpineItem::new("content_001");
1652                spine_item.with_id("spine1");
1653
1654                assert_eq!(spine_item.idref, "content_001");
1655                assert_eq!(spine_item.id, Some("spine1".to_string()));
1656                assert_eq!(spine_item.properties, None);
1657                assert_eq!(spine_item.linear, true);
1658            }
1659
1660            #[test]
1661            fn test_spine_item_append_property() {
1662                let mut spine_item = SpineItem::new("content_001");
1663                spine_item.append_property("page-spread-left");
1664
1665                assert_eq!(spine_item.idref, "content_001");
1666                assert_eq!(spine_item.id, None);
1667                assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
1668                assert_eq!(spine_item.linear, true);
1669            }
1670
1671            #[test]
1672            fn test_spine_item_append_multiple_properties() {
1673                let mut spine_item = SpineItem::new("content_001");
1674                spine_item
1675                    .append_property("page-spread-left")
1676                    .append_property("rendition:layout-pre-paginated");
1677
1678                assert_eq!(
1679                    spine_item.properties,
1680                    Some("page-spread-left rendition:layout-pre-paginated".to_string())
1681                );
1682            }
1683
1684            #[test]
1685            fn test_spine_item_set_linear() {
1686                let mut spine_item = SpineItem::new("content_001");
1687                spine_item.set_linear(false);
1688
1689                assert_eq!(spine_item.idref, "content_001");
1690                assert_eq!(spine_item.id, None);
1691                assert_eq!(spine_item.properties, None);
1692                assert_eq!(spine_item.linear, false);
1693            }
1694
1695            #[test]
1696            fn test_spine_item_build() {
1697                let mut spine_item = SpineItem::new("content_001");
1698                spine_item
1699                    .with_id("spine1")
1700                    .append_property("page-spread-left")
1701                    .set_linear(false);
1702
1703                let built = spine_item.build();
1704
1705                assert_eq!(built.idref, "content_001");
1706                assert_eq!(built.id, Some("spine1".to_string()));
1707                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1708                assert_eq!(built.linear, false);
1709            }
1710
1711            #[test]
1712            fn test_spine_item_builder_chaining() {
1713                let mut spine_item = SpineItem::new("content_001");
1714                spine_item
1715                    .with_id("spine1")
1716                    .append_property("page-spread-left")
1717                    .set_linear(false);
1718
1719                let built = spine_item.build();
1720
1721                assert_eq!(built.idref, "content_001");
1722                assert_eq!(built.id, Some("spine1".to_string()));
1723                assert_eq!(built.properties, Some("page-spread-left".to_string()));
1724                assert_eq!(built.linear, false);
1725            }
1726
1727            #[test]
1728            fn test_spine_item_attributes() {
1729                let mut spine_item = SpineItem::new("content_001");
1730                spine_item
1731                    .with_id("spine1")
1732                    .append_property("page-spread-left")
1733                    .set_linear(false);
1734
1735                let attributes = spine_item.attributes();
1736
1737                // Check that all expected attributes are present
1738                assert!(attributes.contains(&("idref", "content_001")));
1739                assert!(attributes.contains(&("id", "spine1")));
1740                assert!(attributes.contains(&("properties", "page-spread-left")));
1741                assert!(attributes.contains(&("linear", "no"))); // false should become "no"
1742            }
1743
1744            #[test]
1745            fn test_spine_item_attributes_linear_yes() {
1746                let spine_item = SpineItem::new("content_001");
1747                let attributes = spine_item.attributes();
1748
1749                // Linear true should become "yes"
1750                assert!(attributes.contains(&("linear", "yes")));
1751            }
1752
1753            #[test]
1754            fn test_spine_item_attributes_optional_fields() {
1755                let spine_item = SpineItem::new("content_001");
1756                let attributes = spine_item.attributes();
1757
1758                // Should only contain required attributes when optional fields are None
1759                assert!(attributes.contains(&("idref", "content_001")));
1760                assert!(attributes.contains(&("linear", "yes")));
1761
1762                // Should not contain optional attributes when they are None
1763                assert!(!attributes.iter().any(|(k, _)| k == &"id"));
1764                assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
1765            }
1766        }
1767
1768        mod navpoint {
1769
1770            use std::path::PathBuf;
1771
1772            use crate::types::NavPoint;
1773
1774            #[test]
1775            fn test_navpoint_new() {
1776                let navpoint = NavPoint::new("Test Chapter");
1777
1778                assert_eq!(navpoint.label, "Test Chapter");
1779                assert_eq!(navpoint.content, None);
1780                assert_eq!(navpoint.children.len(), 0);
1781            }
1782
1783            #[test]
1784            fn test_navpoint_with_content() {
1785                let mut navpoint = NavPoint::new("Test Chapter");
1786                navpoint.with_content("chapter1.html");
1787
1788                assert_eq!(navpoint.label, "Test Chapter");
1789                assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
1790                assert_eq!(navpoint.children.len(), 0);
1791            }
1792
1793            #[test]
1794            fn test_navpoint_append_child() {
1795                let mut parent = NavPoint::new("Parent Chapter");
1796
1797                let mut child1 = NavPoint::new("Child Section 1");
1798                child1.with_content("section1.html");
1799
1800                let mut child2 = NavPoint::new("Child Section 2");
1801                child2.with_content("section2.html");
1802
1803                parent.append_child(child1.build());
1804                parent.append_child(child2.build());
1805
1806                assert_eq!(parent.children.len(), 2);
1807                assert_eq!(parent.children[0].label, "Child Section 1");
1808                assert_eq!(parent.children[1].label, "Child Section 2");
1809            }
1810
1811            #[test]
1812            fn test_navpoint_set_children() {
1813                let mut navpoint = NavPoint::new("Main Chapter");
1814                let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
1815
1816                navpoint.set_children(children);
1817
1818                assert_eq!(navpoint.children.len(), 2);
1819                assert_eq!(navpoint.children[0].label, "Section 1");
1820                assert_eq!(navpoint.children[1].label, "Section 2");
1821            }
1822
1823            #[test]
1824            fn test_navpoint_build() {
1825                let mut navpoint = NavPoint::new("Complete Chapter");
1826                navpoint.with_content("complete.html");
1827
1828                let child = NavPoint::new("Sub Section");
1829                navpoint.append_child(child.build());
1830
1831                let built = navpoint.build();
1832
1833                assert_eq!(built.label, "Complete Chapter");
1834                assert_eq!(built.content, Some(PathBuf::from("complete.html")));
1835                assert_eq!(built.children.len(), 1);
1836                assert_eq!(built.children[0].label, "Sub Section");
1837            }
1838
1839            #[test]
1840            fn test_navpoint_builder_chaining() {
1841                let mut navpoint = NavPoint::new("Chained Chapter");
1842
1843                navpoint
1844                    .with_content("chained.html")
1845                    .append_child(NavPoint::new("Child 1").build())
1846                    .append_child(NavPoint::new("Child 2").build());
1847
1848                let built = navpoint.build();
1849
1850                assert_eq!(built.label, "Chained Chapter");
1851                assert_eq!(built.content, Some(PathBuf::from("chained.html")));
1852                assert_eq!(built.children.len(), 2);
1853            }
1854
1855            #[test]
1856            fn test_navpoint_empty_children() {
1857                let navpoint = NavPoint::new("No Children Chapter");
1858                let built = navpoint.build();
1859
1860                assert_eq!(built.children.len(), 0);
1861            }
1862
1863            #[test]
1864            fn test_navpoint_complex_hierarchy() {
1865                let mut root = NavPoint::new("Book");
1866
1867                let mut chapter1 = NavPoint::new("Chapter 1");
1868                chapter1
1869                    .with_content("chapter1.html")
1870                    .append_child(
1871                        NavPoint::new("Section 1.1")
1872                            .with_content("sec1_1.html")
1873                            .build(),
1874                    )
1875                    .append_child(
1876                        NavPoint::new("Section 1.2")
1877                            .with_content("sec1_2.html")
1878                            .build(),
1879                    );
1880
1881                let mut chapter2 = NavPoint::new("Chapter 2");
1882                chapter2.with_content("chapter2.html").append_child(
1883                    NavPoint::new("Section 2.1")
1884                        .with_content("sec2_1.html")
1885                        .build(),
1886                );
1887
1888                root.append_child(chapter1.build())
1889                    .append_child(chapter2.build());
1890
1891                let book = root.build();
1892
1893                assert_eq!(book.label, "Book");
1894                assert_eq!(book.children.len(), 2);
1895
1896                let ch1 = &book.children[0];
1897                assert_eq!(ch1.label, "Chapter 1");
1898                assert_eq!(ch1.children.len(), 2);
1899
1900                let ch2 = &book.children[1];
1901                assert_eq!(ch2.label, "Chapter 2");
1902                assert_eq!(ch2.children.len(), 1);
1903            }
1904        }
1905    }
1906
1907    #[cfg(feature = "content_builder")]
1908    mod footnote_tests {
1909        use crate::types::Footnote;
1910
1911        #[test]
1912        fn test_footnote_basic_creation() {
1913            let footnote = Footnote {
1914                locate: 100,
1915                content: "Sample footnote".to_string(),
1916            };
1917
1918            assert_eq!(footnote.locate, 100);
1919            assert_eq!(footnote.content, "Sample footnote");
1920        }
1921
1922        #[test]
1923        fn test_footnote_equality() {
1924            let footnote1 = Footnote {
1925                locate: 100,
1926                content: "First note".to_string(),
1927            };
1928
1929            let footnote2 = Footnote {
1930                locate: 100,
1931                content: "First note".to_string(),
1932            };
1933
1934            let footnote3 = Footnote {
1935                locate: 100,
1936                content: "Different note".to_string(),
1937            };
1938
1939            let footnote4 = Footnote {
1940                locate: 200,
1941                content: "First note".to_string(),
1942            };
1943
1944            assert_eq!(footnote1, footnote2);
1945            assert_ne!(footnote1, footnote3);
1946            assert_ne!(footnote1, footnote4);
1947        }
1948
1949        #[test]
1950        fn test_footnote_ordering() {
1951            let footnote1 = Footnote {
1952                locate: 100,
1953                content: "First".to_string(),
1954            };
1955
1956            let footnote2 = Footnote {
1957                locate: 200,
1958                content: "Second".to_string(),
1959            };
1960
1961            let footnote3 = Footnote {
1962                locate: 150,
1963                content: "Middle".to_string(),
1964            };
1965
1966            assert!(footnote1 < footnote2);
1967            assert!(footnote2 > footnote1);
1968            assert!(footnote1 < footnote3);
1969            assert!(footnote3 < footnote2);
1970            assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
1971        }
1972
1973        #[test]
1974        fn test_footnote_sorting() {
1975            let mut footnotes = vec![
1976                Footnote {
1977                    locate: 300,
1978                    content: "Third note".to_string(),
1979                },
1980                Footnote {
1981                    locate: 100,
1982                    content: "First note".to_string(),
1983                },
1984                Footnote {
1985                    locate: 200,
1986                    content: "Second note".to_string(),
1987                },
1988            ];
1989
1990            footnotes.sort();
1991
1992            assert_eq!(footnotes[0].locate, 100);
1993            assert_eq!(footnotes[1].locate, 200);
1994            assert_eq!(footnotes[2].locate, 300);
1995
1996            assert_eq!(footnotes[0].content, "First note");
1997            assert_eq!(footnotes[1].content, "Second note");
1998            assert_eq!(footnotes[2].content, "Third note");
1999        }
2000    }
2001
2002    #[cfg(feature = "content_builder")]
2003    mod block_type_tests {
2004        use crate::types::BlockType;
2005
2006        #[test]
2007        fn test_block_type_variants() {
2008            let _ = BlockType::Text;
2009            let _ = BlockType::Quote;
2010            let _ = BlockType::Title;
2011            let _ = BlockType::Image;
2012            let _ = BlockType::Audio;
2013            let _ = BlockType::Video;
2014            let _ = BlockType::MathML;
2015        }
2016
2017        #[test]
2018        fn test_block_type_debug() {
2019            let text = format!("{:?}", BlockType::Text);
2020            assert_eq!(text, "Text");
2021
2022            let quote = format!("{:?}", BlockType::Quote);
2023            assert_eq!(quote, "Quote");
2024
2025            let image = format!("{:?}", BlockType::Image);
2026            assert_eq!(image, "Image");
2027        }
2028    }
2029
2030    mod style_options_tests {
2031        use crate::types::{ColorScheme, PageLayout, StyleOptions, TextAlign, TextStyle};
2032
2033        #[test]
2034        fn test_style_options_default() {
2035            let options = StyleOptions::default();
2036
2037            assert_eq!(options.text.font_size, 1.0);
2038            assert_eq!(options.text.line_height, 1.6);
2039            assert_eq!(
2040                options.text.font_family,
2041                "-apple-system, Roboto, sans-serif"
2042            );
2043            assert_eq!(options.text.font_weight, "normal");
2044            assert_eq!(options.text.font_style, "normal");
2045            assert_eq!(options.text.letter_spacing, "normal");
2046            assert_eq!(options.text.text_indent, 2.0);
2047
2048            assert_eq!(options.color_scheme.background, "#FFFFFF");
2049            assert_eq!(options.color_scheme.text, "#000000");
2050            assert_eq!(options.color_scheme.link, "#6f6f6f");
2051
2052            assert_eq!(options.layout.margin, 20);
2053            assert_eq!(options.layout.text_align, TextAlign::Left);
2054            assert_eq!(options.layout.paragraph_spacing, 16);
2055        }
2056
2057        #[test]
2058        fn test_style_options_custom_values() {
2059            let text = TextStyle {
2060                font_size: 1.5,
2061                line_height: 2.0,
2062                font_family: "Georgia, serif".to_string(),
2063                font_weight: "bold".to_string(),
2064                font_style: "italic".to_string(),
2065                letter_spacing: "0.1em".to_string(),
2066                text_indent: 3.0,
2067            };
2068
2069            let color_scheme = ColorScheme {
2070                background: "#F0F0F0".to_string(),
2071                text: "#333333".to_string(),
2072                link: "#0066CC".to_string(),
2073            };
2074
2075            let layout = PageLayout {
2076                margin: 30,
2077                text_align: TextAlign::Center,
2078                paragraph_spacing: 20,
2079            };
2080
2081            let options = StyleOptions { text, color_scheme, layout };
2082
2083            assert_eq!(options.text.font_size, 1.5);
2084            assert_eq!(options.text.font_weight, "bold");
2085            assert_eq!(options.color_scheme.background, "#F0F0F0");
2086            assert_eq!(options.layout.text_align, TextAlign::Center);
2087        }
2088
2089        #[test]
2090        fn test_text_style_default() {
2091            let style = TextStyle::default();
2092
2093            assert_eq!(style.font_size, 1.0);
2094            assert_eq!(style.line_height, 1.6);
2095            assert_eq!(style.font_family, "-apple-system, Roboto, sans-serif");
2096            assert_eq!(style.font_weight, "normal");
2097            assert_eq!(style.font_style, "normal");
2098            assert_eq!(style.letter_spacing, "normal");
2099            assert_eq!(style.text_indent, 2.0);
2100        }
2101
2102        #[test]
2103        fn test_text_style_custom_values() {
2104            let style = TextStyle {
2105                font_size: 2.0,
2106                line_height: 1.8,
2107                font_family: "Times New Roman".to_string(),
2108                font_weight: "bold".to_string(),
2109                font_style: "italic".to_string(),
2110                letter_spacing: "0.05em".to_string(),
2111                text_indent: 0.0,
2112            };
2113
2114            assert_eq!(style.font_size, 2.0);
2115            assert_eq!(style.line_height, 1.8);
2116            assert_eq!(style.font_family, "Times New Roman");
2117            assert_eq!(style.font_weight, "bold");
2118            assert_eq!(style.font_style, "italic");
2119            assert_eq!(style.letter_spacing, "0.05em");
2120            assert_eq!(style.text_indent, 0.0);
2121        }
2122
2123        #[test]
2124        fn test_text_style_debug() {
2125            let style = TextStyle::default();
2126            let debug_str = format!("{:?}", style);
2127            assert!(debug_str.contains("TextStyle"));
2128            assert!(debug_str.contains("font_size"));
2129        }
2130
2131        #[test]
2132        fn test_color_scheme_default() {
2133            let scheme = ColorScheme::default();
2134
2135            assert_eq!(scheme.background, "#FFFFFF");
2136            assert_eq!(scheme.text, "#000000");
2137            assert_eq!(scheme.link, "#6f6f6f");
2138        }
2139
2140        #[test]
2141        fn test_color_scheme_custom_values() {
2142            let scheme = ColorScheme {
2143                background: "#000000".to_string(),
2144                text: "#FFFFFF".to_string(),
2145                link: "#00FF00".to_string(),
2146            };
2147
2148            assert_eq!(scheme.background, "#000000");
2149            assert_eq!(scheme.text, "#FFFFFF");
2150            assert_eq!(scheme.link, "#00FF00");
2151        }
2152
2153        #[test]
2154        fn test_color_scheme_debug() {
2155            let scheme = ColorScheme::default();
2156            let debug_str = format!("{:?}", scheme);
2157            assert!(debug_str.contains("ColorScheme"));
2158            assert!(debug_str.contains("background"));
2159        }
2160
2161        #[test]
2162        fn test_page_layout_default() {
2163            let layout = PageLayout::default();
2164
2165            assert_eq!(layout.margin, 20);
2166            assert_eq!(layout.text_align, TextAlign::Left);
2167            assert_eq!(layout.paragraph_spacing, 16);
2168        }
2169
2170        #[test]
2171        fn test_page_layout_custom_values() {
2172            let layout = PageLayout {
2173                margin: 40,
2174                text_align: TextAlign::Justify,
2175                paragraph_spacing: 24,
2176            };
2177
2178            assert_eq!(layout.margin, 40);
2179            assert_eq!(layout.text_align, TextAlign::Justify);
2180            assert_eq!(layout.paragraph_spacing, 24);
2181        }
2182
2183        #[test]
2184        fn test_page_layout_debug() {
2185            let layout = PageLayout::default();
2186            let debug_str = format!("{:?}", layout);
2187            assert!(debug_str.contains("PageLayout"));
2188            assert!(debug_str.contains("margin"));
2189        }
2190
2191        #[test]
2192        fn test_text_align_default() {
2193            let align = TextAlign::default();
2194            assert_eq!(align, TextAlign::Left);
2195        }
2196
2197        #[test]
2198        fn test_text_align_display() {
2199            assert_eq!(TextAlign::Left.to_string(), "left");
2200            assert_eq!(TextAlign::Right.to_string(), "right");
2201            assert_eq!(TextAlign::Justify.to_string(), "justify");
2202            assert_eq!(TextAlign::Center.to_string(), "center");
2203        }
2204
2205        #[test]
2206        fn test_text_align_all_variants() {
2207            let left = TextAlign::Left;
2208            let right = TextAlign::Right;
2209            let justify = TextAlign::Justify;
2210            let center = TextAlign::Center;
2211
2212            assert!(matches!(left, TextAlign::Left));
2213            assert!(matches!(right, TextAlign::Right));
2214            assert!(matches!(justify, TextAlign::Justify));
2215            assert!(matches!(center, TextAlign::Center));
2216        }
2217
2218        #[test]
2219        fn test_text_align_debug() {
2220            assert_eq!(format!("{:?}", TextAlign::Left), "Left");
2221            assert_eq!(format!("{:?}", TextAlign::Right), "Right");
2222            assert_eq!(format!("{:?}", TextAlign::Justify), "Justify");
2223            assert_eq!(format!("{:?}", TextAlign::Center), "Center");
2224        }
2225    }
2226}