Skip to main content

rama_http/protocols/html/
join.rs

1//! Render an iterator of values as a separated (e.g. comma-separated) list,
2//! without the element type having to know anything about HTML.
3
4use std::fmt::{self, Write as _};
5
6use super::core::{IntoHtml, escape_into};
7
8/// Render the items of `iter` into HTML, separated by `sep`.
9///
10/// Each item is rendered through its [`Display`](std::fmt::Display) impl and
11/// HTML-escaped, so the element type only needs `Display` — it does **not**
12/// need to implement [`IntoHtml`]. The separator is written verbatim: it is
13/// treated as trusted, developer-provided structure, exactly like the static
14/// parts of a template (so the *data* is escaped, the *structure* is not).
15///
16/// This is the idiomatic way to build a CSV-style attribute value (or body)
17/// straight from an iterator, instead of pre-joining into a `String`:
18///
19/// ```ignore
20/// use rama_http::protocols::html::*;
21///
22/// // any `Display` iterator works; here: a list of language tags
23/// let langs = ["en", "fr", "de"];
24/// let tag = meta!("http-equiv" = "Content-Language", content = join_display(langs, ", "));
25/// assert_eq!(
26///     tag.into_string(),
27///     r#"<meta http-equiv="Content-Language" content="en, fr, de">"#,
28/// );
29/// ```
30pub fn join_display<I, S>(iter: I, sep: S) -> impl IntoHtml
31where
32    I: IntoIterator,
33    I::Item: fmt::Display,
34    S: AsRef<str>,
35{
36    // A `FnOnce(&mut String)` is itself `IntoHtml` (see `core`), so the
37    // closure is all we need — it renders lazily at write time.
38    move |buf: &mut String| {
39        let sep = sep.as_ref();
40        for (i, item) in iter.into_iter().enumerate() {
41            if i > 0 {
42                buf.push_str(sep);
43            }
44            // `Display` straight into the buffer, escaping on the fly so we
45            // never allocate a per-item scratch `String`.
46            _ = write!(EscapeWriter(buf), "{item}");
47        }
48    }
49}
50
51/// A [`fmt::Write`] that HTML-escapes everything written through it into the
52/// wrapped buffer. Escaping per write is correct because every escapable byte
53/// is a single ASCII byte, so chunk boundaries never split one.
54struct EscapeWriter<'a>(&'a mut String);
55
56impl fmt::Write for EscapeWriter<'_> {
57    #[inline]
58    fn write_str(&mut self, s: &str) -> fmt::Result {
59        escape_into(self.0, s);
60        Ok(())
61    }
62}