use std::io::Cursor;
use std::sync::Arc;
use boko::Book;
use boko::export::{EpubConfig, EpubExporter, Exporter, GlobalStylePool, normalize_book};
use boko::model::Chapter;
use boko::style::{ComputedStyle, FontStyle, FontWeight, StyleId};
#[test]
fn test_global_style_pool_merge_deduplicates() {
let mut global = GlobalStylePool::new();
let mut chapter1 = Chapter::new();
let bold = ComputedStyle {
font_weight: FontWeight::BOLD,
..Default::default()
};
let bold_id1 = chapter1.styles.intern(bold.clone());
let mut chapter2 = Chapter::new();
let bold_id2 = chapter2.styles.intern(bold);
global.merge(0, &chapter1);
global.merge(1, &chapter2);
let global_id1 = global.remap(0, bold_id1);
let global_id2 = global.remap(1, bold_id2);
assert_eq!(global_id1, global_id2);
assert_eq!(global.pool().len(), 2);
}
#[test]
fn test_global_style_pool_different_styles_get_different_ids() {
let mut global = GlobalStylePool::new();
let mut chapter1 = Chapter::new();
let bold = ComputedStyle {
font_weight: FontWeight::BOLD,
..Default::default()
};
let bold_id = chapter1.styles.intern(bold);
let mut chapter2 = Chapter::new();
let italic = ComputedStyle {
font_style: FontStyle::Italic,
..Default::default()
};
let italic_id = chapter2.styles.intern(italic);
global.merge(0, &chapter1);
global.merge(1, &chapter2);
let global_bold = global.remap(0, bold_id);
let global_italic = global.remap(1, italic_id);
assert_ne!(global_bold, global_italic);
assert_eq!(global.pool().len(), 3);
}
#[test]
fn test_global_style_pool_remap_unknown_returns_default() {
let global = GlobalStylePool::new();
let result = global.remap(999, StyleId(999));
assert_eq!(result, StyleId::DEFAULT);
}
#[test]
fn test_global_style_pool_used_styles_dedupes_and_sorts() {
let mut global = GlobalStylePool::new();
let mut chapter1 = Chapter::new();
let bold = ComputedStyle {
font_weight: FontWeight::BOLD,
..Default::default()
};
let bold_id1 = chapter1.styles.intern(bold.clone());
let mut chapter2 = Chapter::new();
let bold_id2 = chapter2.styles.intern(bold);
global.merge(0, &chapter1);
global.merge(1, &chapter2);
let used = global.used_styles();
assert!(used.contains(&StyleId::DEFAULT));
assert_eq!(used.len(), 2);
let global_bold1 = global.remap(0, bold_id1);
let global_bold2 = global.remap(1, bold_id2);
assert_eq!(global_bold1, global_bold2);
assert_eq!(used, vec![StyleId::DEFAULT, global_bold1]);
}
fn extract_style_classes(document: &str) -> Vec<String> {
let mut classes = Vec::new();
let mut rest = document;
while let Some(idx) = rest.find("class=\"") {
rest = &rest[idx + 7..];
let Some(end) = rest.find('"') else {
break;
};
let class_attr = &rest[..end];
for token in class_attr.split_whitespace() {
if token.starts_with('c') && token[1..].chars().all(|c| c.is_ascii_digit()) {
classes.push(token.to_string());
}
}
rest = &rest[end + 1..];
}
classes
}
#[test]
fn test_normalize_book_emits_css_for_used_classes() {
let mut book = Book::open("tests/fixtures/epictetus.epub").expect("Failed to open test book");
let content = normalize_book(&mut book).expect("normalize_book failed");
let mut all_classes = Vec::new();
for chapter in &content.chapters {
assert!(
chapter.document.contains("style.css"),
"Chapter should reference style.css"
);
all_classes.extend(extract_style_classes(&chapter.document));
}
assert!(
!all_classes.is_empty(),
"Expected normalized XHTML to reference at least one style class"
);
for class_name in all_classes {
let needle = format!(".{}", class_name);
assert!(
content.css.contains(&needle),
"CSS should contain rule for class {}",
class_name
);
}
}
#[test]
fn test_normalized_export_contains_css_and_numbered_chapters() {
let mut book = Book::open("tests/fixtures/epictetus.epub").expect("Failed to open test book");
let mut output = Cursor::new(Vec::new());
let exporter = EpubExporter::new().with_config(EpubConfig {
normalize: true,
..Default::default()
});
exporter
.export(&mut book, &mut output)
.expect("Normalized export failed");
let data = output.into_inner();
let reader = Cursor::new(data);
let mut archive = zip::ZipArchive::new(reader).expect("Failed to read ZIP");
let mut found_style = false;
let mut found_chapter_0 = false;
for i in 0..archive.len() {
let file = archive.by_index(i).expect("Failed to read ZIP entry");
if file.name().ends_with("style.css") {
found_style = true;
}
if file.name().contains("chapter_0.xhtml") {
found_chapter_0 = true;
}
}
assert!(found_style, "Normalized EPUB should contain style.css");
assert!(
found_chapter_0,
"Normalized EPUB should have numbered chapter files"
);
}
#[test]
fn test_load_chapter_cached_returns_same_arc() {
let mut book = Book::open("tests/fixtures/epictetus.epub").expect("Failed to open test book");
let spine: Vec<_> = book.spine().to_vec();
assert!(!spine.is_empty(), "Book should have spine entries");
let chapter1 = book
.load_chapter_cached(spine[0].id)
.expect("First load failed");
let chapter2 = book
.load_chapter_cached(spine[0].id)
.expect("Second load failed");
assert!(
Arc::ptr_eq(&chapter1, &chapter2),
"Expected cached load to return the same Arc"
);
}