1use anyhow::{anyhow, Result};
6use rbook::Epub;
7
8use super::{page, page::Page, sanitize, styled};
9
10pub struct SpineData {
12 pub pages: Vec<Page>,
14 pub plain_text: String,
16 pub plain_text_chars: usize,
18}
19
20pub fn spine_hrefs(book: &Epub) -> Result<Vec<String>> {
22 let spine = book.spine().elements();
23 let mut out: Vec<String> = Vec::with_capacity(spine.len());
24 for el in &spine {
25 let idref = el.name();
26 let item = book
27 .manifest()
28 .by_id(idref)
29 .ok_or_else(|| anyhow!("manifest missing idref {}", idref))?;
30 out.push(item.value().to_string());
31 }
32 Ok(out)
33}
34
35pub fn chapter_titles_from_book(book: &Epub) -> Vec<String> {
41 let spine_hrefs_result = spine_hrefs(book);
42 let hrefs = match spine_hrefs_result {
43 Ok(h) => h,
44 Err(_) => return Vec::new(),
45 };
46
47 let toc_entries: Vec<(String, String)> = book
49 .toc()
50 .elements_flat()
51 .into_iter()
52 .filter_map(|e| {
53 let label = e.name().trim().to_string();
54 let href = e.value().trim().to_string();
55 if label.is_empty() || href.is_empty() {
56 None
57 } else {
58 Some((strip_fragment(&href).to_string(), label))
59 }
60 })
61 .collect();
62
63 hrefs
64 .iter()
65 .enumerate()
66 .map(|(i, href)| {
67 let href_path = strip_fragment(href);
68 let exact = toc_entries
71 .iter()
72 .find(|(h, _)| h == href_path)
73 .map(|(_, l)| l.clone());
74 let by_base = exact.or_else(|| {
75 let target_base = basename(href_path);
76 toc_entries
77 .iter()
78 .find(|(h, _)| basename(h) == target_base)
79 .map(|(_, l)| l.clone())
80 });
81 by_base.unwrap_or_else(|| format!("Chapter {}", i + 1))
82 })
83 .collect()
84}
85
86pub fn load_spine_data(book: &Epub, idx: usize, width: u16, height: u16) -> Result<SpineData> {
88 let hrefs = spine_hrefs(book)?;
89 let href = hrefs
90 .get(idx)
91 .ok_or_else(|| anyhow!("spine index {} out of bounds (len {})", idx, hrefs.len()))?;
92 let html = book.read_file(href)?;
93 Ok(load_spine_from_html(&html, width, height))
94}
95
96pub fn load_spine_from_html(html: &str, width: u16, height: u16) -> SpineData {
99 let safe = sanitize::clean(html);
100 let spans = styled::to_spans(&safe);
101 let plain_text: String = spans
102 .iter()
103 .map(|s| s.text.as_str())
104 .collect::<Vec<_>>()
105 .concat();
106 let pages = page::paginate(&spans, width, height.saturating_sub(2));
107 let plain_text_chars = plain_text.chars().count();
108 SpineData {
109 pages,
110 plain_text,
111 plain_text_chars,
112 }
113}
114
115fn strip_fragment(s: &str) -> &str {
116 match s.find('#') {
117 Some(i) => &s[..i],
118 None => s,
119 }
120}
121
122fn basename(s: &str) -> &str {
123 match s.rfind('/') {
124 Some(i) => &s[i + 1..],
125 None => s,
126 }
127}