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:
parent
4c5d5808aa
commit
568fb0acc8
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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<utils::DocLink> for ResolvedLink {
|
||||
fn resolve_link(
|
||||
doc_name: &doc::Fqn,
|
||||
doc_ty: doc::ItemType,
|
||||
link: String,
|
||||
link: &str,
|
||||
) -> anyhow::Result<ResolvedLink> {
|
||||
// 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))),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<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 {
|
||||
pub fn new(html: &str, highlighter: Option<utils::Highlighter>, max_width: usize) -> HtmlView {
|
||||
HtmlView {
|
||||
impl HtmlRenderer {
|
||||
pub fn new(html: &str, highlighter: Option<utils::Highlighter>) -> 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<F>(&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<usize>) -> 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<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,
|
||||
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<utils::HighlightedHtmlElement<'s>> 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<usize>) {
|
||||
self.update(constraint);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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<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('#')
|
||||
}
|
||||
|
||||
#[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)]
|
||||
struct Tag {
|
||||
style: theme::Style,
|
||||
|
Loading…
Reference in New Issue
Block a user