aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/epub.rs114
-rw-r--r--src/main.rs88
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))?;
}