diff options
-rw-r--r-- | src/epub.rs | 114 | ||||
-rw-r--r-- | src/main.rs | 88 |
2 files changed, 110 insertions, 92 deletions
diff --git a/src/epub.rs b/src/epub.rs index 68c834d..7cce777 100644 --- a/src/epub.rs +++ b/src/epub.rs @@ -1,13 +1,20 @@ use anyhow::Result; -use crossterm::style::Attribute; +use crossterm::style::{Attribute, Attributes}; use roxmltree::{Document, Node}; use std::{collections::HashMap, fs::File, io::Read}; -type Attrs = Vec<(usize, Attribute)>; +pub struct Chapter { + pub title: String, + // single string for search + pub text: String, + pub lines: Vec<(usize, usize)>, + // crossterm gives us a bitset but doesn't let us diff it, so store the state transition + pub attrs: Vec<(usize, Attribute, Attributes)>, +} pub struct Epub { container: zip::ZipArchive<File>, - pub chapters: Vec<(String, String, Attrs)>, + pub chapters: Vec<Chapter>, pub meta: String, } @@ -43,13 +50,18 @@ impl Epub { // UnknownEntityReference for HTML entities let doc = Document::parse(&xml).unwrap(); let body = doc.root_element().last_element_child().unwrap(); - let mut text = String::new(); - let mut attrs = vec![(0, Attribute::Reset)]; - render(body, &mut text, &mut attrs); - if text.is_empty() { + let attrs = Attributes::default(); + let mut c = Chapter { + title, + text: String::new(), + lines: Vec::new(), + attrs: vec![(0, Attribute::Reset, attrs)], + }; + render(body, &mut c, attrs); + if c.text.is_empty() { None } else { - Some((title, text, attrs)) + Some(c) } }) .collect(); @@ -78,16 +90,14 @@ impl Epub { let manifest_node = children.next().unwrap(); let spine_node = children.next().unwrap(); - meta_node - .children() - .filter(Node::is_element) - .for_each(|n| { - let name = n.tag_name().name(); - let text = n.text(); - if text.is_some() && name != "meta" { - self.meta.push_str(&format!("{}: {}\n", name, text.unwrap())); - } - }); + meta_node.children().filter(Node::is_element).for_each(|n| { + let name = n.tag_name().name(); + let text = n.text(); + if text.is_some() && name != "meta" { + self.meta + .push_str(&format!("{}: {}\n", name, text.unwrap())); + } + }); manifest_node .children() .filter(Node::is_element) @@ -126,58 +136,70 @@ impl Epub { } } -fn render(n: Node, buf: &mut String, attrs: &mut Attrs) { +fn render(n: Node, c: &mut Chapter, attrs: Attributes) { if n.is_text() { let text = n.text().unwrap(); if !text.trim().is_empty() { - buf.push_str(text); + c.text.push_str(text); } return; } + // fuck this gay earth match n.tag_name().name() { + "a" => { + let a = Attribute::Underlined; + c.attrs.push((c.text.len(), a, attrs | a)); + for child in n.children() { + render(child, c, attrs | a); + } + c.attrs.push((c.text.len(), Attribute::NoUnderline, attrs)); + } "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => { - buf.push('\n'); - attrs.push((buf.len(), Attribute::Bold)); - for c in n.children() { - render(c, buf, attrs); + c.text.push('\n'); + let a = Attribute::Bold; + c.attrs.push((c.text.len(), a, attrs | a)); + for child in n.children() { + render(child, c, attrs | a); } - attrs.push((buf.len(), Attribute::NoBold)); - buf.push('\n'); + c.attrs.push((c.text.len(), Attribute::NoBold, attrs)); + c.text.push('\n'); } "em" => { - attrs.push((buf.len(), Attribute::Italic)); - for c in n.children() { - render(c, buf, attrs); + let a = Attribute::Italic; + c.attrs.push((c.text.len(), a, attrs | a)); + for child in n.children() { + render(child, c, attrs | a); } - attrs.push((buf.len(), Attribute::NoItalic)); + c.attrs.push((c.text.len(), Attribute::NoItalic, attrs)); } "strong" => { - attrs.push((buf.len(), Attribute::Bold)); - for c in n.children() { - render(c, buf, attrs); + let a = Attribute::Bold; + c.attrs.push((c.text.len(), a, attrs | a)); + for child in n.children() { + render(child, c, attrs | a); } - attrs.push((buf.len(), Attribute::NoBold)); + c.attrs.push((c.text.len(), Attribute::NoBold, attrs)); } "blockquote" | "p" | "tr" => { - buf.push('\n'); - for c in n.children() { - render(c, buf, attrs); + c.text.push('\n'); + for child in n.children() { + render(child, c, attrs); } - buf.push('\n'); + c.text.push('\n'); } "li" => { - buf.push_str("\n- "); - for c in n.children() { - render(c, buf, attrs); + c.text.push_str("\n- "); + for child in n.children() { + render(child, c, attrs); } - buf.push('\n'); + c.text.push('\n'); } - "br" => buf.push('\n'), - "hr" => buf.push_str("\n* * *\n"), + "br" => c.text.push('\n'), + "hr" => c.text.push_str("\n* * *\n"), _ => { - for c in n.children() { - render(c, buf, attrs); + for child in n.children() { + render(child, c, attrs); } } } diff --git a/src/main.rs b/src/main.rs index 1b7269f..b46defc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use crossterm::{ }; use serde::{Deserialize, Serialize}; use std::{ - cmp::{max, min}, + cmp::min, collections::HashMap, env, fs, io::{stdout, Write}, @@ -17,6 +17,7 @@ use std::{ }; mod epub; +use epub::Chapter; // XXX assumes a char is i unit wide fn wrap(text: &str, width: usize) -> Vec<(usize, usize)> { @@ -304,7 +305,6 @@ impl View for Page { let c = bk.chap(); let line_end = min(bk.line + bk.rows, c.lines.len()); - // XXX does not pick up combined bold italic state if both attributes start above screen let attrs = { let text_start = c.lines[bk.line].0; let text_end = c.lines[line_end - 1].1; @@ -319,25 +319,34 @@ impl View for Page { } let mut search_iter = search.into_iter().peekable(); - let attr_end = match c.attrs.binary_search_by_key(&text_end, |&(pos, _)| pos) { + let mut merged = Vec::new(); + let attr_start = match c + .attrs + .binary_search_by_key(&text_start, |&(pos, _, _)| pos) + { Ok(n) => n, - Err(n) => n, + Err(n) => n - 1, }; - let attr_start = c.attrs[..attr_end] - .iter() - .rposition(|&(pos, _)| pos <= text_start) - .unwrap(); - // keep attr pos >= line start - let (pos, attr) = c.attrs[attr_start]; - let head = (max(pos, text_start), attr); - let tail = &c.attrs[attr_start + 1..attr_end]; - let mut attrs_iter = iter::once(&head).chain(tail.iter()).peekable(); - - // seems like this should be simpler. use itertools? - let mut merged = Vec::new(); + let mut attrs_iter = c.attrs[attr_start..].iter(); + let (_, _, attr) = attrs_iter.next().unwrap(); + if attr.has(Attribute::Bold) { + merged.push((text_start, Attribute::Bold)); + } + if attr.has(Attribute::Italic) { + merged.push((text_start, Attribute::Italic)); + } + if attr.has(Attribute::Underlined) { + merged.push((text_start, Attribute::Underlined)); + } + let mut attrs_iter = attrs_iter + .map(|&(pos, a, _)| (pos, a)) + .take_while(|(pos, _)| pos <= &text_end) + .peekable(); + + // use itertools? loop { match (search_iter.peek(), attrs_iter.peek()) { - (None, None) => panic!("double none wtf"), + (None, None) => break, (Some(_), None) => { merged.extend(search_iter); break; @@ -346,7 +355,7 @@ impl View for Page { merged.extend(attrs_iter); break; } - (Some(&s), Some(&&a)) => { + (Some(&s), Some(&a)) => { if s.0 < a.0 { merged.push(s); search_iter.next(); @@ -357,6 +366,7 @@ impl View for Page { } } } + merged }; @@ -380,7 +390,6 @@ impl View for Page { } } - struct Search; impl View for Search { fn run(&self, bk: &mut Bk, kc: KeyCode) { @@ -431,17 +440,8 @@ impl View for Search { } } -struct Chapter { - title: String, - // a single string for searching - text: String, - // byte indexes - lines: Vec<(usize, usize)>, - attrs: Vec<(usize, Attribute)>, -} - struct Bk<'a> { - chapters: Vec<Chapter>, + chapters: Vec<epub::Chapter>, // position in the book chapter: usize, line: usize, @@ -468,24 +468,17 @@ impl Bk<'_> { .map(|(a, b)| String::from(&epub.meta[a..b])) .collect(); - let mut chapters = Vec::with_capacity(epub.chapters.len()); - for (title, text, attrs) in epub.chapters { - let title = if title.chars().count() > width { - title + let mut chapters = epub.chapters; + for c in &mut chapters { + c.lines = wrap(&c.text, width); + if c.title.chars().count() > width { + c.title = c + .title .chars() .take(width - 1) .chain(iter::once('…')) - .collect() - } else { - title - }; - let lines = wrap(&text, width); - chapters.push(Chapter { - title, - text, - lines, - attrs, - }); + .collect(); + } } let line = match chapters[args.chapter] @@ -528,8 +521,11 @@ impl Bk<'_> { while let Some(view) = self.view { let pad = self.cols.saturating_sub(self.max_width) / 2; - // clearing the screen doesn't reset attributes wtf - queue!(stdout, terminal::Clear(terminal::ClearType::All), Print(Attribute::Reset))?; + queue!( + stdout, + terminal::Clear(terminal::ClearType::All), + Print(Attribute::Reset) + )?; for (i, line) in view.render(self).iter().enumerate() { queue!(stdout, cursor::MoveTo(pad, i as u16), Print(line))?; } |