ahtml/util/
softpre.rs

1use anyhow::Result;
2
3use crate::{att, AId, HtmlAllocator, Node, ToASlice, NBSP};
4
5use super::autolink;
6
7/// Create plain text to HTML that mimicks `<pre>`, but still allows
8/// dynamic line breaks
9pub struct SoftPre {
10    /// Whether to replace tabs with the given number of non-breaking
11    /// spaces.
12    pub tabs_to_nbsp: Option<u8>,
13
14    /// Whether to turn http:// and https:// URLs into links.
15    pub autolink: bool,
16
17    /// The input text is split on this string into lines
18    pub input_line_separator: &'static str,
19
20    /// Whether to add a trailing `<br>`
21    pub trailing_br: bool,
22}
23
24impl Default for SoftPre {
25    fn default() -> Self {
26        Self {
27            tabs_to_nbsp: Some(8),
28            autolink: true,
29            input_line_separator: "\n",
30            trailing_br: true,
31        }
32    }
33}
34
35impl SoftPre {
36    pub fn format(&self, text: &str, html: &HtmlAllocator) -> Result<AId<Node>> {
37        let mut formatted_body = html.new_vec();
38        let mut iter = text.split(self.input_line_separator).peekable();
39        while let Some(line) = iter.next() {
40            let mut formatted_line = if self.autolink {
41                autolink(html, line)?
42            } else {
43                html.text(line)?.to_aslice(html)?
44            };
45
46            if let Some(n) = self.tabs_to_nbsp {
47                let mut items = html.new_vec();
48                for id in formatted_line.iter_aid(html) {
49                    match html.get_node(id).expect("todo: when can this fail?") {
50                        Node::String(s) => {
51                            let mut s2 = String::new();
52                            for c in s.chars() {
53                                if c == '\t' {
54                                    for _ in 0..n {
55                                        s2.push_str(NBSP)
56                                    }
57                                } else {
58                                    s2.push(c)
59                                }
60                            }
61                            items.push(html.text(s2)?)?;
62                        }
63                        _ => items.push(id)?,
64                    }
65                }
66                formatted_line = items.as_slice();
67            }
68
69            // (Future: also map over the items and replace
70            // multiple-space sections in all the text segments with
71            // space/nbsp alterations? Difficulty is those ending up
72            // on the next line for wrapped lines, though?)
73
74            formatted_body.append(formatted_line)?;
75
76            if self.trailing_br || iter.peek().is_some() {
77                formatted_body.push(html.br([], [])?)?;
78            }
79        }
80        html.span([att("class", "soft_pre")], formatted_body)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use crate::Print;
87
88    use super::*;
89
90    #[test]
91    fn t_softpre() -> Result<()> {
92        let softpre = SoftPre::default();
93        let html = HtmlAllocator::new(1000, std::sync::Arc::new(""));
94        let t = |s| -> String {
95            softpre
96                .format(s, &html)
97                .unwrap()
98                .to_html_fragment_string(&html)
99                .unwrap()
100        };
101        assert_eq!(t("foo bar"), "<span class=\"soft_pre\">foo bar<br></span>");
102        assert_eq!(t("foo bar\n\tbaz"), "<span class=\"soft_pre\">foo bar<br>\u{a0}\u{a0}\u{a0}\u{a0}\u{a0}\u{a0}\u{a0}\u{a0}baz<br></span>");
103        Ok(())
104    }
105}