Skip to main content

laterite_admin/
html.rs

1//! Trusted HTML markup.
2//!
3//! [`Markup`] wraps a `String` of escaped HTML: what a field type renders, into a
4//! template's `|safe` slot. Produced only via [`Markup::from_template`] (Askama
5//! auto-escapes), [`Markup::from_override`] (engine-autoescaped), or the audited
6//! [`Markup::raw_unchecked`] trapdoor. An escaping element builder joins this
7//! when a field type first hand-builds a fragment.
8
9use askama::Template;
10
11/// Trusted, escaped HTML.
12#[derive(Debug, Clone, Default)]
13pub struct Markup(String);
14
15impl Markup {
16    /// Empty markup.
17    pub fn empty() -> Self {
18        Self(String::new())
19    }
20
21    /// Renders an Askama template (which auto-escapes) into trusted markup.
22    pub fn from_template<T: Template>(t: &T) -> Result<Self, askama::Error> {
23        Ok(Self(t.render()?))
24    }
25
26    /// Wraps a string as trusted HTML with **no escaping**. An audit point (CI
27    /// greps for it); use only when the input is provably safe.
28    pub fn raw_unchecked(html: impl Into<String>) -> Self {
29        Self(html.into())
30    }
31
32    /// Wraps an [`crate::field::OverrideResolver`]'s output (engine-autoescaped).
33    /// Resolver-only, so a provider hands us a `String`, never a raw `Markup`.
34    pub(crate) fn from_override(rendered: String) -> Self {
35        Self(rendered)
36    }
37
38    /// The HTML as a string slice (for the template `|safe` slot).
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42
43    /// Consumes into the owned HTML string.
44    pub fn into_string(self) -> String {
45        self.0
46    }
47
48    /// Appends another fragment.
49    pub fn push(&mut self, other: Markup) {
50        self.0.push_str(&other.0);
51    }
52}
53
54impl std::fmt::Display for Markup {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.write_str(&self.0)
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use std::path::Path;
63
64    /// Grep-gate: `raw_unchecked` may only appear in this module, so every
65    /// unescaped-HTML site is here and audited. A call elsewhere fails until
66    /// reviewed and allowlisted.
67    #[test]
68    fn raw_html_is_confined_to_the_html_module() {
69        let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
70        let mut offenders = Vec::new();
71        visit(&src, &mut |path, text| {
72            if path.file_name().and_then(|n| n.to_str()) == Some("html.rs") {
73                return;
74            }
75            if text.contains("raw_unchecked") {
76                offenders.push(path.display().to_string());
77            }
78        });
79        assert!(
80            offenders.is_empty(),
81            "`raw_unchecked` used outside html.rs; audit each and update this allowlist: {offenders:?}"
82        );
83    }
84
85    fn visit(dir: &Path, f: &mut impl FnMut(&Path, &str)) {
86        for entry in std::fs::read_dir(dir).unwrap().flatten() {
87            let path = entry.path();
88            if path.is_dir() {
89                visit(&path, f);
90            } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
91                let text = std::fs::read_to_string(&path).unwrap_or_default();
92                f(&path, &text);
93            }
94        }
95    }
96}