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}