aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/epub.rs34
-rw-r--r--src/main.rs118
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]