use std::{collections::HashMap, path::PathBuf};
#[cfg(feature = "builder")]
use crate::{
error::{EpubBuilderError, EpubError},
utils::ELEMENT_IN_DC_NAMESPACE,
};
#[derive(Debug, PartialEq, Eq)]
pub enum EpubVersion {
Version2_0,
Version3_0,
}
#[derive(Debug, Clone)]
pub struct MetadataItem {
pub id: Option<String>,
pub property: String,
pub value: String,
pub lang: Option<String>,
pub refined: Vec<MetadataRefinement>,
}
#[cfg(feature = "builder")]
impl MetadataItem {
pub fn new(property: &str, value: &str) -> Self {
Self {
id: None,
property: property.to_string(),
value: value.to_string(),
lang: None,
refined: vec![],
}
}
pub fn with_id(&mut self, id: &str) -> &mut Self {
self.id = Some(id.to_string());
self
}
pub fn with_lang(&mut self, lang: &str) -> &mut Self {
self.lang = Some(lang.to_string());
self
}
pub fn append_refinement(&mut self, refine: MetadataRefinement) -> &mut Self {
if self.id.is_some() {
self.refined.push(refine);
} else {
}
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
let mut attributes = Vec::new();
if !ELEMENT_IN_DC_NAMESPACE.contains(&self.property.as_str()) {
attributes.push(("property", self.property.as_str()));
}
if let Some(id) = &self.id {
attributes.push(("id", id.as_str()));
};
if let Some(lang) = &self.lang {
attributes.push(("lang", lang.as_str()));
};
attributes
}
}
#[derive(Debug, Clone)]
pub struct MetadataRefinement {
pub refines: String,
pub property: String,
pub value: String,
pub lang: Option<String>,
pub scheme: Option<String>,
}
#[cfg(feature = "builder")]
impl MetadataRefinement {
pub fn new(refines: &str, property: &str, value: &str) -> Self {
Self {
refines: refines.to_string(),
property: property.to_string(),
value: value.to_string(),
lang: None,
scheme: None,
}
}
pub fn with_lang(&mut self, lang: &str) -> &mut Self {
self.lang = Some(lang.to_string());
self
}
pub fn with_scheme(&mut self, scheme: &str) -> &mut Self {
self.scheme = Some(scheme.to_string());
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
let mut attributes = Vec::new();
attributes.push(("refines", self.refines.as_str()));
attributes.push(("property", self.property.as_str()));
if let Some(lang) = &self.lang {
attributes.push(("lang", lang.as_str()));
};
if let Some(scheme) = &self.scheme {
attributes.push(("scheme", scheme.as_str()));
};
attributes
}
}
#[derive(Debug)]
pub struct MetadataLinkItem {
pub href: String,
pub rel: String,
pub hreflang: Option<String>,
pub id: Option<String>,
pub mime: Option<String>,
pub properties: Option<String>,
pub refines: Option<String>,
}
#[derive(Debug, Default)]
pub struct MetadataSheet {
pub contributor: Vec<String>,
pub creator: Vec<String>,
pub date: HashMap<String, String>,
pub identifier: HashMap<String, String>,
pub language: Vec<String>,
pub relation: Vec<String>,
pub subject: Vec<String>,
pub title: Vec<String>,
pub coverage: String,
pub description: String,
pub format: String,
pub publisher: String,
pub rights: String,
pub source: String,
pub epub_type: String,
}
impl MetadataSheet {
pub fn new() -> Self {
Self {
contributor: Vec::new(),
creator: Vec::new(),
date: HashMap::new(),
identifier: HashMap::new(),
language: Vec::new(),
relation: Vec::new(),
subject: Vec::new(),
title: Vec::new(),
coverage: String::new(),
description: String::new(),
format: String::new(),
publisher: String::new(),
rights: String::new(),
source: String::new(),
epub_type: String::new(),
}
}
}
#[cfg(feature = "builder")]
impl MetadataSheet {
pub fn append_contributor(&mut self, contributor: impl Into<String>) -> &mut Self {
self.contributor.push(contributor.into());
self
}
pub fn append_creator(&mut self, creator: impl Into<String>) -> &mut Self {
self.creator.push(creator.into());
self
}
pub fn append_language(&mut self, language: impl Into<String>) -> &mut Self {
self.language.push(language.into());
self
}
pub fn append_relation(&mut self, relation: impl Into<String>) -> &mut Self {
self.relation.push(relation.into());
self
}
pub fn append_subject(&mut self, subject: impl Into<String>) -> &mut Self {
self.subject.push(subject.into());
self
}
pub fn append_title(&mut self, title: impl Into<String>) -> &mut Self {
self.title.push(title.into());
self
}
pub fn append_date(&mut self, date: impl Into<String>, event: impl Into<String>) -> &mut Self {
self.date.insert(date.into(), event.into());
self
}
pub fn append_identifier(
&mut self,
id: impl Into<String>,
value: impl Into<String>,
) -> &mut Self {
self.identifier.insert(id.into(), value.into());
self
}
pub fn with_coverage(&mut self, coverage: impl Into<String>) -> &mut Self {
self.coverage = coverage.into();
self
}
pub fn with_description(&mut self, description: impl Into<String>) -> &mut Self {
self.description = description.into();
self
}
pub fn with_format(&mut self, format: impl Into<String>) -> &mut Self {
self.format = format.into();
self
}
pub fn with_publisher(&mut self, publisher: impl Into<String>) -> &mut Self {
self.publisher = publisher.into();
self
}
pub fn with_rights(&mut self, rights: impl Into<String>) -> &mut Self {
self.rights = rights.into();
self
}
pub fn with_source(&mut self, source: impl Into<String>) -> &mut Self {
self.source = source.into();
self
}
pub fn with_epub_type(&mut self, epub_type: impl Into<String>) -> &mut Self {
self.epub_type = epub_type.into();
self
}
pub fn build(&self) -> MetadataSheet {
MetadataSheet {
contributor: self.contributor.clone(),
creator: self.creator.clone(),
date: self.date.clone(),
identifier: self.identifier.clone(),
language: self.language.clone(),
relation: self.relation.clone(),
subject: self.subject.clone(),
title: self.title.clone(),
coverage: self.coverage.clone(),
description: self.description.clone(),
format: self.format.clone(),
publisher: self.publisher.clone(),
rights: self.rights.clone(),
source: self.source.clone(),
epub_type: self.epub_type.clone(),
}
}
}
#[cfg(feature = "builder")]
impl From<MetadataSheet> for Vec<MetadataItem> {
fn from(sheet: MetadataSheet) -> Vec<MetadataItem> {
let mut items = Vec::new();
for title in &sheet.title {
items.push(MetadataItem::new("title", title));
}
for creator in &sheet.creator {
items.push(MetadataItem::new("creator", creator));
}
for contributor in &sheet.contributor {
items.push(MetadataItem::new("contributor", contributor));
}
for subject in &sheet.subject {
items.push(MetadataItem::new("subject", subject));
}
for language in &sheet.language {
items.push(MetadataItem::new("language", language));
}
for relation in &sheet.relation {
items.push(MetadataItem::new("relation", relation));
}
for (date, event) in &sheet.date {
let mut item = MetadataItem::new("date", date);
if !event.is_empty() {
let refinement_id = format!("date-{}", items.len());
item.id = Some(refinement_id.clone());
item.refined
.push(MetadataRefinement::new(&refinement_id, "event", event));
}
items.push(item);
}
for (id, value) in &sheet.identifier {
let mut item = MetadataItem::new("identifier", value);
if !id.is_empty() {
item.id = Some(id.clone());
}
items.push(item);
}
if !sheet.description.is_empty() {
items.push(MetadataItem::new("description", &sheet.description));
}
if !sheet.format.is_empty() {
items.push(MetadataItem::new("format", &sheet.format));
}
if !sheet.publisher.is_empty() {
items.push(MetadataItem::new("publisher", &sheet.publisher));
}
if !sheet.rights.is_empty() {
items.push(MetadataItem::new("rights", &sheet.rights));
}
if !sheet.source.is_empty() {
items.push(MetadataItem::new("source", &sheet.source));
}
if !sheet.coverage.is_empty() {
items.push(MetadataItem::new("coverage", &sheet.coverage));
}
if !sheet.epub_type.is_empty() {
items.push(MetadataItem::new("type", &sheet.epub_type));
}
items
}
}
#[derive(Debug, Clone)]
pub struct ManifestItem {
pub id: String,
pub path: PathBuf,
pub mime: String,
pub properties: Option<String>,
pub fallback: Option<String>,
}
#[cfg(feature = "builder")]
impl ManifestItem {
pub fn new(id: &str, path: &str) -> Result<Self, EpubError> {
if path.starts_with("../") {
return Err(
EpubBuilderError::IllegalManifestPath { manifest_id: id.to_string() }.into(),
);
}
Ok(Self {
id: id.to_string(),
path: PathBuf::from(path),
mime: String::new(),
properties: None,
fallback: None,
})
}
pub(crate) fn set_mime(self, mime: &str) -> Self {
Self {
id: self.id,
path: self.path,
mime: mime.to_string(),
properties: self.properties,
fallback: self.fallback,
}
}
pub fn append_property(&mut self, property: &str) -> &mut Self {
let new_properties = if let Some(properties) = &self.properties {
format!("{} {}", properties, property)
} else {
property.to_string()
};
self.properties = Some(new_properties);
self
}
pub fn with_fallback(&mut self, fallback: &str) -> &mut Self {
self.fallback = Some(fallback.to_string());
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
pub fn attributes(&self) -> Vec<(&str, &str)> {
let mut attributes = Vec::new();
attributes.push(("id", self.id.as_str()));
attributes.push(("href", self.path.to_str().unwrap()));
attributes.push(("media-type", self.mime.as_str()));
if let Some(properties) = &self.properties {
attributes.push(("properties", properties.as_str()));
}
if let Some(fallback) = &self.fallback {
attributes.push(("fallback", fallback.as_str()));
}
attributes
}
}
#[derive(Debug, Clone)]
pub struct SpineItem {
pub idref: String,
pub id: Option<String>,
pub properties: Option<String>,
pub linear: bool,
}
#[cfg(feature = "builder")]
impl SpineItem {
pub fn new(idref: &str) -> Self {
Self {
idref: idref.to_string(),
id: None,
properties: None,
linear: true,
}
}
pub fn with_id(&mut self, id: &str) -> &mut Self {
self.id = Some(id.to_string());
self
}
pub fn append_property(&mut self, property: &str) -> &mut Self {
let new_properties = if let Some(properties) = &self.properties {
format!("{} {}", properties, property)
} else {
property.to_string()
};
self.properties = Some(new_properties);
self
}
pub fn set_linear(&mut self, linear: bool) -> &mut Self {
self.linear = linear;
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
pub(crate) fn attributes(&self) -> Vec<(&str, &str)> {
let mut attributes = Vec::new();
attributes.push(("idref", self.idref.as_str()));
attributes.push(("linear", if self.linear { "yes" } else { "no" }));
if let Some(id) = &self.id {
attributes.push(("id", id.as_str()));
}
if let Some(properties) = &self.properties {
attributes.push(("properties", properties.as_str()));
}
attributes
}
}
#[derive(Debug, Clone)]
pub struct EncryptionData {
pub method: String,
pub data: String,
}
#[derive(Debug, Eq, Clone)]
pub struct NavPoint {
pub label: String,
pub content: Option<PathBuf>,
pub children: Vec<NavPoint>,
pub play_order: Option<usize>,
}
#[cfg(feature = "builder")]
impl NavPoint {
pub fn new(label: &str) -> Self {
Self {
label: label.to_string(),
content: None,
children: vec![],
play_order: None,
}
}
pub fn with_content(&mut self, content: &str) -> &mut Self {
self.content = Some(PathBuf::from(content));
self
}
pub fn append_child(&mut self, child: NavPoint) -> &mut Self {
self.children.push(child);
self
}
pub fn set_children(&mut self, children: Vec<NavPoint>) -> &mut Self {
self.children = children;
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
}
impl Ord for NavPoint {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.play_order.cmp(&other.play_order)
}
}
impl PartialOrd for NavPoint {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for NavPoint {
fn eq(&self, other: &Self) -> bool {
self.play_order == other.play_order
}
}
#[cfg(feature = "content-builder")]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Footnote {
pub locate: usize,
pub content: String,
}
#[cfg(feature = "content-builder")]
impl Ord for Footnote {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.locate.cmp(&other.locate)
}
}
#[cfg(feature = "content-builder")]
impl PartialOrd for Footnote {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[cfg(feature = "content-builder")]
#[derive(Debug, Copy, Clone)]
pub enum BlockType {
Text,
Quote,
Title,
Image,
Audio,
Video,
MathML,
}
#[cfg(feature = "content-builder")]
impl std::fmt::Display for BlockType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BlockType::Text => write!(f, "Text"),
BlockType::Quote => write!(f, "Quote"),
BlockType::Title => write!(f, "Title"),
BlockType::Image => write!(f, "Image"),
BlockType::Audio => write!(f, "Audio"),
BlockType::Video => write!(f, "Video"),
BlockType::MathML => write!(f, "MathML"),
}
}
}
#[cfg(feature = "content-builder")]
#[derive(Debug, Default, Clone)]
pub struct StyleOptions {
pub text: TextStyle,
pub color_scheme: ColorScheme,
pub layout: PageLayout,
}
#[cfg(feature = "content-builder")]
#[cfg(feature = "builder")]
impl StyleOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_text(&mut self, text: TextStyle) -> &mut Self {
self.text = text;
self
}
pub fn with_color_scheme(&mut self, color_scheme: ColorScheme) -> &mut Self {
self.color_scheme = color_scheme;
self
}
pub fn with_layout(&mut self, layout: PageLayout) -> &mut Self {
self.layout = layout;
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
}
#[cfg(feature = "content-builder")]
#[derive(Debug, Clone)]
pub struct TextStyle {
pub font_size: f32,
pub line_height: f32,
pub font_family: String,
pub font_weight: String,
pub font_style: String,
pub letter_spacing: String,
pub text_indent: f32,
}
#[cfg(feature = "content-builder")]
impl Default for TextStyle {
fn default() -> Self {
Self {
font_size: 1.0,
line_height: 1.6,
font_family: "-apple-system, Roboto, sans-serif".to_string(),
font_weight: "normal".to_string(),
font_style: "normal".to_string(),
letter_spacing: "normal".to_string(),
text_indent: 2.0,
}
}
}
#[cfg(feature = "content-builder")]
impl TextStyle {
pub fn new() -> Self {
Self::default()
}
pub fn with_font_size(&mut self, font_size: f32) -> &mut Self {
self.font_size = font_size;
self
}
pub fn with_line_height(&mut self, line_height: f32) -> &mut Self {
self.line_height = line_height;
self
}
pub fn with_font_family(&mut self, font_family: &str) -> &mut Self {
self.font_family = font_family.to_string();
self
}
pub fn with_font_weight(&mut self, font_weight: &str) -> &mut Self {
self.font_weight = font_weight.to_string();
self
}
pub fn with_font_style(&mut self, font_style: &str) -> &mut Self {
self.font_style = font_style.to_string();
self
}
pub fn with_letter_spacing(&mut self, letter_spacing: &str) -> &mut Self {
self.letter_spacing = letter_spacing.to_string();
self
}
pub fn with_text_indent(&mut self, text_indent: f32) -> &mut Self {
self.text_indent = text_indent;
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
}
#[cfg(feature = "content-builder")]
#[derive(Debug, Clone)]
pub struct ColorScheme {
pub background: String,
pub text: String,
pub link: String,
}
#[cfg(feature = "content-builder")]
impl Default for ColorScheme {
fn default() -> Self {
Self {
background: "#FFFFFF".to_string(),
text: "#000000".to_string(),
link: "#6f6f6f".to_string(),
}
}
}
#[cfg(feature = "content-builder")]
impl ColorScheme {
pub fn new() -> Self {
Self::default()
}
pub fn with_background(&mut self, background: &str) -> &mut Self {
self.background = background.to_string();
self
}
pub fn with_text(&mut self, text: &str) -> &mut Self {
self.text = text.to_string();
self
}
pub fn with_link(&mut self, link: &str) -> &mut Self {
self.link = link.to_string();
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
}
#[cfg(feature = "content-builder")]
#[derive(Debug, Clone)]
pub struct PageLayout {
pub margin: usize,
pub text_align: TextAlign,
pub paragraph_spacing: usize,
}
#[cfg(feature = "content-builder")]
impl Default for PageLayout {
fn default() -> Self {
Self {
margin: 20,
text_align: Default::default(),
paragraph_spacing: 16,
}
}
}
#[cfg(feature = "content-builder")]
impl PageLayout {
pub fn new() -> Self {
Self::default()
}
pub fn with_margin(&mut self, margin: usize) -> &mut Self {
self.margin = margin;
self
}
pub fn with_text_align(&mut self, text_align: TextAlign) -> &mut Self {
self.text_align = text_align;
self
}
pub fn with_paragraph_spacing(&mut self, paragraph_spacing: usize) -> &mut Self {
self.paragraph_spacing = paragraph_spacing;
self
}
pub fn build(&self) -> Self {
Self { ..self.clone() }
}
}
#[cfg(feature = "content-builder")]
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub enum TextAlign {
#[default]
Left,
Right,
Justify,
Center,
}
#[cfg(feature = "content-builder")]
impl std::fmt::Display for TextAlign {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TextAlign::Left => write!(f, "left"),
TextAlign::Right => write!(f, "right"),
TextAlign::Justify => write!(f, "justify"),
TextAlign::Center => write!(f, "center"),
}
}
}
#[cfg(test)]
mod tests {
mod navpoint_tests {
use std::path::PathBuf;
use crate::types::NavPoint;
#[test]
fn test_navpoint_partial_eq() {
let nav1 = NavPoint {
label: "Chapter 1".to_string(),
content: Some(PathBuf::from("chapter1.html")),
children: vec![],
play_order: Some(1),
};
let nav2 = NavPoint {
label: "Chapter 1".to_string(),
content: Some(PathBuf::from("chapter2.html")),
children: vec![],
play_order: Some(1),
};
let nav3 = NavPoint {
label: "Chapter 2".to_string(),
content: Some(PathBuf::from("chapter1.html")),
children: vec![],
play_order: Some(2),
};
assert_eq!(nav1, nav2); assert_ne!(nav1, nav3); }
#[test]
fn test_navpoint_ord() {
let nav1 = NavPoint {
label: "Chapter 1".to_string(),
content: Some(PathBuf::from("chapter1.html")),
children: vec![],
play_order: Some(1),
};
let nav2 = NavPoint {
label: "Chapter 2".to_string(),
content: Some(PathBuf::from("chapter2.html")),
children: vec![],
play_order: Some(2),
};
let nav3 = NavPoint {
label: "Chapter 3".to_string(),
content: Some(PathBuf::from("chapter3.html")),
children: vec![],
play_order: Some(3),
};
assert!(nav1 < nav2);
assert!(nav2 > nav1);
assert!(nav1 == nav1);
assert_eq!(nav1.partial_cmp(&nav2), Some(std::cmp::Ordering::Less));
assert_eq!(nav2.partial_cmp(&nav1), Some(std::cmp::Ordering::Greater));
assert_eq!(nav1.partial_cmp(&nav1), Some(std::cmp::Ordering::Equal));
let mut nav_points = vec![nav2.clone(), nav3.clone(), nav1.clone()];
nav_points.sort();
assert_eq!(nav_points, vec![nav1, nav2, nav3]);
}
#[test]
fn test_navpoint_ord_with_none_play_order() {
let nav_with_order = NavPoint {
label: "Chapter 1".to_string(),
content: Some(PathBuf::from("chapter1.html")),
children: vec![],
play_order: Some(1),
};
let nav_without_order = NavPoint {
label: "Preface".to_string(),
content: Some(PathBuf::from("preface.html")),
children: vec![],
play_order: None,
};
assert!(nav_without_order < nav_with_order);
assert!(nav_with_order > nav_without_order);
let nav_without_order2 = NavPoint {
label: "Introduction".to_string(),
content: Some(PathBuf::from("intro.html")),
children: vec![],
play_order: None,
};
assert!(nav_without_order == nav_without_order2);
}
#[test]
fn test_navpoint_with_children() {
let child1 = NavPoint {
label: "Section 1.1".to_string(),
content: Some(PathBuf::from("section1_1.html")),
children: vec![],
play_order: Some(1),
};
let child2 = NavPoint {
label: "Section 1.2".to_string(),
content: Some(PathBuf::from("section1_2.html")),
children: vec![],
play_order: Some(2),
};
let parent1 = NavPoint {
label: "Chapter 1".to_string(),
content: Some(PathBuf::from("chapter1.html")),
children: vec![child1.clone(), child2.clone()],
play_order: Some(1),
};
let parent2 = NavPoint {
label: "Chapter 1".to_string(),
content: Some(PathBuf::from("chapter1.html")),
children: vec![child1.clone(), child2.clone()],
play_order: Some(1),
};
assert!(parent1 == parent2);
let parent3 = NavPoint {
label: "Chapter 2".to_string(),
content: Some(PathBuf::from("chapter2.html")),
children: vec![child1.clone(), child2.clone()],
play_order: Some(2),
};
assert!(parent1 != parent3);
assert!(parent1 < parent3);
}
#[test]
fn test_navpoint_with_none_content() {
let nav1 = NavPoint {
label: "Chapter 1".to_string(),
content: None,
children: vec![],
play_order: Some(1),
};
let nav2 = NavPoint {
label: "Chapter 1".to_string(),
content: None,
children: vec![],
play_order: Some(1),
};
assert!(nav1 == nav2);
}
}
#[cfg(feature = "builder")]
mod builder_tests {
mod metadata_item {
use crate::types::{MetadataItem, MetadataRefinement};
#[test]
fn test_metadata_item_new() {
let metadata_item = MetadataItem::new("title", "EPUB Test Book");
assert_eq!(metadata_item.property, "title");
assert_eq!(metadata_item.value, "EPUB Test Book");
assert_eq!(metadata_item.id, None);
assert_eq!(metadata_item.lang, None);
assert_eq!(metadata_item.refined.len(), 0);
}
#[test]
fn test_metadata_item_with_id() {
let mut metadata_item = MetadataItem::new("creator", "John Doe");
metadata_item.with_id("creator-1");
assert_eq!(metadata_item.property, "creator");
assert_eq!(metadata_item.value, "John Doe");
assert_eq!(metadata_item.id, Some("creator-1".to_string()));
assert_eq!(metadata_item.lang, None);
assert_eq!(metadata_item.refined.len(), 0);
}
#[test]
fn test_metadata_item_with_lang() {
let mut metadata_item = MetadataItem::new("title", "测试书籍");
metadata_item.with_lang("zh-CN");
assert_eq!(metadata_item.property, "title");
assert_eq!(metadata_item.value, "测试书籍");
assert_eq!(metadata_item.id, None);
assert_eq!(metadata_item.lang, Some("zh-CN".to_string()));
assert_eq!(metadata_item.refined.len(), 0);
}
#[test]
fn test_metadata_item_append_refinement() {
let mut metadata_item = MetadataItem::new("creator", "John Doe");
metadata_item.with_id("creator-1");
let refinement = MetadataRefinement::new("creator-1", "role", "author");
metadata_item.append_refinement(refinement);
assert_eq!(metadata_item.refined.len(), 1);
assert_eq!(metadata_item.refined[0].refines, "creator-1");
assert_eq!(metadata_item.refined[0].property, "role");
assert_eq!(metadata_item.refined[0].value, "author");
}
#[test]
fn test_metadata_item_append_refinement_without_id() {
let mut metadata_item = MetadataItem::new("title", "Test Book");
let refinement = MetadataRefinement::new("title", "title-type", "main");
metadata_item.append_refinement(refinement);
assert_eq!(metadata_item.refined.len(), 0);
}
#[test]
fn test_metadata_item_build() {
let mut metadata_item = MetadataItem::new("identifier", "urn:isbn:1234567890");
metadata_item.with_id("pub-id").with_lang("en");
let built = metadata_item.build();
assert_eq!(built.property, "identifier");
assert_eq!(built.value, "urn:isbn:1234567890");
assert_eq!(built.id, Some("pub-id".to_string()));
assert_eq!(built.lang, Some("en".to_string()));
assert_eq!(built.refined.len(), 0);
}
#[test]
fn test_metadata_item_builder_chaining() {
let mut metadata_item = MetadataItem::new("title", "EPUB 3.3 Guide");
metadata_item.with_id("title").with_lang("en");
let refinement = MetadataRefinement::new("title", "title-type", "main");
metadata_item.append_refinement(refinement);
let built = metadata_item.build();
assert_eq!(built.property, "title");
assert_eq!(built.value, "EPUB 3.3 Guide");
assert_eq!(built.id, Some("title".to_string()));
assert_eq!(built.lang, Some("en".to_string()));
assert_eq!(built.refined.len(), 1);
}
#[test]
fn test_metadata_item_attributes_dc_namespace() {
let mut metadata_item = MetadataItem::new("title", "Test Book");
metadata_item.with_id("title-id");
let attributes = metadata_item.attributes();
assert!(!attributes.iter().any(|(k, _)| k == &"property"));
assert!(
attributes
.iter()
.any(|(k, v)| k == &"id" && v == &"title-id")
);
}
#[test]
fn test_metadata_item_attributes_non_dc_namespace() {
let mut metadata_item = MetadataItem::new("meta", "value");
metadata_item.with_id("meta-id");
let attributes = metadata_item.attributes();
assert!(attributes.iter().any(|(k, _)| k == &"property"));
assert!(
attributes
.iter()
.any(|(k, v)| k == &"id" && v == &"meta-id")
);
}
#[test]
fn test_metadata_item_attributes_with_lang() {
let mut metadata_item = MetadataItem::new("title", "Test Book");
metadata_item.with_id("title-id").with_lang("en");
let attributes = metadata_item.attributes();
assert!(
attributes
.iter()
.any(|(k, v)| k == &"id" && v == &"title-id")
);
assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
}
}
mod metadata_refinement {
use crate::types::MetadataRefinement;
#[test]
fn test_metadata_refinement_new() {
let refinement = MetadataRefinement::new("title", "title-type", "main");
assert_eq!(refinement.refines, "title");
assert_eq!(refinement.property, "title-type");
assert_eq!(refinement.value, "main");
assert_eq!(refinement.lang, None);
assert_eq!(refinement.scheme, None);
}
#[test]
fn test_metadata_refinement_with_lang() {
let mut refinement = MetadataRefinement::new("creator", "role", "author");
refinement.with_lang("en");
assert_eq!(refinement.refines, "creator");
assert_eq!(refinement.property, "role");
assert_eq!(refinement.value, "author");
assert_eq!(refinement.lang, Some("en".to_string()));
assert_eq!(refinement.scheme, None);
}
#[test]
fn test_metadata_refinement_with_scheme() {
let mut refinement = MetadataRefinement::new("creator", "role", "author");
refinement.with_scheme("marc:relators");
assert_eq!(refinement.refines, "creator");
assert_eq!(refinement.property, "role");
assert_eq!(refinement.value, "author");
assert_eq!(refinement.lang, None);
assert_eq!(refinement.scheme, Some("marc:relators".to_string()));
}
#[test]
fn test_metadata_refinement_build() {
let mut refinement = MetadataRefinement::new("title", "alternate-script", "テスト");
refinement.with_lang("ja").with_scheme("iso-15924");
let built = refinement.build();
assert_eq!(built.refines, "title");
assert_eq!(built.property, "alternate-script");
assert_eq!(built.value, "テスト");
assert_eq!(built.lang, Some("ja".to_string()));
assert_eq!(built.scheme, Some("iso-15924".to_string()));
}
#[test]
fn test_metadata_refinement_builder_chaining() {
let mut refinement = MetadataRefinement::new("creator", "file-as", "Doe, John");
refinement.with_lang("en").with_scheme("dcterms");
let built = refinement.build();
assert_eq!(built.refines, "creator");
assert_eq!(built.property, "file-as");
assert_eq!(built.value, "Doe, John");
assert_eq!(built.lang, Some("en".to_string()));
assert_eq!(built.scheme, Some("dcterms".to_string()));
}
#[test]
fn test_metadata_refinement_attributes() {
let mut refinement = MetadataRefinement::new("title", "title-type", "main");
refinement.with_lang("en").with_scheme("onix:codelist5");
let attributes = refinement.attributes();
assert!(
attributes
.iter()
.any(|(k, v)| k == &"refines" && v == &"title")
);
assert!(
attributes
.iter()
.any(|(k, v)| k == &"property" && v == &"title-type")
);
assert!(attributes.iter().any(|(k, v)| k == &"lang" && v == &"en"));
assert!(
attributes
.iter()
.any(|(k, v)| k == &"scheme" && v == &"onix:codelist5")
);
}
#[test]
fn test_metadata_refinement_attributes_optional_fields() {
let refinement = MetadataRefinement::new("creator", "role", "author");
let attributes = refinement.attributes();
assert!(
attributes
.iter()
.any(|(k, v)| k == &"refines" && v == &"creator")
);
assert!(
attributes
.iter()
.any(|(k, v)| k == &"property" && v == &"role")
);
assert!(!attributes.iter().any(|(k, _)| k == &"lang"));
assert!(!attributes.iter().any(|(k, _)| k == &"scheme"));
}
}
mod manifest_item {
use std::path::PathBuf;
use crate::types::ManifestItem;
#[test]
fn test_manifest_item_new() {
let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
assert!(manifest_item.is_ok());
let manifest_item = manifest_item.unwrap();
assert_eq!(manifest_item.id, "cover");
assert_eq!(manifest_item.path, PathBuf::from("images/cover.jpg"));
assert_eq!(manifest_item.mime, "");
assert_eq!(manifest_item.properties, None);
assert_eq!(manifest_item.fallback, None);
}
#[test]
fn test_manifest_item_append_property() {
let manifest_item = ManifestItem::new("nav", "nav.xhtml");
assert!(manifest_item.is_ok());
let mut manifest_item = manifest_item.unwrap();
manifest_item.append_property("nav");
assert_eq!(manifest_item.id, "nav");
assert_eq!(manifest_item.path, PathBuf::from("nav.xhtml"));
assert_eq!(manifest_item.mime, "");
assert_eq!(manifest_item.properties, Some("nav".to_string()));
assert_eq!(manifest_item.fallback, None);
}
#[test]
fn test_manifest_item_append_multiple_properties() {
let manifest_item = ManifestItem::new("content", "content.xhtml");
assert!(manifest_item.is_ok());
let mut manifest_item = manifest_item.unwrap();
manifest_item
.append_property("nav")
.append_property("scripted")
.append_property("svg");
assert_eq!(
manifest_item.properties,
Some("nav scripted svg".to_string())
);
}
#[test]
fn test_manifest_item_with_fallback() {
let manifest_item = ManifestItem::new("image", "image.tiff");
assert!(manifest_item.is_ok());
let mut manifest_item = manifest_item.unwrap();
manifest_item.with_fallback("image-fallback");
assert_eq!(manifest_item.id, "image");
assert_eq!(manifest_item.path, PathBuf::from("image.tiff"));
assert_eq!(manifest_item.mime, "");
assert_eq!(manifest_item.properties, None);
assert_eq!(manifest_item.fallback, Some("image-fallback".to_string()));
}
#[test]
fn test_manifest_item_set_mime() {
let manifest_item = ManifestItem::new("style", "style.css");
assert!(manifest_item.is_ok());
let manifest_item = manifest_item.unwrap();
let updated_item = manifest_item.set_mime("text/css");
assert_eq!(updated_item.id, "style");
assert_eq!(updated_item.path, PathBuf::from("style.css"));
assert_eq!(updated_item.mime, "text/css");
assert_eq!(updated_item.properties, None);
assert_eq!(updated_item.fallback, None);
}
#[test]
fn test_manifest_item_build() {
let manifest_item = ManifestItem::new("cover", "images/cover.jpg");
assert!(manifest_item.is_ok());
let mut manifest_item = manifest_item.unwrap();
manifest_item
.append_property("cover-image")
.with_fallback("cover-fallback");
let built = manifest_item.build();
assert_eq!(built.id, "cover");
assert_eq!(built.path, PathBuf::from("images/cover.jpg"));
assert_eq!(built.mime, "");
assert_eq!(built.properties, Some("cover-image".to_string()));
assert_eq!(built.fallback, Some("cover-fallback".to_string()));
}
#[test]
fn test_manifest_item_builder_chaining() {
let manifest_item = ManifestItem::new("content", "content.xhtml");
assert!(manifest_item.is_ok());
let mut manifest_item = manifest_item.unwrap();
manifest_item
.append_property("scripted")
.append_property("svg")
.with_fallback("fallback-content");
let built = manifest_item.build();
assert_eq!(built.id, "content");
assert_eq!(built.path, PathBuf::from("content.xhtml"));
assert_eq!(built.mime, "");
assert_eq!(built.properties, Some("scripted svg".to_string()));
assert_eq!(built.fallback, Some("fallback-content".to_string()));
}
#[test]
fn test_manifest_item_attributes() {
let manifest_item = ManifestItem::new("nav", "nav.xhtml");
assert!(manifest_item.is_ok());
let mut manifest_item = manifest_item.unwrap();
manifest_item
.append_property("nav")
.with_fallback("fallback-nav");
let manifest_item = manifest_item.set_mime("application/xhtml+xml");
let attributes = manifest_item.attributes();
assert!(attributes.contains(&("id", "nav")));
assert!(attributes.contains(&("href", "nav.xhtml")));
assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
assert!(attributes.contains(&("properties", "nav")));
assert!(attributes.contains(&("fallback", "fallback-nav")));
}
#[test]
fn test_manifest_item_attributes_optional_fields() {
let manifest_item = ManifestItem::new("simple", "simple.xhtml");
assert!(manifest_item.is_ok());
let manifest_item = manifest_item.unwrap();
let manifest_item = manifest_item.set_mime("application/xhtml+xml");
let attributes = manifest_item.attributes();
assert!(attributes.contains(&("id", "simple")));
assert!(attributes.contains(&("href", "simple.xhtml")));
assert!(attributes.contains(&("media-type", "application/xhtml+xml")));
assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
assert!(!attributes.iter().any(|(k, _)| k == &"fallback"));
}
#[test]
fn test_manifest_item_path_handling() {
let manifest_item = ManifestItem::new("test", "../images/test.png");
assert!(manifest_item.is_err());
let err = manifest_item.unwrap_err();
assert_eq!(
err.to_string(),
"Epub builder error: A manifest with id 'test' should not use a relative path starting with '../'."
);
}
}
mod spine_item {
use crate::types::SpineItem;
#[test]
fn test_spine_item_new() {
let spine_item = SpineItem::new("content_001");
assert_eq!(spine_item.idref, "content_001");
assert_eq!(spine_item.id, None);
assert_eq!(spine_item.properties, None);
assert_eq!(spine_item.linear, true);
}
#[test]
fn test_spine_item_with_id() {
let mut spine_item = SpineItem::new("content_001");
spine_item.with_id("spine1");
assert_eq!(spine_item.idref, "content_001");
assert_eq!(spine_item.id, Some("spine1".to_string()));
assert_eq!(spine_item.properties, None);
assert_eq!(spine_item.linear, true);
}
#[test]
fn test_spine_item_append_property() {
let mut spine_item = SpineItem::new("content_001");
spine_item.append_property("page-spread-left");
assert_eq!(spine_item.idref, "content_001");
assert_eq!(spine_item.id, None);
assert_eq!(spine_item.properties, Some("page-spread-left".to_string()));
assert_eq!(spine_item.linear, true);
}
#[test]
fn test_spine_item_append_multiple_properties() {
let mut spine_item = SpineItem::new("content_001");
spine_item
.append_property("page-spread-left")
.append_property("rendition:layout-pre-paginated");
assert_eq!(
spine_item.properties,
Some("page-spread-left rendition:layout-pre-paginated".to_string())
);
}
#[test]
fn test_spine_item_set_linear() {
let mut spine_item = SpineItem::new("content_001");
spine_item.set_linear(false);
assert_eq!(spine_item.idref, "content_001");
assert_eq!(spine_item.id, None);
assert_eq!(spine_item.properties, None);
assert_eq!(spine_item.linear, false);
}
#[test]
fn test_spine_item_build() {
let mut spine_item = SpineItem::new("content_001");
spine_item
.with_id("spine1")
.append_property("page-spread-left")
.set_linear(false);
let built = spine_item.build();
assert_eq!(built.idref, "content_001");
assert_eq!(built.id, Some("spine1".to_string()));
assert_eq!(built.properties, Some("page-spread-left".to_string()));
assert_eq!(built.linear, false);
}
#[test]
fn test_spine_item_builder_chaining() {
let mut spine_item = SpineItem::new("content_001");
spine_item
.with_id("spine1")
.append_property("page-spread-left")
.set_linear(false);
let built = spine_item.build();
assert_eq!(built.idref, "content_001");
assert_eq!(built.id, Some("spine1".to_string()));
assert_eq!(built.properties, Some("page-spread-left".to_string()));
assert_eq!(built.linear, false);
}
#[test]
fn test_spine_item_attributes() {
let mut spine_item = SpineItem::new("content_001");
spine_item
.with_id("spine1")
.append_property("page-spread-left")
.set_linear(false);
let attributes = spine_item.attributes();
assert!(attributes.contains(&("idref", "content_001")));
assert!(attributes.contains(&("id", "spine1")));
assert!(attributes.contains(&("properties", "page-spread-left")));
assert!(attributes.contains(&("linear", "no"))); }
#[test]
fn test_spine_item_attributes_linear_yes() {
let spine_item = SpineItem::new("content_001");
let attributes = spine_item.attributes();
assert!(attributes.contains(&("linear", "yes")));
}
#[test]
fn test_spine_item_attributes_optional_fields() {
let spine_item = SpineItem::new("content_001");
let attributes = spine_item.attributes();
assert!(attributes.contains(&("idref", "content_001")));
assert!(attributes.contains(&("linear", "yes")));
assert!(!attributes.iter().any(|(k, _)| k == &"id"));
assert!(!attributes.iter().any(|(k, _)| k == &"properties"));
}
}
mod metadata_sheet {
use crate::types::{MetadataItem, MetadataSheet};
#[test]
fn test_metadata_sheet_new() {
let sheet = MetadataSheet::new();
assert!(sheet.contributor.is_empty());
assert!(sheet.creator.is_empty());
assert!(sheet.date.is_empty());
assert!(sheet.identifier.is_empty());
assert!(sheet.language.is_empty());
assert!(sheet.relation.is_empty());
assert!(sheet.subject.is_empty());
assert!(sheet.title.is_empty());
assert!(sheet.coverage.is_empty());
assert!(sheet.description.is_empty());
assert!(sheet.format.is_empty());
assert!(sheet.publisher.is_empty());
assert!(sheet.rights.is_empty());
assert!(sheet.source.is_empty());
assert!(sheet.epub_type.is_empty());
}
#[test]
fn test_metadata_sheet_append_vec_fields() {
let mut sheet = MetadataSheet::new();
sheet
.append_title("Test Book")
.append_creator("John Doe")
.append_creator("Jane Smith")
.append_contributor("Editor One")
.append_language("en")
.append_language("zh-CN")
.append_subject("Fiction")
.append_subject("Drama")
.append_relation("prequel");
assert_eq!(sheet.title.len(), 1);
assert_eq!(sheet.title[0], "Test Book");
assert_eq!(sheet.creator.len(), 2);
assert_eq!(sheet.creator[0], "John Doe");
assert_eq!(sheet.creator[1], "Jane Smith");
assert_eq!(sheet.contributor.len(), 1);
assert_eq!(sheet.contributor[0], "Editor One");
assert_eq!(sheet.language.len(), 2);
assert_eq!(sheet.language[0], "en");
assert_eq!(sheet.language[1], "zh-CN");
assert_eq!(sheet.subject.len(), 2);
assert_eq!(sheet.subject[0], "Fiction");
assert_eq!(sheet.subject[1], "Drama");
assert_eq!(sheet.relation.len(), 1);
assert_eq!(sheet.relation[0], "prequel");
}
#[test]
fn test_metadata_sheet_append_date_and_identifier() {
let mut sheet = MetadataSheet::new();
sheet
.append_date("2024-01-15", "publication")
.append_date("2024-01-10", "creation")
.append_identifier("book-id", "urn:isbn:1234567890")
.append_identifier("uuid-id", "urn:uuid:12345678-1234-1234-1234-123456789012");
assert_eq!(sheet.date.len(), 2);
assert_eq!(
sheet.date.get("2024-01-15"),
Some(&"publication".to_string())
);
assert_eq!(sheet.date.get("2024-01-10"), Some(&"creation".to_string()));
assert_eq!(sheet.identifier.len(), 2);
assert_eq!(
sheet.identifier.get("book-id"),
Some(&"urn:isbn:1234567890".to_string())
);
assert_eq!(
sheet.identifier.get("uuid-id"),
Some(&"urn:uuid:12345678-1234-1234-1234-123456789012".to_string())
);
}
#[test]
fn test_metadata_sheet_with_string_fields() {
let mut sheet = MetadataSheet::new();
sheet
.with_coverage("Spatial coverage")
.with_description("A test book description")
.with_format("application/epub+zip")
.with_publisher("Test Publisher")
.with_rights("Copyright 2024")
.with_source("Original source")
.with_epub_type("buku");
assert_eq!(sheet.coverage, "Spatial coverage");
assert_eq!(sheet.description, "A test book description");
assert_eq!(sheet.format, "application/epub+zip");
assert_eq!(sheet.publisher, "Test Publisher");
assert_eq!(sheet.rights, "Copyright 2024");
assert_eq!(sheet.source, "Original source");
assert_eq!(sheet.epub_type, "buku");
}
#[test]
fn test_metadata_sheet_builder_chaining() {
let mut sheet = MetadataSheet::new();
sheet
.append_title("Chained Book")
.append_creator("Chained Author")
.append_date("2024-01-01", "")
.append_identifier("id-1", "test-id")
.with_publisher("Chained Publisher")
.with_description("Chained description");
assert_eq!(sheet.title.len(), 1);
assert_eq!(sheet.title[0], "Chained Book");
assert_eq!(sheet.creator.len(), 1);
assert_eq!(sheet.creator[0], "Chained Author");
assert_eq!(sheet.date.len(), 1);
assert_eq!(sheet.identifier.len(), 1);
assert_eq!(sheet.publisher, "Chained Publisher");
assert_eq!(sheet.description, "Chained description");
}
#[test]
fn test_metadata_sheet_build() {
let mut sheet = MetadataSheet::new();
sheet
.append_title("Original Title")
.with_publisher("Original Publisher");
let built = sheet.build();
assert_eq!(built.title.len(), 1);
assert_eq!(built.title[0], "Original Title");
assert_eq!(built.publisher, "Original Publisher");
sheet.append_title("New Title");
sheet.with_publisher("New Publisher");
assert_eq!(sheet.title.len(), 2);
assert_eq!(built.title.len(), 1);
assert_eq!(built.publisher, "Original Publisher");
}
#[test]
fn test_metadata_sheet_into_metadata_items() {
let mut sheet = MetadataSheet::new();
sheet
.append_title("Test Title")
.append_creator("Test Creator")
.with_description("Test Description")
.with_publisher("Test Publisher");
let items: Vec<MetadataItem> = sheet.into();
assert_eq!(items.len(), 4);
assert!(
items
.iter()
.any(|i| i.property == "title" && i.value == "Test Title")
);
assert!(
items
.iter()
.any(|i| i.property == "creator" && i.value == "Test Creator")
);
assert!(
items
.iter()
.any(|i| i.property == "description" && i.value == "Test Description")
);
assert!(
items
.iter()
.any(|i| i.property == "publisher" && i.value == "Test Publisher")
);
}
#[test]
fn test_metadata_sheet_into_metadata_items_with_date_and_identifier() {
let mut sheet = MetadataSheet::new();
sheet
.append_date("2024-01-15", "publication")
.append_identifier("book-id", "urn:isbn:9876543210");
let items: Vec<MetadataItem> = sheet.into();
assert_eq!(items.len(), 2);
let date_item = items.iter().find(|i| i.property == "date").unwrap();
assert_eq!(date_item.value, "2024-01-15");
assert!(date_item.id.is_some());
assert_eq!(date_item.refined.len(), 1);
assert_eq!(date_item.refined[0].property, "event");
assert_eq!(date_item.refined[0].value, "publication");
let id_item = items.iter().find(|i| i.property == "identifier").unwrap();
assert_eq!(id_item.value, "urn:isbn:9876543210");
assert_eq!(id_item.id, Some("book-id".to_string()));
}
#[test]
fn test_metadata_sheet_into_metadata_items_ignores_empty_fields() {
let mut sheet = MetadataSheet::new();
sheet.append_title("Valid Title").with_description("");
let items: Vec<MetadataItem> = sheet.into();
assert_eq!(items.len(), 1);
assert_eq!(items[0].property, "title");
}
}
mod navpoint {
use std::path::PathBuf;
use crate::types::NavPoint;
#[test]
fn test_navpoint_new() {
let navpoint = NavPoint::new("Test Chapter");
assert_eq!(navpoint.label, "Test Chapter");
assert_eq!(navpoint.content, None);
assert_eq!(navpoint.children.len(), 0);
}
#[test]
fn test_navpoint_with_content() {
let mut navpoint = NavPoint::new("Test Chapter");
navpoint.with_content("chapter1.html");
assert_eq!(navpoint.label, "Test Chapter");
assert_eq!(navpoint.content, Some(PathBuf::from("chapter1.html")));
assert_eq!(navpoint.children.len(), 0);
}
#[test]
fn test_navpoint_append_child() {
let mut parent = NavPoint::new("Parent Chapter");
let mut child1 = NavPoint::new("Child Section 1");
child1.with_content("section1.html");
let mut child2 = NavPoint::new("Child Section 2");
child2.with_content("section2.html");
parent.append_child(child1.build());
parent.append_child(child2.build());
assert_eq!(parent.children.len(), 2);
assert_eq!(parent.children[0].label, "Child Section 1");
assert_eq!(parent.children[1].label, "Child Section 2");
}
#[test]
fn test_navpoint_set_children() {
let mut navpoint = NavPoint::new("Main Chapter");
let children = vec![NavPoint::new("Section 1"), NavPoint::new("Section 2")];
navpoint.set_children(children);
assert_eq!(navpoint.children.len(), 2);
assert_eq!(navpoint.children[0].label, "Section 1");
assert_eq!(navpoint.children[1].label, "Section 2");
}
#[test]
fn test_navpoint_build() {
let mut navpoint = NavPoint::new("Complete Chapter");
navpoint.with_content("complete.html");
let child = NavPoint::new("Sub Section");
navpoint.append_child(child.build());
let built = navpoint.build();
assert_eq!(built.label, "Complete Chapter");
assert_eq!(built.content, Some(PathBuf::from("complete.html")));
assert_eq!(built.children.len(), 1);
assert_eq!(built.children[0].label, "Sub Section");
}
#[test]
fn test_navpoint_builder_chaining() {
let mut navpoint = NavPoint::new("Chained Chapter");
navpoint
.with_content("chained.html")
.append_child(NavPoint::new("Child 1").build())
.append_child(NavPoint::new("Child 2").build());
let built = navpoint.build();
assert_eq!(built.label, "Chained Chapter");
assert_eq!(built.content, Some(PathBuf::from("chained.html")));
assert_eq!(built.children.len(), 2);
}
#[test]
fn test_navpoint_empty_children() {
let navpoint = NavPoint::new("No Children Chapter");
let built = navpoint.build();
assert_eq!(built.children.len(), 0);
}
#[test]
fn test_navpoint_complex_hierarchy() {
let mut root = NavPoint::new("Book");
let mut chapter1 = NavPoint::new("Chapter 1");
chapter1
.with_content("chapter1.html")
.append_child(
NavPoint::new("Section 1.1")
.with_content("sec1_1.html")
.build(),
)
.append_child(
NavPoint::new("Section 1.2")
.with_content("sec1_2.html")
.build(),
);
let mut chapter2 = NavPoint::new("Chapter 2");
chapter2.with_content("chapter2.html").append_child(
NavPoint::new("Section 2.1")
.with_content("sec2_1.html")
.build(),
);
root.append_child(chapter1.build())
.append_child(chapter2.build());
let book = root.build();
assert_eq!(book.label, "Book");
assert_eq!(book.children.len(), 2);
let ch1 = &book.children[0];
assert_eq!(ch1.label, "Chapter 1");
assert_eq!(ch1.children.len(), 2);
let ch2 = &book.children[1];
assert_eq!(ch2.label, "Chapter 2");
assert_eq!(ch2.children.len(), 1);
}
}
}
#[cfg(feature = "content-builder")]
mod footnote_tests {
use crate::types::Footnote;
#[test]
fn test_footnote_basic_creation() {
let footnote = Footnote {
locate: 100,
content: "Sample footnote".to_string(),
};
assert_eq!(footnote.locate, 100);
assert_eq!(footnote.content, "Sample footnote");
}
#[test]
fn test_footnote_equality() {
let footnote1 = Footnote {
locate: 100,
content: "First note".to_string(),
};
let footnote2 = Footnote {
locate: 100,
content: "First note".to_string(),
};
let footnote3 = Footnote {
locate: 100,
content: "Different note".to_string(),
};
let footnote4 = Footnote {
locate: 200,
content: "First note".to_string(),
};
assert_eq!(footnote1, footnote2);
assert_ne!(footnote1, footnote3);
assert_ne!(footnote1, footnote4);
}
#[test]
fn test_footnote_ordering() {
let footnote1 = Footnote {
locate: 100,
content: "First".to_string(),
};
let footnote2 = Footnote {
locate: 200,
content: "Second".to_string(),
};
let footnote3 = Footnote {
locate: 150,
content: "Middle".to_string(),
};
assert!(footnote1 < footnote2);
assert!(footnote2 > footnote1);
assert!(footnote1 < footnote3);
assert!(footnote3 < footnote2);
assert_eq!(footnote1.cmp(&footnote1), std::cmp::Ordering::Equal);
}
#[test]
fn test_footnote_sorting() {
let mut footnotes = vec![
Footnote {
locate: 300,
content: "Third note".to_string(),
},
Footnote {
locate: 100,
content: "First note".to_string(),
},
Footnote {
locate: 200,
content: "Second note".to_string(),
},
];
footnotes.sort();
assert_eq!(footnotes[0].locate, 100);
assert_eq!(footnotes[1].locate, 200);
assert_eq!(footnotes[2].locate, 300);
assert_eq!(footnotes[0].content, "First note");
assert_eq!(footnotes[1].content, "Second note");
assert_eq!(footnotes[2].content, "Third note");
}
}
#[cfg(feature = "content-builder")]
mod block_type_tests {
use crate::types::BlockType;
#[test]
fn test_block_type_variants() {
let _ = BlockType::Text;
let _ = BlockType::Quote;
let _ = BlockType::Title;
let _ = BlockType::Image;
let _ = BlockType::Audio;
let _ = BlockType::Video;
let _ = BlockType::MathML;
}
#[test]
fn test_block_type_debug() {
let text = format!("{:?}", BlockType::Text);
assert_eq!(text, "Text");
let quote = format!("{:?}", BlockType::Quote);
assert_eq!(quote, "Quote");
let image = format!("{:?}", BlockType::Image);
assert_eq!(image, "Image");
}
}
#[cfg(feature = "content-builder")]
mod style_options_tests {
use crate::types::{ColorScheme, PageLayout, StyleOptions, TextAlign, TextStyle};
#[test]
fn test_style_options_default() {
let options = StyleOptions::default();
assert_eq!(options.text.font_size, 1.0);
assert_eq!(options.text.line_height, 1.6);
assert_eq!(
options.text.font_family,
"-apple-system, Roboto, sans-serif"
);
assert_eq!(options.text.font_weight, "normal");
assert_eq!(options.text.font_style, "normal");
assert_eq!(options.text.letter_spacing, "normal");
assert_eq!(options.text.text_indent, 2.0);
assert_eq!(options.color_scheme.background, "#FFFFFF");
assert_eq!(options.color_scheme.text, "#000000");
assert_eq!(options.color_scheme.link, "#6f6f6f");
assert_eq!(options.layout.margin, 20);
assert_eq!(options.layout.text_align, TextAlign::Left);
assert_eq!(options.layout.paragraph_spacing, 16);
}
#[test]
fn test_style_options_custom_values() {
let text = TextStyle {
font_size: 1.5,
line_height: 2.0,
font_family: "Georgia, serif".to_string(),
font_weight: "bold".to_string(),
font_style: "italic".to_string(),
letter_spacing: "0.1em".to_string(),
text_indent: 3.0,
};
let color_scheme = ColorScheme {
background: "#F0F0F0".to_string(),
text: "#333333".to_string(),
link: "#0066CC".to_string(),
};
let layout = PageLayout {
margin: 30,
text_align: TextAlign::Center,
paragraph_spacing: 20,
};
let options = StyleOptions { text, color_scheme, layout };
assert_eq!(options.text.font_size, 1.5);
assert_eq!(options.text.font_weight, "bold");
assert_eq!(options.color_scheme.background, "#F0F0F0");
assert_eq!(options.layout.text_align, TextAlign::Center);
}
#[test]
fn test_text_style_default() {
let style = TextStyle::default();
assert_eq!(style.font_size, 1.0);
assert_eq!(style.line_height, 1.6);
assert_eq!(style.font_family, "-apple-system, Roboto, sans-serif");
assert_eq!(style.font_weight, "normal");
assert_eq!(style.font_style, "normal");
assert_eq!(style.letter_spacing, "normal");
assert_eq!(style.text_indent, 2.0);
}
#[test]
fn test_text_style_custom_values() {
let style = TextStyle {
font_size: 2.0,
line_height: 1.8,
font_family: "Times New Roman".to_string(),
font_weight: "bold".to_string(),
font_style: "italic".to_string(),
letter_spacing: "0.05em".to_string(),
text_indent: 0.0,
};
assert_eq!(style.font_size, 2.0);
assert_eq!(style.line_height, 1.8);
assert_eq!(style.font_family, "Times New Roman");
assert_eq!(style.font_weight, "bold");
assert_eq!(style.font_style, "italic");
assert_eq!(style.letter_spacing, "0.05em");
assert_eq!(style.text_indent, 0.0);
}
#[test]
fn test_text_style_debug() {
let style = TextStyle::default();
let debug_str = format!("{:?}", style);
assert!(debug_str.contains("TextStyle"));
assert!(debug_str.contains("font_size"));
}
#[test]
fn test_color_scheme_default() {
let scheme = ColorScheme::default();
assert_eq!(scheme.background, "#FFFFFF");
assert_eq!(scheme.text, "#000000");
assert_eq!(scheme.link, "#6f6f6f");
}
#[test]
fn test_color_scheme_custom_values() {
let scheme = ColorScheme {
background: "#000000".to_string(),
text: "#FFFFFF".to_string(),
link: "#00FF00".to_string(),
};
assert_eq!(scheme.background, "#000000");
assert_eq!(scheme.text, "#FFFFFF");
assert_eq!(scheme.link, "#00FF00");
}
#[test]
fn test_color_scheme_debug() {
let scheme = ColorScheme::default();
let debug_str = format!("{:?}", scheme);
assert!(debug_str.contains("ColorScheme"));
assert!(debug_str.contains("background"));
}
#[test]
fn test_page_layout_default() {
let layout = PageLayout::default();
assert_eq!(layout.margin, 20);
assert_eq!(layout.text_align, TextAlign::Left);
assert_eq!(layout.paragraph_spacing, 16);
}
#[test]
fn test_page_layout_custom_values() {
let layout = PageLayout {
margin: 40,
text_align: TextAlign::Justify,
paragraph_spacing: 24,
};
assert_eq!(layout.margin, 40);
assert_eq!(layout.text_align, TextAlign::Justify);
assert_eq!(layout.paragraph_spacing, 24);
}
#[test]
fn test_page_layout_debug() {
let layout = PageLayout::default();
let debug_str = format!("{:?}", layout);
assert!(debug_str.contains("PageLayout"));
assert!(debug_str.contains("margin"));
}
#[test]
fn test_text_align_default() {
let align = TextAlign::default();
assert_eq!(align, TextAlign::Left);
}
#[test]
fn test_text_align_display() {
assert_eq!(TextAlign::Left.to_string(), "left");
assert_eq!(TextAlign::Right.to_string(), "right");
assert_eq!(TextAlign::Justify.to_string(), "justify");
assert_eq!(TextAlign::Center.to_string(), "center");
}
#[test]
fn test_text_align_all_variants() {
let left = TextAlign::Left;
let right = TextAlign::Right;
let justify = TextAlign::Justify;
let center = TextAlign::Center;
assert!(matches!(left, TextAlign::Left));
assert!(matches!(right, TextAlign::Right));
assert!(matches!(justify, TextAlign::Justify));
assert!(matches!(center, TextAlign::Center));
}
#[test]
fn test_text_align_debug() {
assert_eq!(format!("{:?}", TextAlign::Left), "Left");
assert_eq!(format!("{:?}", TextAlign::Right), "Right");
assert_eq!(format!("{:?}", TextAlign::Justify), "Justify");
assert_eq!(format!("{:?}", TextAlign::Center), "Center");
}
#[test]
fn test_style_options_builder_new() {
let options = StyleOptions::new();
assert_eq!(options.text.font_size, 1.0);
assert_eq!(options.color_scheme.background, "#FFFFFF");
assert_eq!(options.layout.margin, 20);
}
#[test]
fn test_style_options_builder_with_text() {
let mut options = StyleOptions::new();
let text_style = TextStyle::new()
.with_font_size(2.0)
.with_font_weight("bold")
.build();
options.with_text(text_style);
assert_eq!(options.text.font_size, 2.0);
assert_eq!(options.text.font_weight, "bold");
}
#[test]
fn test_style_options_builder_with_color_scheme() {
let mut options = StyleOptions::new();
let color = ColorScheme::new()
.with_background("#000000")
.with_text("#FFFFFF")
.build();
options.with_color_scheme(color);
assert_eq!(options.color_scheme.background, "#000000");
assert_eq!(options.color_scheme.text, "#FFFFFF");
}
#[test]
fn test_style_options_builder_with_layout() {
let mut options = StyleOptions::new();
let layout = PageLayout::new()
.with_margin(40)
.with_text_align(TextAlign::Justify)
.with_paragraph_spacing(24)
.build();
options.with_layout(layout);
assert_eq!(options.layout.margin, 40);
assert_eq!(options.layout.text_align, TextAlign::Justify);
assert_eq!(options.layout.paragraph_spacing, 24);
}
#[test]
fn test_style_options_builder_build() {
let options = StyleOptions::new()
.with_text(TextStyle::new().with_font_size(1.5).build())
.with_color_scheme(ColorScheme::new().with_link("#FF0000").build())
.with_layout(PageLayout::new().with_margin(30).build())
.build();
assert_eq!(options.text.font_size, 1.5);
assert_eq!(options.color_scheme.link, "#FF0000");
assert_eq!(options.layout.margin, 30);
}
#[test]
fn test_style_options_builder_chaining() {
let options = StyleOptions::new()
.with_text(
TextStyle::new()
.with_font_size(1.5)
.with_line_height(2.0)
.with_font_family("Arial")
.with_font_weight("bold")
.with_font_style("italic")
.with_letter_spacing("0.1em")
.with_text_indent(1.5)
.build(),
)
.with_color_scheme(
ColorScheme::new()
.with_background("#CCCCCC")
.with_text("#111111")
.with_link("#0000FF")
.build(),
)
.with_layout(
PageLayout::new()
.with_margin(25)
.with_text_align(TextAlign::Right)
.with_paragraph_spacing(20)
.build(),
)
.build();
assert_eq!(options.text.font_size, 1.5);
assert_eq!(options.text.line_height, 2.0);
assert_eq!(options.text.font_family, "Arial");
assert_eq!(options.text.font_weight, "bold");
assert_eq!(options.text.font_style, "italic");
assert_eq!(options.text.letter_spacing, "0.1em");
assert_eq!(options.text.text_indent, 1.5);
assert_eq!(options.color_scheme.background, "#CCCCCC");
assert_eq!(options.color_scheme.text, "#111111");
assert_eq!(options.color_scheme.link, "#0000FF");
assert_eq!(options.layout.margin, 25);
assert_eq!(options.layout.text_align, TextAlign::Right);
assert_eq!(options.layout.paragraph_spacing, 20);
}
#[test]
fn test_text_style_builder_new() {
let style = TextStyle::new();
assert_eq!(style.font_size, 1.0);
assert_eq!(style.line_height, 1.6);
}
#[test]
fn test_text_style_builder_with_font_size() {
let mut style = TextStyle::new();
style.with_font_size(2.5);
assert_eq!(style.font_size, 2.5);
}
#[test]
fn test_text_style_builder_with_line_height() {
let mut style = TextStyle::new();
style.with_line_height(2.0);
assert_eq!(style.line_height, 2.0);
}
#[test]
fn test_text_style_builder_with_font_family() {
let mut style = TextStyle::new();
style.with_font_family("Helvetica, Arial");
assert_eq!(style.font_family, "Helvetica, Arial");
}
#[test]
fn test_text_style_builder_with_font_weight() {
let mut style = TextStyle::new();
style.with_font_weight("bold");
assert_eq!(style.font_weight, "bold");
}
#[test]
fn test_text_style_builder_with_font_style() {
let mut style = TextStyle::new();
style.with_font_style("italic");
assert_eq!(style.font_style, "italic");
}
#[test]
fn test_text_style_builder_with_letter_spacing() {
let mut style = TextStyle::new();
style.with_letter_spacing("0.05em");
assert_eq!(style.letter_spacing, "0.05em");
}
#[test]
fn test_text_style_builder_with_text_indent() {
let mut style = TextStyle::new();
style.with_text_indent(3.0);
assert_eq!(style.text_indent, 3.0);
}
#[test]
fn test_text_style_builder_build() {
let style = TextStyle::new()
.with_font_size(1.8)
.with_line_height(1.9)
.build();
assert_eq!(style.font_size, 1.8);
assert_eq!(style.line_height, 1.9);
}
#[test]
fn test_text_style_builder_chaining() {
let style = TextStyle::new()
.with_font_size(2.0)
.with_line_height(1.8)
.with_font_family("Georgia")
.with_font_weight("bold")
.with_font_style("italic")
.with_letter_spacing("0.1em")
.with_text_indent(0.5)
.build();
assert_eq!(style.font_size, 2.0);
assert_eq!(style.line_height, 1.8);
assert_eq!(style.font_family, "Georgia");
assert_eq!(style.font_weight, "bold");
assert_eq!(style.font_style, "italic");
assert_eq!(style.letter_spacing, "0.1em");
assert_eq!(style.text_indent, 0.5);
}
#[test]
fn test_color_scheme_builder_new() {
let scheme = ColorScheme::new();
assert_eq!(scheme.background, "#FFFFFF");
assert_eq!(scheme.text, "#000000");
}
#[test]
fn test_color_scheme_builder_with_background() {
let mut scheme = ColorScheme::new();
scheme.with_background("#FF0000");
assert_eq!(scheme.background, "#FF0000");
}
#[test]
fn test_color_scheme_builder_with_text() {
let mut scheme = ColorScheme::new();
scheme.with_text("#333333");
assert_eq!(scheme.text, "#333333");
}
#[test]
fn test_color_scheme_builder_with_link() {
let mut scheme = ColorScheme::new();
scheme.with_link("#0000FF");
assert_eq!(scheme.link, "#0000FF");
}
#[test]
fn test_color_scheme_builder_build() {
let scheme = ColorScheme::new().with_background("#123456").build();
assert_eq!(scheme.background, "#123456");
assert_eq!(scheme.text, "#000000");
}
#[test]
fn test_color_scheme_builder_chaining() {
let scheme = ColorScheme::new()
.with_background("#AABBCC")
.with_text("#DDEEFF")
.with_link("#112233")
.build();
assert_eq!(scheme.background, "#AABBCC");
assert_eq!(scheme.text, "#DDEEFF");
assert_eq!(scheme.link, "#112233");
}
#[test]
fn test_page_layout_builder_new() {
let layout = PageLayout::new();
assert_eq!(layout.margin, 20);
assert_eq!(layout.text_align, TextAlign::Left);
assert_eq!(layout.paragraph_spacing, 16);
}
#[test]
fn test_page_layout_builder_with_margin() {
let mut layout = PageLayout::new();
layout.with_margin(50);
assert_eq!(layout.margin, 50);
}
#[test]
fn test_page_layout_builder_with_text_align() {
let mut layout = PageLayout::new();
layout.with_text_align(TextAlign::Center);
assert_eq!(layout.text_align, TextAlign::Center);
}
#[test]
fn test_page_layout_builder_with_paragraph_spacing() {
let mut layout = PageLayout::new();
layout.with_paragraph_spacing(30);
assert_eq!(layout.paragraph_spacing, 30);
}
#[test]
fn test_page_layout_builder_build() {
let layout = PageLayout::new().with_margin(35).build();
assert_eq!(layout.margin, 35);
assert_eq!(layout.text_align, TextAlign::Left);
}
#[test]
fn test_page_layout_builder_chaining() {
let layout = PageLayout::new()
.with_margin(45)
.with_text_align(TextAlign::Justify)
.with_paragraph_spacing(28)
.build();
assert_eq!(layout.margin, 45);
assert_eq!(layout.text_align, TextAlign::Justify);
assert_eq!(layout.paragraph_spacing, 28);
}
#[test]
fn test_page_layout_builder_all_text_align_variants() {
let left = PageLayout::new().with_text_align(TextAlign::Left).build();
assert_eq!(left.text_align, TextAlign::Left);
let right = PageLayout::new().with_text_align(TextAlign::Right).build();
assert_eq!(right.text_align, TextAlign::Right);
let center = PageLayout::new().with_text_align(TextAlign::Center).build();
assert_eq!(center.text_align, TextAlign::Center);
let justify = PageLayout::new()
.with_text_align(TextAlign::Justify)
.build();
assert_eq!(justify.text_align, TextAlign::Justify);
}
}
}