use std::collections::{HashMap, HashSet};
use std::io;
use std::sync::Arc;
use crate::book::Book;
use crate::import::ChapterId;
use crate::ir::{IRChapter, NodeId, Role, StyleId, StylePool};
use super::{generate_css, synthesize_xhtml_document};
#[derive(Debug)]
pub struct GlobalStylePool {
pool: StylePool,
remaps: Vec<HashMap<StyleId, StyleId>>,
}
impl Default for GlobalStylePool {
fn default() -> Self {
Self::new()
}
}
impl GlobalStylePool {
pub fn new() -> Self {
Self {
pool: StylePool::new(),
remaps: Vec::new(),
}
}
pub fn merge(&mut self, chapter_idx: usize, chapter: &IRChapter) {
while self.remaps.len() <= chapter_idx {
self.remaps.push(HashMap::new());
}
let remap = &mut self.remaps[chapter_idx];
for (local_id, style) in chapter.styles.iter() {
let global_id = self.pool.intern(style.clone());
remap.insert(local_id, global_id);
}
}
pub fn remap(&self, chapter_idx: usize, local_id: StyleId) -> StyleId {
self.remaps
.get(chapter_idx)
.and_then(|m| m.get(&local_id))
.copied()
.unwrap_or(StyleId::DEFAULT)
}
pub fn pool(&self) -> &StylePool {
&self.pool
}
pub fn used_styles(&self) -> Vec<StyleId> {
self.remaps
.iter()
.flat_map(|m| m.values())
.copied()
.collect()
}
}
#[derive(Debug, Clone)]
pub struct ChapterContent {
pub id: ChapterId,
pub source_path: String,
pub document: String,
}
#[derive(Debug)]
pub struct NormalizedContent {
pub styles: GlobalStylePool,
pub chapters: Vec<ChapterContent>,
pub assets: HashSet<String>,
pub css: String,
}
pub fn normalize_book(book: &mut Book) -> io::Result<NormalizedContent> {
let spine: Vec<_> = book.spine().to_vec();
let mut global_styles = GlobalStylePool::new();
let mut ir_chapters: Vec<(ChapterId, String, Arc<IRChapter>)> = Vec::with_capacity(spine.len());
for (idx, entry) in spine.iter().enumerate() {
let source_path = book
.source_id(entry.id)
.unwrap_or("unknown.xhtml")
.to_string();
let chapter = book.load_chapter_cached(entry.id)?;
global_styles.merge(idx, &chapter);
ir_chapters.push((entry.id, source_path, chapter));
}
let used_styles = global_styles.used_styles();
let css_artifact = generate_css(global_styles.pool(), &used_styles);
let mut chapters = Vec::with_capacity(ir_chapters.len());
let mut all_assets = HashSet::new();
for (idx, (chapter_id, source_path, ir)) in ir_chapters.iter().enumerate() {
let mut remapped_class_map: HashMap<StyleId, String> = HashMap::new();
for (local_id, _) in ir.styles.iter() {
let global_id = global_styles.remap(idx, local_id);
if let Some(class_name) = css_artifact.class_map.get(&global_id) {
remapped_class_map.insert(local_id, class_name.clone());
}
}
let title = extract_chapter_title(ir).unwrap_or_else(|| source_path.clone());
let result = synthesize_xhtml_document(ir, &remapped_class_map, &title, Some("style.css"));
all_assets.extend(result.assets);
chapters.push(ChapterContent {
id: *chapter_id,
source_path: source_path.clone(),
document: result.body,
});
}
Ok(NormalizedContent {
styles: global_styles,
chapters,
assets: all_assets,
css: css_artifact.stylesheet,
})
}
fn extract_chapter_title(ir: &IRChapter) -> Option<String> {
for node_id in ir.iter_dfs() {
if let Some(node) = ir.node(node_id)
&& matches!(node.role, Role::Heading(_))
{
let mut title = String::new();
collect_text_recursive(ir, node_id, &mut title);
if !title.is_empty() {
return Some(title.trim().to_string());
}
}
}
None
}
fn collect_text_recursive(ir: &IRChapter, node_id: NodeId, buf: &mut String) {
if let Some(node) = ir.node(node_id)
&& node.role == Role::Text
{
buf.push_str(ir.text(node.text));
}
for child_id in ir.children(node_id) {
collect_text_recursive(ir, child_id, buf);
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
use crate::ir::{ComputedStyle, FontWeight, Node};
#[test]
fn test_global_style_pool_new() {
let pool = GlobalStylePool::new();
assert_eq!(pool.pool().len(), 1); assert!(pool.remaps.is_empty());
}
#[test]
fn test_global_style_pool_merge() {
let mut global = GlobalStylePool::new();
let mut chapter1 = IRChapter::new();
let mut bold = ComputedStyle::default();
bold.font_weight = FontWeight::BOLD;
let bold_id = chapter1.styles.intern(bold.clone());
let mut chapter2 = IRChapter::new();
let bold_id2 = chapter2.styles.intern(bold);
global.merge(0, &chapter1);
global.merge(1, &chapter2);
let global_id1 = global.remap(0, bold_id);
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_remap_unknown() {
let global = GlobalStylePool::new();
let result = global.remap(999, StyleId(999));
assert_eq!(result, StyleId::DEFAULT);
}
#[test]
fn test_global_style_pool_used_styles() {
let mut global = GlobalStylePool::new();
let mut chapter = IRChapter::new();
let mut bold = ComputedStyle::default();
bold.font_weight = FontWeight::BOLD;
chapter.styles.intern(bold);
global.merge(0, &chapter);
let used = global.used_styles();
assert!(!used.is_empty());
}
#[test]
fn test_extract_chapter_title() {
let mut chapter = IRChapter::new();
let h1 = chapter.alloc_node(Node::new(Role::Heading(1)));
chapter.append_child(NodeId::ROOT, h1);
let text_range = chapter.append_text("Chapter One");
let mut text_node = Node::new(Role::Text);
text_node.text = text_range;
let text_id = chapter.alloc_node(text_node);
chapter.append_child(h1, text_id);
let title = extract_chapter_title(&chapter);
assert_eq!(title, Some("Chapter One".to_string()));
}
#[test]
fn test_extract_chapter_title_no_heading() {
let chapter = IRChapter::new();
let title = extract_chapter_title(&chapter);
assert_eq!(title, None);
}
}