1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: Apache-2.0 or MIT

//! A renderer for HTML documents.
//!
//! *Requires the `html` feature (enabled per default).*
//!
//! This module provides the [`Renderer`][] struct, a renderer that uses [`html2text`][] to render
//! HTML documents.  You can custommize the renderer by settting a [`TextDecorator`][] and a
//! [`Converter`][].  The [`TextDecorator`][] is used by [`html2text`][] to convert the HTML DOM
//! into annotated strings.  The [`Converter`][] is used by the renderer to interpret the
//! annotations and to extract the text format and links.
//!
//! [`html2text`]: https://docs.rs/html2text/latest/html2text/
//! [`TextDecorator`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/trait.TextDecorator.html
//! [`Renderer`]: struct.Renderer.html
//! [`Converter`]: trait.Converter.html

use cursive::theme;
use html2text::render::text_renderer;

use crate::{Element, RenderedDocument};

/// A renderer for HTML documents that uses the default rich text decorator and converter.
pub type RichRenderer = Renderer<text_renderer::RichDecorator, RichConverter>;

/// A renderer for HTML documents.
///
/// This renderer uses [`html2text`][] to parse and render an HTML document.  The provided document
/// is only parsed once: when the instance is constructed.  Then it is rendered every time the
/// width of the view changes.
///
/// You can custommize the renderer by settting a custom [`TextDecorator`][] and [`Converter`][].
/// The [`TextDecorator`][] is used by [`html2text`][] to convert the HTML DOM into annotated
/// strings.  The [`Converter`][] is used by the renderer to interpret the annotations and to
/// extract the text format and links.
///
/// Per default, the renderer uses the [`RichDecorator`][] and the [`RichConverter`][].
///
/// [`html2text`]: https://docs.rs/html2text/latest/html2text/
/// [`TextDecorator`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/trait.TextDecorator.html
/// [`RichDecorator`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/trait.RichDecorator.html
/// [`Converter`]: trait.Converter.html
/// [`RichConverter`]: trait.RichConverter.html
pub struct Renderer<D: text_renderer::TextDecorator + Clone, C: Converter<D::Annotation>> {
    render_tree: html2text::RenderTree,
    decorator: D,
    converter: C,
}

/// A converter for HTML annotations.
///
/// This trait extracts the text formatting and links from the annotations created by a
/// [`TextDecorator`][].
///
/// [`TextDecorator`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/trait.TextDecorator.html
pub trait Converter<A> {
    /// Returns the style for the given annotation (if any).
    fn get_style(&self, annotation: &A) -> Option<theme::Style>;

    /// Returns the link target for the given annotation (if any).
    fn get_link<'a>(&self, annotation: &'a A) -> Option<&'a str>;
}

/// A converter for [`RichAnnotation`][].
///
/// Besides the straightforward mappings of links and text effects, this converter styles links
/// with the underline effect and code snippets with the secondary palette color.
///
/// [`RichAnnotation`]: https://docs.rs/html2text/latest/html2text/render/text_renderer/enum.RichAnnotation.html
pub struct RichConverter;

impl Renderer<text_renderer::RichDecorator, RichConverter> {
    /// Creates a new renderer for the given HTML document using the default settings.
    pub fn new(html: &str) -> Renderer<text_renderer::RichDecorator, RichConverter> {
        Renderer::custom(html, text_renderer::RichDecorator::new(), RichConverter)
    }
}

impl<D: text_renderer::TextDecorator + Clone, C: Converter<D::Annotation>> Renderer<D, C> {
    /// Creates a new renderer for the given HTML document using a custom decorator and converter.
    pub fn custom(html: &str, decorator: D, converter: C) -> Renderer<D, C> {
        Renderer {
            render_tree: html2text::parse(html.as_bytes()),
            decorator,
            converter,
        }
    }
}

impl<D: text_renderer::TextDecorator + Clone, C: Converter<D::Annotation>> super::Renderer
    for Renderer<D, C>
{
    fn render(&self, constraint: cursive::XY<usize>) -> RenderedDocument {
        let mut doc = RenderedDocument::new(constraint);

        let lines = self
            .render_tree
            .clone()
            .render(std::cmp::max(5, constraint.x), self.decorator.clone())
            .into_lines();
        for line in lines {
            let mut elements = Vec::new();
            for element in line.iter() {
                if let text_renderer::TaggedLineElement::Str(ts) = element {
                    let styles: Vec<_> = ts
                        .tag
                        .iter()
                        .filter_map(|a| self.converter.get_style(a))
                        .collect();
                    let link_target = ts
                        .tag
                        .iter()
                        .find_map(|a| self.converter.get_link(a))
                        .map(ToOwned::to_owned);
                    elements.push(Element::new(
                        ts.s.clone(),
                        theme::Style::merge(&styles),
                        link_target,
                    ));
                }
            }
            doc.push_line(elements);
        }

        doc
    }
}

impl Converter<text_renderer::RichAnnotation> for RichConverter {
    fn get_style(&self, annotation: &text_renderer::RichAnnotation) -> Option<theme::Style> {
        use text_renderer::RichAnnotation;
        match annotation {
            RichAnnotation::Default => None,
            RichAnnotation::Link(_) => Some(theme::Effect::Underline.into()),
            RichAnnotation::Image => None,
            RichAnnotation::Emphasis => Some(theme::Effect::Italic.into()),
            RichAnnotation::Strong => Some(theme::Effect::Bold.into()),
            RichAnnotation::Strikeout => Some(theme::Effect::Strikethrough.into()),
            RichAnnotation::Code => Some(theme::PaletteColor::Secondary.into()),
            RichAnnotation::Preformat(_) => None,
        }
    }

    fn get_link<'a>(&self, annotation: &'a text_renderer::RichAnnotation) -> Option<&'a str> {
        if let text_renderer::RichAnnotation::Link(target) = annotation {
            Some(target)
        } else {
            None
        }
    }
}