cursive_markup/html.rs
1// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
2// SPDX-License-Identifier: Apache-2.0 or MIT
3
4//! A renderer for HTML documents.
5//!
6//! *Requires the `html` feature (enabled per default).*
7//!
8//! This module provides the [`Renderer`][] struct, a renderer that uses [`html2text`][] to render
9//! HTML documents. You can custommize the renderer by settting a [`TextDecorator`][] and a
10//! [`Converter`][]. The [`TextDecorator`][] is used by [`html2text`][] to convert the HTML DOM
11//! into annotated strings. The [`Converter`][] is used by the renderer to interpret the
12//! annotations and to extract the text format and links.
13//!
14//! [`html2text`]: https://docs.rs/html2text/latest/html2text/
15//! [`TextDecorator`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/trait.TextDecorator.html
16//! [`Renderer`]: struct.Renderer.html
17//! [`Converter`]: trait.Converter.html
18
19use cursive_core::theme;
20use html2text::render::text_renderer;
21
22use crate::{Element, RenderedDocument};
23
24/// A renderer for HTML documents that uses the default rich text decorator and converter.
25pub type RichRenderer = Renderer<text_renderer::RichDecorator, RichConverter>;
26
27/// A renderer for HTML documents.
28///
29/// This renderer uses [`html2text`][] to parse and render an HTML document. The provided document
30/// is only parsed once: when the instance is constructed. Then it is rendered every time the
31/// width of the view changes.
32///
33/// You can custommize the renderer by settting a custom [`TextDecorator`][] and [`Converter`][].
34/// The [`TextDecorator`][] is used by [`html2text`][] to convert the HTML DOM into annotated
35/// strings. The [`Converter`][] is used by the renderer to interpret the annotations and to
36/// extract the text format and links.
37///
38/// Per default, the renderer uses the [`RichDecorator`][] and the [`RichConverter`][].
39///
40/// [`html2text`]: https://docs.rs/html2text/latest/html2text/
41/// [`TextDecorator`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/trait.TextDecorator.html
42/// [`RichDecorator`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/trait.RichDecorator.html
43/// [`Converter`]: trait.Converter.html
44/// [`RichConverter`]: trait.RichConverter.html
45pub struct Renderer<D: text_renderer::TextDecorator + Clone, C: Converter<D::Annotation>> {
46 render_tree: html2text::RenderTree,
47 decorator: D,
48 converter: C,
49}
50
51/// A converter for HTML annotations.
52///
53/// This trait extracts the text formatting and links from the annotations created by a
54/// [`TextDecorator`][].
55///
56/// [`TextDecorator`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/trait.TextDecorator.html
57pub trait Converter<A> {
58 /// Returns the style for the given annotation (if any).
59 fn get_style(&self, annotation: &A) -> Option<theme::Style>;
60
61 /// Returns the link target for the given annotation (if any).
62 fn get_link<'a>(&self, annotation: &'a A) -> Option<&'a str>;
63}
64
65/// A converter for [`RichAnnotation`][].
66///
67/// Besides the straightforward mappings of links and text effects, this converter styles links
68/// with the underline effect and code snippets with the secondary palette color.
69///
70/// [`RichAnnotation`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/enum.RichAnnotation.html
71pub struct RichConverter;
72
73impl Renderer<text_renderer::RichDecorator, RichConverter> {
74 /// Creates a new renderer for the given HTML document using the default settings.
75 pub fn new(html: &str) -> Renderer<text_renderer::RichDecorator, RichConverter> {
76 Renderer::custom(html, text_renderer::RichDecorator::new(), RichConverter)
77 }
78}
79
80impl<D: text_renderer::TextDecorator + Clone, C: Converter<D::Annotation>> Renderer<D, C> {
81 /// Creates a new renderer for the given HTML document using a custom decorator and converter.
82 pub fn custom(html: &str, decorator: D, converter: C) -> Renderer<D, C> {
83 Renderer {
84 render_tree: html2text::parse(html.as_bytes()),
85 decorator,
86 converter,
87 }
88 }
89}
90
91impl<D: text_renderer::TextDecorator + Clone, C: Converter<D::Annotation>> super::Renderer
92 for Renderer<D, C>
93{
94 fn render(&self, constraint: cursive_core::XY<usize>) -> RenderedDocument {
95 let mut doc = RenderedDocument::new(constraint);
96
97 let lines = self
98 .render_tree
99 .clone()
100 .render(std::cmp::max(5, constraint.x), self.decorator.clone())
101 .into_lines();
102 for line in lines {
103 let mut elements = Vec::new();
104 for element in line.iter() {
105 if let text_renderer::TaggedLineElement::Str(ts) = element {
106 let styles: Vec<_> = ts
107 .tag
108 .iter()
109 .filter_map(|a| self.converter.get_style(a))
110 .collect();
111 let link_target = ts
112 .tag
113 .iter()
114 .find_map(|a| self.converter.get_link(a))
115 .map(ToOwned::to_owned);
116 elements.push(Element::new(
117 ts.s.clone(),
118 theme::Style::merge(&styles),
119 link_target,
120 ));
121 }
122 }
123 doc.push_line(elements);
124 }
125
126 doc
127 }
128}
129
130impl Converter<text_renderer::RichAnnotation> for RichConverter {
131 fn get_style(&self, annotation: &text_renderer::RichAnnotation) -> Option<theme::Style> {
132 use text_renderer::RichAnnotation;
133 match annotation {
134 RichAnnotation::Default => None,
135 RichAnnotation::Link(_) => Some(theme::Effect::Underline.into()),
136 RichAnnotation::Image => None,
137 RichAnnotation::Emphasis => Some(theme::Effect::Italic.into()),
138 RichAnnotation::Strong => Some(theme::Effect::Bold.into()),
139 RichAnnotation::Strikeout => Some(theme::Effect::Strikethrough.into()),
140 RichAnnotation::Code => Some(theme::PaletteColor::Secondary.into()),
141 RichAnnotation::Preformat(_) => None,
142 }
143 }
144
145 fn get_link<'a>(&self, annotation: &'a text_renderer::RichAnnotation) -> Option<&'a str> {
146 if let text_renderer::RichAnnotation::Link(target) = annotation {
147 Some(target)
148 } else {
149 None
150 }
151 }
152}