Use cursive-markup to replace HtmlView

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.
This commit is contained in:
Robin Krahl 2020-10-08 10:00:26 +02:00
parent 4c5d5808aa
commit 568fb0acc8
No known key found for this signature in database
GPG Key ID: 8E9B0870524F69D8
4 changed files with 51 additions and 288 deletions

12
Cargo.lock generated
View File

@ -236,6 +236,16 @@ dependencies = [
"unicode-width 0.1.8 (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-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]] [[package]]
name = "cursive_core" name = "cursive_core"
version = "0.1.1" version = "0.1.1"
@ -1005,6 +1015,7 @@ dependencies = [
"assert_cmd 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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 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)", "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)", "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)", "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 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 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 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 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 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" "checksum darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"

View File

@ -20,6 +20,7 @@ ansi_term = "0.12.1"
anyhow = "1.0.31" anyhow = "1.0.31"
atty = "0.2.14" atty = "0.2.14"
cursive = "0.15.0" cursive = "0.15.0"
cursive-markup = "0.1"
html2text = "0.2.1" html2text = "0.2.1"
kuchiki = "0.8.0" kuchiki = "0.8.0"
log = "0.4.11" log = "0.4.11"

View File

@ -9,13 +9,14 @@ use anyhow::Context as _;
use cursive::view::{Resizable as _, Scrollable as _}; use cursive::view::{Resizable as _, Scrollable as _};
use cursive::views::{Dialog, LinearLayout, PaddedView, Panel, TextView}; use cursive::views::{Dialog, LinearLayout, PaddedView, Panel, TextView};
use cursive::{event, theme, utils::markup}; use cursive::{event, theme, utils::markup};
use cursive_markup::MarkupView;
use crate::args; use crate::args;
use crate::doc; use crate::doc;
use crate::source; use crate::source;
use crate::viewer::{self, utils, utils::ManRenderer as _}; use crate::viewer::{self, utils, utils::ManRenderer as _};
use views::{CodeView, HtmlView, LinkView}; use views::{CodeView, HtmlRenderer, LinkView};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TuiViewer {} 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> { fn print_text(&mut self, indent: u8, text: &doc::Text) -> Result<(), Self::Error> {
let indent = usize::from(indent); let indent = usize::from(indent);
let mut text = HtmlView::new( let renderer = HtmlRenderer::new(&text.html, self.highlighter.cloned());
&text.html, let mut view = MarkupView::with_renderer(renderer);
self.highlighter.cloned(), view.set_maximum_width(self.max_width.saturating_sub(indent));
self.max_width.saturating_sub(indent),
);
let doc_name = self.doc_name.clone(); let doc_name = self.doc_name.clone();
let doc_ty = self.doc_ty; let doc_ty = self.doc_ty;
text.set_on_link(move |s, link| handle_link(s, &doc_name.clone(), doc_ty, link)); view.on_link_select(move |s, link| handle_link(s, &doc_name, doc_ty, link));
self.layout.add_child(indent_view(indent, text)); self.layout.add_child(indent_view(indent, view));
Ok(()) Ok(())
} }
@ -238,7 +237,7 @@ fn report_error(s: &mut cursive::Cursive, error: anyhow::Error) {
s.add_layer(dialog); 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)); let result = resolve_link(doc_name, doc_ty, link).and_then(|link| open_link(s, link));
if let Err(err) = result { if let Err(err) = result {
report_error(s, err); report_error(s, err);
@ -291,16 +290,14 @@ impl From<utils::DocLink> for ResolvedLink {
fn resolve_link( fn resolve_link(
doc_name: &doc::Fqn, doc_name: &doc::Fqn,
doc_ty: doc::ItemType, doc_ty: doc::ItemType,
link: String, link: &str,
) -> anyhow::Result<ResolvedLink> { ) -> anyhow::Result<ResolvedLink> {
// TODO: support docs.rs and doc.rust-lang.org links // TODO: support docs.rs and doc.rust-lang.org links
match url::Url::parse(&link) { match url::Url::parse(link) {
Ok(_) => Ok(ResolvedLink::External(link)), Ok(_) => Ok(ResolvedLink::External(link.to_owned())),
Err(url::ParseError::RelativeUrlWithoutBase) => resolve_doc_link(doc_name, doc_ty, &link) Err(url::ParseError::RelativeUrlWithoutBase) => resolve_doc_link(doc_name, doc_ty, link)
.with_context(|| format!("Could not parse relative link URL: {}", &link)), .with_context(|| format!("Could not parse relative link URL: {}", link)),
Err(e) => { Err(e) => Err(anyhow::Error::new(e).context(format!("Could not parse link URL: {}", link))),
Err(anyhow::Error::new(e).context(format!("Could not parse link URL: {}", &link)))
}
} }
} }

View File

@ -3,228 +3,57 @@
use std::cmp; use std::cmp;
use std::iter; use std::iter;
use std::rc;
use cursive::{event, theme, utils::markup}; use cursive::{event, theme, utils::markup};
use html2text::render::text_renderer; use html2text::render::text_renderer;
use crate::viewer::utils; use crate::viewer::utils;
pub struct HtmlView { pub struct HtmlRenderer {
render_tree: html2text::RenderTree, render_tree: html2text::RenderTree,
highlighter: Option<utils::Highlighter>, highlighter: Option<utils::Highlighter>,
max_width: usize,
rendered_html: Option<RenderedHtml>,
focus: Option<usize>,
on_link: Option<rc::Rc<dyn Fn(&mut cursive::Cursive, String)>>,
} }
impl HtmlView { impl HtmlRenderer {
pub fn new(html: &str, highlighter: Option<utils::Highlighter>, max_width: usize) -> HtmlView { pub fn new(html: &str, highlighter: Option<utils::Highlighter>) -> HtmlRenderer {
HtmlView { HtmlRenderer {
render_tree: html2text::parse(html.as_bytes()), render_tree: html2text::parse(html.as_bytes()),
highlighter, highlighter,
max_width,
rendered_html: None,
focus: None,
on_link: None,
} }
} }
}
pub fn set_on_link<F>(&mut self, cb: F) impl cursive_markup::Renderer for HtmlRenderer {
where fn render(&self, constraint: cursive::XY<usize>) -> cursive_markup::RenderedDocument {
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);
let decorator = utils::RichDecorator::new(show_link, utils::LinkMode::Annotate); let decorator = utils::RichDecorator::new(show_link, utils::LinkMode::Annotate);
let raw_lines = self let raw_lines = self
.render_tree .render_tree
.clone() .clone()
.render(width, decorator) .render(constraint.x, decorator)
.into_lines(); .into_lines();
let highlighted_lines = utils::highlight_html(&raw_lines, self.highlighter.as_ref()); let highlighted_lines = utils::highlight_html(&raw_lines, self.highlighter.as_ref());
for (y, line) in highlighted_lines.enumerate() { let mut doc = cursive_markup::RenderedDocument::new(constraint);
rendered_html.push_line(y, line); for line in highlighted_lines {
} doc.push_line(line.into_iter().map(From::from))
rendered_html
}
fn update(&mut self, constraint: cursive::XY<usize>) -> cursive::XY<usize> {
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,
} }
doc
} }
} }
impl cursive::View for HtmlView { impl<'s> From<utils::HighlightedHtmlElement<'s>> for cursive_markup::Element {
fn draw(&self, printer: &cursive::Printer) { fn from(e: utils::HighlightedHtmlElement<'s>) -> cursive_markup::Element {
let lines = &self match e {
.rendered_html utils::HighlightedHtmlElement::RichString(ts) => {
.as_ref() let tag: Tag = ts.tag.iter().collect();
.expect("layout not called before draw") cursive_markup::Element::new(ts.s.clone(), tag.style, tag.link_target)
.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();
} }
} utils::HighlightedHtmlElement::StyledString(s) => {
} let s = utils::reset_background(s);
cursive_markup::Element::styled(
fn layout(&mut self, constraint: cursive::XY<usize>) { s.s.to_owned(),
self.update(constraint); s.style.map_or_else(Default::default, From::from),
} )
fn required_size(&mut self, constraint: cursive::XY<usize>) -> cursive::XY<usize> {
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
}
} }
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<usize>) -> 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('#') !url.starts_with('#')
} }
#[derive(Clone, Debug)]
struct HtmlElement {
text: String,
style: theme::Style,
link_idx: Option<usize>,
}
#[derive(Clone, Debug)]
struct Link {
position: cursive::XY<usize>,
target: String,
width: usize,
}
#[derive(Clone, Debug)]
struct RenderedHtml {
width: usize,
size: cursive::XY<usize>,
lines: Vec<Vec<HtmlElement>>,
links: Vec<Link>,
}
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<utils::HighlightedHtmlElement>) {
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)] #[derive(Clone, Debug, Default, PartialEq)]
struct Tag { struct Tag {
style: theme::Style, style: theme::Style,