stylish_html/
html.rs

1use core::fmt;
2
3use askama_escape::{escape, Html as AskamaHtml};
4use stylish_core::{Style, Write};
5
6use crate::util;
7
8/// An adaptor to allow writing [`stylish`] attributed data to an output stream
9/// by turning attributes into HTML elements.
10///
11/// ```rust
12/// let mut writer = stylish::Html::new(String::new());
13/// stylish::write!(writer, "Hello {:(fg=red)}", "Ferris")?;
14/// assert_eq!(
15///     writer.finish()?,
16///     "Hello <span style=color:red>Ferris</span>",
17/// );
18/// # Ok::<(), core::fmt::Error>(())
19/// ```
20#[derive(Clone, Debug, Default)]
21pub struct Html<T: core::fmt::Write> {
22    inner: T,
23    current: Style,
24}
25
26impl<T: core::fmt::Write> Html<T> {
27    /// Wrap the given output stream in this adaptor.
28    pub fn new(inner: T) -> Self {
29        Self {
30            inner,
31            current: Style::default(),
32        }
33    }
34
35    /// Inherent delegation to
36    /// [`stylish::Write::write_fmt`](stylish_core::Write::write_fmt) to not
37    /// require a trait import.
38    pub fn write_fmt(&mut self, args: stylish_core::Arguments<'_>) -> fmt::Result {
39        stylish_core::Write::write_fmt(self, args)
40    }
41
42    /// Ensure the output stream is reset back to the default style and return
43    /// it, if you don't call this the stream will be left in whatever style
44    /// the last output data was.
45    pub fn finish(mut self) -> Result<T, fmt::Error> {
46        if self.current != Style::default() {
47            self.inner.write_str("</span>")?;
48        }
49        Ok(self.inner)
50    }
51}
52
53impl<T: fmt::Write> Write for Html<T> {
54    fn write_str(&mut self, s: &str, style: Style) -> fmt::Result {
55        if s.is_empty() {
56            return Ok(());
57        }
58
59        if style == Style::default() {
60            if self.current != Style::default() {
61                self.inner.write_str("</span>")?;
62            }
63        } else if style != self.current {
64            let diff = style.diff_from(Style::default());
65            let segments = [
66                diff.foreground.map(util::foreground),
67                diff.background.map(util::background),
68                diff.intensity.map(util::intensity),
69            ];
70            let mut segments = segments.iter().filter_map(|&s| s);
71            if let Some(segment) = segments.next() {
72                if self.current != Style::default() {
73                    self.inner.write_str("</span>")?;
74                }
75                self.inner.write_str("<span style=")?;
76                self.inner.write_str(segment)?;
77                for segment in segments {
78                    self.inner.write_str(";")?;
79                    self.inner.write_str(segment)?;
80                }
81                self.inner.write_str(">")?;
82            }
83        }
84        self.current = style;
85
86        write!(self.inner, "{}", escape(s, AskamaHtml))?;
87
88        Ok(())
89    }
90}