diff options
| -rw-r--r-- | src/epub.rs | 35 | ||||
| -rw-r--r-- | src/main.rs | 207 | 
2 files changed, 140 insertions, 102 deletions
| diff --git a/src/epub.rs b/src/epub.rs index 508e6b1..f80e2bc 100644 --- a/src/epub.rs +++ b/src/epub.rs @@ -7,7 +7,7 @@ use roxmltree::{Document, Node};  pub struct Epub {      container: zip::ZipArchive<File>,      pub nav: Vec<String>, -    pub pages: Vec<Vec<String>>, +    pub chapters: Vec<String>,  }  impl Epub { @@ -16,55 +16,57 @@ impl Epub {          let mut epub = Epub {              container: zip::ZipArchive::new(file)?,              nav: Vec::new(), -            pages: Vec::new(), +            chapters: Vec::new(),          };          let nav = epub.get_nav();          epub.nav.reserve_exact(nav.len()); -        epub.pages.reserve_exact(nav.len()); +        epub.chapters.reserve_exact(nav.len());          for (path, label) in nav {              epub.nav.push(label);              let xml = epub.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 page = Vec::new(); -            Epub::render(&mut page, body); -            epub.pages.push(page); +            // XXX no initial string, buf.last is none +            let mut chapter = String::new(); +            Epub::render(&mut chapter, body); +            epub.chapters.push(chapter);          }          Ok(epub)      } -    fn render(buf: &mut Vec<String>, n: Node) { +    fn render(buf: &mut String, n: Node) {          if n.is_text() {              let text = n.text().unwrap();              if !text.trim().is_empty() { -                let last = buf.last_mut().unwrap(); -                last.push_str(text); +                buf.push_str(text);              }              return;          }          match n.tag_name().name() {              "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => { -                buf.push(String::from("\x1b\x5b1m")); +                buf.push_str("\n\x1b\x5b1m");                  for c in n.children() {                      Self::render(buf, c);                  } -                buf.push(String::from("\x1b\x5b0m")); +                buf.push_str("\x1b\x5b0m\n");              }              "blockquote" | "p" => { -                buf.push(String::new()); +                buf.push('\n');                  for c in n.children() {                      Self::render(buf, c);                  } -                buf.push(String::new()); +                buf.push('\n');              }              "li" => { -                buf.push(String::from("- ")); +                buf.push_str("\n- ");                  for c in n.children() {                      Self::render(buf, c);                  } -                buf.push(String::new()); +                buf.push('\n');              } -            "br" => buf.push(String::new()), +            "br" => buf.push('\n'),              _ => {                  for c in n.children() {                      Self::render(buf, c); @@ -106,7 +108,6 @@ impl Epub {                  manifest.insert(n.attribute("id").unwrap(), n.attribute("href").unwrap());              }); -        // TODO check if epub3 nav is reliable w/o spine          let mut nav = HashMap::new();          if doc.root_element().attribute("version") == Some("3.0") {              let path = doc diff --git a/src/main.rs b/src/main.rs index 6a87411..c050d1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,47 +11,55 @@ use crossterm::{  mod epub;  use epub::Epub; -fn wrap(text: &String, width: u16) -> Vec<String> { +fn wrap(text: &str, width: usize) -> Vec<(usize, String)> {      // XXX assumes a char is 1 unit wide -    let mut wrapped = Vec::new(); +    let mut lines = Vec::new();      let mut start = 0; -    let mut brk = 0; -    let mut line = 0; +    let mut end = 0; +    let mut len = 0;      let mut word = 0;      let mut skip = 0;      for (i, c) in text.char_indices() { +        len += 1;          match c {              ' ' => { -                brk = i; +                end = i;                  skip = 1;                  word = 0;              } -            // https://www.unicode.org/reports/tr14/ -            // https://en.wikipedia.org/wiki/Line_wrap_and_word_wrap -            // currently only break at hyphen and em-dash :shrug:              '-' | '—' => { -                brk = i + c.len_utf8(); -                skip = 0; -                word = 0; +                if len > width { +                    // `end = i + 1` will extend over the margin +                    word += 1; +                } else { +                    end = i + c.len_utf8(); +                    skip = 0; +                    word = 0; +                }              }              _ => {                  word += 1;              }          } - -        if line == width { -            wrapped.push(String::from(&text[start..brk])); -            start = brk + skip; -            line = word; -        } else { -            line += 1; +        if c == '\n' { +            lines.push((start, String::from(&text[start..i]))); +            start = i + 1; +            len = 0; +        } else if len > width { +            let line = if word == len { +                &text[start..i] +            } else { +                &text[start..end] +            }; +            lines.push((start, String::from(line))); +            start = end + skip; +            len = word;          }      } -    wrapped.push(String::from(&text[start..])); -    wrapped +    lines  }  struct Position(String, usize, usize); @@ -104,8 +112,8 @@ impl View for Nav {                  bk.view = Some(&Page);              }              KeyCode::Enter | KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => { -                bk.get_chapter(bk.nav_idx); -                bk.pos = 0; +                bk.chapter = bk.nav_idx; +                bk.line = 0;                  bk.view = Some(&Page);              }              KeyCode::Down | KeyCode::Char('j') => { @@ -158,25 +166,28 @@ impl View for Page {          match kc {              KeyCode::Esc | KeyCode::Char('q') => bk.view = None,              KeyCode::Tab => { -                bk.nav_idx = bk.chapter_idx; +                bk.nav_idx = bk.chapter;                  bk.nav_top = bk.nav_idx.saturating_sub(bk.rows - 1);                  bk.view = Some(&Nav);              }              KeyCode::F(1) | KeyCode::Char('?') => bk.view = Some(&Help),              KeyCode::Char('/') => {                  bk.search = String::new(); +                bk.jump = bk.line;                  bk.view = Some(&Search);              }              KeyCode::Char('N') => {                  bk.search(Direction::Backward);              }              KeyCode::Char('n') => { +                // FIXME +                bk.scroll_down(1);                  bk.search(Direction::Forward);              }              KeyCode::End | KeyCode::Char('G') => { -                bk.pos = (bk.chapter.len() / bk.rows) * bk.rows; +                bk.line = bk.lines().len().saturating_sub(bk.rows);              } -            KeyCode::Home | KeyCode::Char('g') => bk.pos = 0, +            KeyCode::Home | KeyCode::Char('g') => bk.line = 0,              KeyCode::Char('d') => {                  bk.scroll_down(bk.rows / 2);              } @@ -205,8 +216,8 @@ impl View for Page {          }      }      fn render(&self, bk: &Bk) -> Vec<String> { -        let end = std::cmp::min(bk.pos + bk.rows, bk.chapter.len()); -        bk.chapter[bk.pos..end].iter().map(String::from).collect() +        let end = std::cmp::min(bk.line + bk.rows, bk.lines().len()); +        bk.lines()[bk.line..end].iter().map(String::from).collect()      }  } @@ -215,7 +226,7 @@ impl View for Search {      fn run(&self, bk: &mut Bk, kc: KeyCode) {          match kc {              KeyCode::Esc => { -                bk.search = String::new(); +                bk.line = bk.jump;                  bk.view = Some(&Page);              }              KeyCode::Enter => { @@ -223,6 +234,7 @@ impl View for Search {              }              KeyCode::Backspace => {                  bk.search.pop(); +                bk.search(Direction::Forward);              }              KeyCode::Char(c) => {                  bk.search.push(c); @@ -232,10 +244,10 @@ impl View for Search {          }      }      fn render(&self, bk: &Bk) -> Vec<String> { -        let end = std::cmp::min(bk.pos + bk.rows - 1, bk.chapter.len()); +        let end = std::cmp::min(bk.line + bk.rows - 1, bk.lines().len());          let mut buf = Vec::with_capacity(bk.rows); -        for line in bk.chapter[bk.pos..end].iter() { +        for line in bk.lines()[bk.line..end].iter() {              if let Some(i) = line.find(&bk.search) {                  buf.push(format!(                      "{}{}{}{}{}", @@ -258,40 +270,62 @@ impl View for Search {      }  } +struct Chapter { +    text: String, +    lines: Vec<String>, +    bytes: Vec<usize>, +} +  struct Bk<'a> {      view: Option<&'a dyn View>, -    pages: Vec<Vec<String>>, +    chapter: usize,      cols: u16, -    chapter: Vec<String>, -    chapter_idx: usize, +    // ideally we could use string slices as pointers, but self referential structs are hard +    chapters: Vec<Chapter>,      nav_idx: usize,      nav_top: usize, -    pos: usize, +    line: usize, +    jump: usize,      rows: usize,      toc: Vec<String>, -    pad: u16, +    max_width: u16,      search: String,  }  impl Bk<'_> { -    fn new(epub: Epub, pos: &Position, pad: u16) -> Self { +    fn new(epub: Epub, line: &Position, max_width: u16) -> Self {          let (cols, rows) = terminal::size().unwrap(); -        let mut bk = Bk { +        let width = std::cmp::min(cols, max_width) as usize; +        let mut chapters = Vec::with_capacity(epub.chapters.len()); +        for text in epub.chapters { +            let wrap = wrap(&text, width); +            let mut lines = Vec::with_capacity(wrap.len()); +            let mut bytes = Vec::with_capacity(wrap.len()); + +            for (byte, line) in wrap { +                lines.push(line); +                bytes.push(byte); +            } +            chapters.push(Chapter {text, lines, bytes}); +        } + +        Bk { +            jump: 0,              view: Some(&Page), -            chapter: Vec::new(), -            chapter_idx: 0, +            chapter: line.1,              nav_idx: 0,              nav_top: 0,              toc: epub.nav, -            pages: epub.pages, -            pos: pos.2, -            pad, +            chapters, +            line: line.2, +            max_width,              cols,              rows: rows as usize,              search: String::new(), -        }; -        bk.get_chapter(pos.1); -        bk +        } +    } +    fn lines(&self) -> &Vec<String> { +        &self.chapters[self.chapter].lines      }      fn run(&mut self) -> crossterm::Result<()> {          let mut stdout = stdout(); @@ -299,20 +333,18 @@ impl Bk<'_> {          terminal::enable_raw_mode()?;          while let Some(view) = self.view { +            let pad = self.cols.saturating_sub(self.max_width) / 2; +              queue!(stdout, terminal::Clear(terminal::ClearType::All))?;              for (i, line) in view.render(self).iter().enumerate() { -                queue!(stdout, cursor::MoveTo(self.pad, i as u16), Print(line))?; +                queue!(stdout, cursor::MoveTo(pad, i as u16), Print(line))?;              }              stdout.flush().unwrap();              match event::read()? {                  Event::Key(e) => view.run(self, e.code), -                Event::Resize(cols, rows) => { -                    self.cols = cols; -                    self.rows = rows as usize; -                    self.get_chapter(self.chapter_idx); -                }                  // TODO +                Event::Resize(_, _) => (),                  Event::Mouse(_) => (),              }          } @@ -320,58 +352,63 @@ impl Bk<'_> {          queue!(stdout, terminal::LeaveAlternateScreen, cursor::Show)?;          terminal::disable_raw_mode()      } -    fn get_chapter(&mut self, idx: usize) { -        let width = self.cols - (self.pad * 2); -        let page = &self.pages[idx]; -        self.chapter = Vec::with_capacity(page.len() * 2); -        for line in page { -            self.chapter.append(&mut wrap(line, width)) -        } -        self.chapter_idx = idx; -    }      fn next_chapter(&mut self) { -        if self.chapter_idx < self.toc.len() - 1 { -            self.get_chapter(self.chapter_idx + 1); -            self.pos = 0; +        if self.chapter < self.toc.len() - 1 { +            self.chapter += 1; +            self.line = 0;          }      }      fn prev_chapter(&mut self) { -        if self.chapter_idx > 0 { -            self.get_chapter(self.chapter_idx - 1); -            self.pos = 0; +        if self.chapter > 0 { +            self.chapter -= 1; +            self.line = 0;          }      }      fn scroll_down(&mut self, n: usize) { -        if self.rows < self.chapter.len() - self.pos { -            self.pos += n; +        if self.line + self.rows < self.lines().len() { +            self.line += n;          } else {              self.next_chapter();          }      }      fn scroll_up(&mut self, n: usize) { -        if self.pos > 0 { -            self.pos = self.pos.saturating_sub(n); +        if self.line > 0 { +            self.line = self.line.saturating_sub(n);          } else {              self.prev_chapter(); -            self.pos = (self.chapter.len() / self.rows) * self.rows; +            self.line = self.lines().len().saturating_sub(self.rows);          }      }      fn search(&mut self, dir: Direction) { +        // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.binary_search +        // If the value is not found then Result::Err is returned, containing the index where a matching element +        // could be inserted while maintaining sorted order. +        let head = (self.chapter, self.chapters[self.chapter].bytes[self.line]);          match dir {              Direction::Forward => { -                if let Some(i) = self.chapter[self.pos..] -                    .iter() -                    .position(|s| s.contains(&self.search)) -                { -                    self.pos += i; +                let rest = (self.chapter+1..self.chapters.len()-1).map(|n| (n, 0)); +                for (c, byte) in std::iter::once(head).chain(rest) { +                    if let Some(index) = self.chapters[c].text[byte..].find(&self.search) { +                        self.line = match self.chapters[c].bytes.binary_search(&(byte + index)) { +                            Ok(n) => n, +                            Err(n) => n-1, +                        }; +                        self.chapter = c; +                        break; +                    }                  }              }              Direction::Backward => { -                if let Some(i) = self.chapter[..self.pos] -                    .iter() -                    .rposition(|s| s.contains(&self.search)) -                { -                    self.pos = i; +                let rest = (0..self.chapter-1).rev().map(|c| (c, self.chapters[c].text.len())); +                for (c, byte) in std::iter::once(head).chain(rest) { +                    if let Some(index) = self.chapters[c].text[..byte].rfind(&self.search) { +                        self.line = match self.chapters[c].bytes.binary_search(&index) { +                            Ok(n) => n, +                            Err(n) => n-1, +                        }; +                        self.chapter = c; +                        break; +                    }                  }              }          } @@ -408,23 +445,23 @@ fn restore() -> Option<Position> {  }  fn main() { -    let pos = restore().unwrap_or_else(|| { +    let line = restore().unwrap_or_else(|| {          println!("usage: bk path");          std::process::exit(1);      }); -    let epub = Epub::new(&pos.0).unwrap_or_else(|e| { +    let epub = Epub::new(&line.0).unwrap_or_else(|e| {          println!("error reading epub: {}", e);          std::process::exit(1);      }); -    let mut bk = Bk::new(epub, &pos, 3); +    let mut bk = Bk::new(epub, &line, 75);      // crossterm really shouldn't error      bk.run().unwrap();      std::fs::write(          format!("{}/.local/share/bk", std::env::var("HOME").unwrap()), -        format!("{}\n{}\n{}", pos.0, bk.chapter_idx, bk.pos), +        format!("{}\n{}\n{}", line.0, bk.chapter, bk.line),      )      .unwrap_or_else(|e| {          println!("error saving position: {}", e); | 
