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}