From 568fb0acc8e18e0ad60638dd6776485f1a857624 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 8 Oct 2020 10:00:26 +0200 Subject: [PATCH] Use cursive-markup to replace HtmlView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’ve moved the HtmlView into a separate crate, so we can replace our own HtmlView with cursive_markup::MarkupView. We only have to implement a custom Renderer that applies the syntax highlighting to code snippets. --- Cargo.lock | 12 ++ Cargo.toml | 1 + src/viewer/tui/mod.rs | 31 ++--- src/viewer/tui/views.rs | 295 ++++------------------------------------ 4 files changed, 51 insertions(+), 288 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b8b4e7..3009b15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,16 @@ dependencies = [ "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "cursive-markup" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cursive 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "html2text 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cursive_core" version = "0.1.1" @@ -1005,6 +1015,7 @@ dependencies = [ "assert_cmd 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", "cursive 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cursive-markup 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "html2text 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "insta 0.16.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1615,6 +1626,7 @@ dependencies = [ "checksum cssparser 0.27.2 (registry+https://github.com/rust-lang/crates.io-index)" = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" "checksum cssparser-macros 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" "checksum cursive 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a9f12332ab2bca26979ef00cfef9a1c2e287db03b787a83d892ad9961f81374" +"checksum cursive-markup 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "108cee1e66cebbf78ae2e08a53c2dfcfe6f8d536abcebbb6bb614f5ab1955868" "checksum cursive_core 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "85fc5b6a8ba2f1bc743892068bde466438f78d6247197e2dc094bfd53fdea4b7" "checksum darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" "checksum darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" diff --git a/Cargo.toml b/Cargo.toml index 63799b4..8512541 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ ansi_term = "0.12.1" anyhow = "1.0.31" atty = "0.2.14" cursive = "0.15.0" +cursive-markup = "0.1" html2text = "0.2.1" kuchiki = "0.8.0" log = "0.4.11" diff --git a/src/viewer/tui/mod.rs b/src/viewer/tui/mod.rs index ca9fe46..71cb515 100644 --- a/src/viewer/tui/mod.rs +++ b/src/viewer/tui/mod.rs @@ -9,13 +9,14 @@ use anyhow::Context as _; use cursive::view::{Resizable as _, Scrollable as _}; use cursive::views::{Dialog, LinearLayout, PaddedView, Panel, TextView}; use cursive::{event, theme, utils::markup}; +use cursive_markup::MarkupView; use crate::args; use crate::doc; use crate::source; use crate::viewer::{self, utils, utils::ManRenderer as _}; -use views::{CodeView, HtmlView, LinkView}; +use views::{CodeView, HtmlRenderer, LinkView}; #[derive(Clone, Debug)] pub struct TuiViewer {} @@ -168,15 +169,13 @@ impl<'s> utils::ManRenderer for TuiManRenderer<'s> { fn print_text(&mut self, indent: u8, text: &doc::Text) -> Result<(), Self::Error> { let indent = usize::from(indent); - let mut text = HtmlView::new( - &text.html, - self.highlighter.cloned(), - self.max_width.saturating_sub(indent), - ); + let renderer = HtmlRenderer::new(&text.html, self.highlighter.cloned()); + let mut view = MarkupView::with_renderer(renderer); + view.set_maximum_width(self.max_width.saturating_sub(indent)); let doc_name = self.doc_name.clone(); let doc_ty = self.doc_ty; - text.set_on_link(move |s, link| handle_link(s, &doc_name.clone(), doc_ty, link)); - self.layout.add_child(indent_view(indent, text)); + view.on_link_select(move |s, link| handle_link(s, &doc_name, doc_ty, link)); + self.layout.add_child(indent_view(indent, view)); Ok(()) } @@ -238,7 +237,7 @@ fn report_error(s: &mut cursive::Cursive, error: anyhow::Error) { s.add_layer(dialog); } -fn handle_link(s: &mut cursive::Cursive, doc_name: &doc::Fqn, doc_ty: doc::ItemType, link: String) { +fn handle_link(s: &mut cursive::Cursive, doc_name: &doc::Fqn, doc_ty: doc::ItemType, link: &str) { let result = resolve_link(doc_name, doc_ty, link).and_then(|link| open_link(s, link)); if let Err(err) = result { report_error(s, err); @@ -291,16 +290,14 @@ impl From for ResolvedLink { fn resolve_link( doc_name: &doc::Fqn, doc_ty: doc::ItemType, - link: String, + link: &str, ) -> anyhow::Result { // TODO: support docs.rs and doc.rust-lang.org links - match url::Url::parse(&link) { - Ok(_) => Ok(ResolvedLink::External(link)), - Err(url::ParseError::RelativeUrlWithoutBase) => resolve_doc_link(doc_name, doc_ty, &link) - .with_context(|| format!("Could not parse relative link URL: {}", &link)), - Err(e) => { - Err(anyhow::Error::new(e).context(format!("Could not parse link URL: {}", &link))) - } + match url::Url::parse(link) { + Ok(_) => Ok(ResolvedLink::External(link.to_owned())), + Err(url::ParseError::RelativeUrlWithoutBase) => resolve_doc_link(doc_name, doc_ty, link) + .with_context(|| format!("Could not parse relative link URL: {}", link)), + Err(e) => Err(anyhow::Error::new(e).context(format!("Could not parse link URL: {}", link))), } } diff --git a/src/viewer/tui/views.rs b/src/viewer/tui/views.rs index c3b8910..5053c8c 100644 --- a/src/viewer/tui/views.rs +++ b/src/viewer/tui/views.rs @@ -3,228 +3,57 @@ use std::cmp; use std::iter; -use std::rc; use cursive::{event, theme, utils::markup}; use html2text::render::text_renderer; use crate::viewer::utils; -pub struct HtmlView { +pub struct HtmlRenderer { render_tree: html2text::RenderTree, highlighter: Option, - max_width: usize, - rendered_html: Option, - focus: Option, - on_link: Option>, } -impl HtmlView { - pub fn new(html: &str, highlighter: Option, max_width: usize) -> HtmlView { - HtmlView { +impl HtmlRenderer { + pub fn new(html: &str, highlighter: Option) -> HtmlRenderer { + HtmlRenderer { render_tree: html2text::parse(html.as_bytes()), highlighter, - max_width, - rendered_html: None, - focus: None, - on_link: None, } } +} - pub fn set_on_link(&mut self, cb: F) - where - F: Fn(&mut cursive::Cursive, String) + 'static, - { - self.on_link = Some(rc::Rc::new(cb)); - } - - fn render(&self, width: usize) -> RenderedHtml { - let mut rendered_html = RenderedHtml::new(width); +impl cursive_markup::Renderer for HtmlRenderer { + fn render(&self, constraint: cursive::XY) -> cursive_markup::RenderedDocument { let decorator = utils::RichDecorator::new(show_link, utils::LinkMode::Annotate); let raw_lines = self .render_tree .clone() - .render(width, decorator) + .render(constraint.x, decorator) .into_lines(); let highlighted_lines = utils::highlight_html(&raw_lines, self.highlighter.as_ref()); - for (y, line) in highlighted_lines.enumerate() { - rendered_html.push_line(y, line); - } - rendered_html - } - - fn update(&mut self, constraint: cursive::XY) -> cursive::XY { - let width = cmp::min(self.max_width, constraint.x); - - // If we already have rendered the tree with the same width, we can reuse the cached data. - if let Some(rendered_html) = &self.rendered_html { - if rendered_html.width == width { - return rendered_html.size; - } - } - - let rendered_html = self.render(width); - - // Due to changed wrapping, the link count may have changed. So we have to make sure that - // our focus is still valid. - if let Some(focus) = self.focus { - // TODO: Ideally, we would also want to adjust the focus if a previous link was - // re-wrapped. - if focus >= rendered_html.links.len() { - self.focus = Some(rendered_html.links.len() - 1); - } - } - - let size = rendered_html.size; - self.rendered_html = Some(rendered_html); - size - } - - /// Returns the current focus and the list of links if both are available. - fn focus_and_links(&self) -> Option<(usize, &[Link])> { - match (self.focus, self.rendered_html.as_ref().map(|h| &h.links)) { - (Some(focus), Some(links)) => Some((focus, links)), - _ => None, + let mut doc = cursive_markup::RenderedDocument::new(constraint); + for line in highlighted_lines { + doc.push_line(line.into_iter().map(From::from)) } + doc } } -impl cursive::View for HtmlView { - fn draw(&self, printer: &cursive::Printer) { - let lines = &self - .rendered_html - .as_ref() - .expect("layout not called before draw") - .lines; - for (y, line) in lines.iter().enumerate() { - let mut x = 0; - for element in line { - let mut style = element.style; - if element.link_idx == self.focus && printer.focused { - style = style.combine(theme::PaletteColor::Highlight); - } - printer.with_style(style, |printer| printer.print((x, y), &element.text)); - x += element.text.len(); +impl<'s> From> for cursive_markup::Element { + fn from(e: utils::HighlightedHtmlElement<'s>) -> cursive_markup::Element { + match e { + utils::HighlightedHtmlElement::RichString(ts) => { + let tag: Tag = ts.tag.iter().collect(); + cursive_markup::Element::new(ts.s.clone(), tag.style, tag.link_target) } - } - } - - fn layout(&mut self, constraint: cursive::XY) { - self.update(constraint); - } - - fn required_size(&mut self, constraint: cursive::XY) -> cursive::XY { - self.update(constraint) - } - - fn take_focus(&mut self, direction: cursive::direction::Direction) -> bool { - let link_count = self - .rendered_html - .as_ref() - .map(|html| html.links.len()) - .unwrap_or_default(); - if link_count > 0 { - use cursive::direction::{Absolute, Direction, Relative}; - let focus = match direction { - Direction::Abs(abs) => match abs { - Absolute::Up | Absolute::Left | Absolute::None => 0, - Absolute::Down | Absolute::Right => link_count - 1, - }, - Direction::Rel(rel) => match rel { - Relative::Front => 0, - Relative::Back => link_count - 1, - }, - }; - self.focus = Some(focus); - true - } else { - false - } - } - - fn on_event(&mut self, event: event::Event) -> event::EventResult { - use event::{Event, EventResult, Key}; - - let (focus, links) = if let Some(val) = self.focus_and_links() { - val - } else { - return EventResult::Ignored; - }; - if links.is_empty() { - return EventResult::Ignored; - } - - match event { - Event::Key(Key::Left) => { - if focus == 0 { - EventResult::Ignored - } else if links[focus].position.y == links[focus - 1].position.y { - self.focus = Some(focus - 1); - EventResult::Consumed(None) - } else { - EventResult::Ignored - } + utils::HighlightedHtmlElement::StyledString(s) => { + let s = utils::reset_background(s); + cursive_markup::Element::styled( + s.s.to_owned(), + s.style.map_or_else(Default::default, From::from), + ) } - Event::Key(Key::Up) => { - let y = links[focus].position.y; - let next_focus = links[..focus] - .iter() - .enumerate() - .rev() - .find(|(_, link)| link.position.y < y) - .map(|(idx, _)| idx); - match next_focus { - Some(focus) => { - self.focus = Some(focus); - EventResult::Consumed(None) - } - None => EventResult::Ignored, - } - } - Event::Key(Key::Down) => { - let y = links[focus].position.y; - let next_focus = links - .iter() - .enumerate() - .skip(focus) - .find(|(_, link)| link.position.y > y) - .map(|(idx, _)| idx); - match next_focus { - Some(focus) => { - self.focus = Some(focus); - EventResult::Consumed(None) - } - None => EventResult::Ignored, - } - } - Event::Key(Key::Right) => { - if focus + 1 >= links.len() { - EventResult::Ignored - } else if links[focus].position.y == links[focus + 1].position.y { - self.focus = Some(focus + 1); - EventResult::Consumed(None) - } else { - EventResult::Ignored - } - } - Event::Key(Key::Enter) => { - let link = links[focus].target.clone(); - let cb = self - .on_link - .clone() - .map(|cb| event::Callback::from_fn(move |s| cb(s, link.clone()))); - EventResult::Consumed(cb) - } - _ => EventResult::Ignored, - } - } - - fn important_area(&self, _: cursive::XY) -> cursive::Rect { - if let Some((focus, links)) = self.focus_and_links() { - let origin = links[focus].position; - cursive::Rect::from_size(origin, (links[focus].width, 1)) - } else { - cursive::Rect::from((0, 0)) } } } @@ -234,82 +63,6 @@ fn show_link(url: &str) -> bool { !url.starts_with('#') } -#[derive(Clone, Debug)] -struct HtmlElement { - text: String, - style: theme::Style, - link_idx: Option, -} - -#[derive(Clone, Debug)] -struct Link { - position: cursive::XY, - target: String, - width: usize, -} - -#[derive(Clone, Debug)] -struct RenderedHtml { - width: usize, - size: cursive::XY, - lines: Vec>, - links: Vec, -} - -impl RenderedHtml { - pub fn new(width: usize) -> RenderedHtml { - RenderedHtml { - width, - size: (0, 0).into(), - lines: Vec::new(), - links: Vec::new(), - } - } - - pub fn push_link(&mut self, link: Link) -> usize { - self.links.push(link); - self.links.len() - 1 - } - - pub fn push_line(&mut self, y: usize, elements: Vec) { - let mut len = 0; - let mut line = Vec::new(); - - for element in elements { - let element = match element { - utils::HighlightedHtmlElement::RichString(ts) => { - let tag: Tag = ts.tag.iter().collect(); - HtmlElement { - text: ts.s.clone(), - style: tag.style, - link_idx: tag.link_target.map(|target| { - self.push_link(Link { - position: (len, y).into(), - target, - width: ts.s.len(), - }) - }), - } - } - utils::HighlightedHtmlElement::StyledString(s) => { - let s = utils::reset_background(s); - HtmlElement { - text: s.s.to_owned(), - style: s.style.map_or_else(Default::default, From::from), - link_idx: None, - } - } - }; - - len += element.text.len(); - line.push(element); - } - - self.lines.push(line); - self.size = self.size.stack_vertical(&(len, 1).into()); - } -} - #[derive(Clone, Debug, Default, PartialEq)] struct Tag { style: theme::Style,