diff options
-rw-r--r-- | src/epub.rs | 34 | ||||
-rw-r--r-- | src/main.rs | 118 |
2 files changed, 111 insertions, 41 deletions
diff --git a/src/epub.rs b/src/epub.rs index 882175e..b523c06 100644 --- a/src/epub.rs +++ b/src/epub.rs @@ -1,9 +1,12 @@ +use crossterm::style::Attribute; use roxmltree::{Document, Node}; use std::{collections::HashMap, fs::File, io::Read}; +type Attrs = Vec<(usize, Attribute)>; + pub struct Epub { container: zip::ZipArchive<File>, - pub chapters: Vec<(String, String)>, + pub chapters: Vec<(String, String, Attrs)>, pub meta: String, } @@ -33,18 +36,19 @@ impl Epub { fn get_chapters(&mut self, chapters: Vec<(String, String)>) { self.chapters = chapters .into_iter() - .filter_map(|(path, title)| { + .filter_map(|(title, path)| { let xml = self.get_text(&path); // https://github.com/RazrFalcon/roxmltree/issues/12 // UnknownEntityReference for HTML entities let doc = Document::parse(&xml).unwrap(); let body = doc.root_element().last_element_child().unwrap(); - let mut chapter = String::new(); - render(body, &mut chapter); - if chapter.is_empty() { + let mut text = String::new(); + let mut attrs = vec![(0, Attribute::Reset)]; + render(body, &mut text, &mut attrs); + if text.is_empty() { None } else { - Some((chapter, title)) + Some((title, text, attrs)) } }) .collect(); @@ -113,13 +117,13 @@ impl Epub { let path = manifest.remove(id).unwrap(); let label = nav.remove(path).unwrap_or_else(|| i.to_string()); let path = format!("{}{}", rootdir, path); - (path, label) + (label, path) }) .collect() } } -fn render(n: Node, buf: &mut String) { +fn render(n: Node, buf: &mut String, attrs: &mut Attrs) { if n.is_text() { let text = n.text().unwrap(); if !text.trim().is_empty() { @@ -130,30 +134,32 @@ fn render(n: Node, buf: &mut String) { match n.tag_name().name() { "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => { - buf.push_str("\n\x1b[1m"); + buf.push('\n'); + attrs.push((buf.len(), Attribute::Bold)); for c in n.children() { - render(c, buf); + render(c, buf, attrs); } - buf.push_str("\x1b[0m\n"); + attrs.push((buf.len(), Attribute::Reset)); + buf.push('\n'); } "blockquote" | "p" | "tr" => { buf.push('\n'); for c in n.children() { - render(c, buf); + render(c, buf, attrs); } buf.push('\n'); } "li" => { buf.push_str("\n- "); for c in n.children() { - render(c, buf); + render(c, buf, attrs); } buf.push('\n'); } "br" => buf.push('\n'), _ => { for c in n.children() { - render(c, buf); + render(c, buf, attrs); } } } diff --git a/src/main.rs b/src/main.rs index 8626541..9a4e720 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use std::{ - cmp::min, + cmp::{max, min}, collections::HashMap, env, fs, io::{stdout, Write}, @@ -300,13 +300,89 @@ impl View for Page { } } fn render(&self, bk: &Bk) -> Vec<String> { - let c = bk.chap(); - let end = min(bk.line + bk.rows, c.lines.len()); - c.lines[bk.line..end] + render_page(bk, 0) + } +} + +fn render_page(bk: &Bk, offset: usize) -> Vec<String> { + let c = bk.chap(); + let line_end = min(bk.line + bk.rows - offset, bk.chap().lines.len()); + + let attrs = { + let text_start = c.lines[bk.line].0; + let text_end = c.lines[line_end - 1].1; + + let mut search = Vec::new(); + if bk.query != "" { + for (pos, _) in c.text[text_start..text_end].match_indices(&bk.query) { + search.push((text_start + pos, Attribute::Reverse)); + search.push((text_start + pos + bk.query.len(), Attribute::Reset)); + } + } + let mut search_iter = search.into_iter(); + + let attr_end = match c.attrs.binary_search_by_key(&text_end, |&(pos, _)| pos) { + Ok(n) => n, + Err(n) => n, + }; + let attr_start = c.attrs[..attr_end] .iter() - .map(|&(a, b)| String::from(&c.text[a..b])) - .collect() + .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..]; + let mut attrs_iter = iter::once(&head).chain(tail.iter()); + + let mut merged = Vec::new(); + let mut sn = search_iter.next(); + let mut an = attrs_iter.next(); + loop { + match (sn, an) { + (None, None) => panic!("does this happen?"), + (Some(s), None) => { + merged.push(s); + merged.extend(search_iter); + break; + } + (None, Some(&a)) => { + merged.push(a); + merged.extend(attrs_iter); + break; + } + (Some(s), Some(&a)) => { + if s.0 < a.0 { + merged.push(s); + sn = search_iter.next(); + } else { + merged.push(a); + an = attrs_iter.next(); + } + } + } + } + merged + }; + + let mut buf = Vec::new(); + let mut iter = attrs.into_iter().peekable(); + for &(mut start, end) in &c.lines[bk.line..line_end] { + let mut s = String::new(); + while let Some(a) = iter.peek() { + if a.0 <= end { + s.push_str(&c.text[start..a.0]); + s.push_str(&a.1.to_string()); + start = a.0; + iter.next(); + } else { + break; + } + } + s.push_str(&c.text[start..end]); + buf.push(s); } + buf } struct Search; @@ -342,25 +418,7 @@ impl View for Search { } } fn render(&self, bk: &Bk) -> Vec<String> { - let c = bk.chap(); - let end = min(bk.line + bk.rows - 1, c.lines.len()); - let mut buf = Vec::with_capacity(bk.rows); - - for &(a, b) in c.lines[bk.line..end].iter() { - let line = String::from(&c.text[a..b]); - if let Some(i) = line.find(&bk.query) { - buf.push(format!( - "{}{}{}{}{}", - &line[..i], - Attribute::Reverse, - &bk.query, - Attribute::Reset, - &line[i + bk.query.len()..], - )); - } else { - buf.push(line); - } - } + let mut buf = render_page(bk, 1); for _ in buf.len()..bk.rows - 1 { buf.push(String::new()); @@ -380,6 +438,7 @@ struct Chapter { text: String, // byte indexes lines: Vec<(usize, usize)>, + attrs: Vec<(usize, Attribute)>, } struct Bk<'a> { @@ -411,7 +470,7 @@ impl Bk<'_> { .collect(); let mut chapters = Vec::with_capacity(epub.chapters.len()); - for (text, title) in epub.chapters { + for (title, text, attrs) in epub.chapters { let title = if title.chars().count() > width { title .chars() @@ -422,7 +481,12 @@ impl Bk<'_> { title }; let lines = wrap(&text, width); - chapters.push(Chapter { title, text, lines }); + chapters.push(Chapter { + title, + text, + lines, + attrs, + }); } let line = match chapters[args.chapter] |