diff --git a/examples/simple/src/main.rs b/examples/simple/src/main.rs
index 835326a..a449e32 100644
--- a/examples/simple/src/main.rs
+++ b/examples/simple/src/main.rs
@@ -338,3 +338,11 @@ fn lifetimes2() {
"\n
foo
\n\n bar
\n\n",
);
}
+
+#[test]
+fn test_list_join() {
+ assert_eq!(
+ r2s(|o| list_joins_html(o, &[2, 3, 7])),
+ "Items: 2, 3, 7.
\n",
+ )
+}
diff --git a/examples/simple/templates/list_joins.rs.html b/examples/simple/templates/list_joins.rs.html
new file mode 100644
index 0000000..cfdc92f
--- /dev/null
+++ b/examples/simple/templates/list_joins.rs.html
@@ -0,0 +1,6 @@
+@use super::{number_item_html, JoinHtml, JoinToHtml};
+
+@(items: &[u8])
+
+Items: @items.iter().join_to_html(", ").
+Items: @items.iter().cloned().join_html(number_item_html, ", ").
diff --git a/examples/simple/templates/number_item.rs.html b/examples/simple/templates/number_item.rs.html
new file mode 100644
index 0000000..31c8f88
--- /dev/null
+++ b/examples/simple/templates/number_item.rs.html
@@ -0,0 +1,2 @@
+@(item: u8)
+#@item
diff --git a/src/templates/utils.rs b/src/templates/utils.rs
index 15e741d..fd337d8 100644
--- a/src/templates/utils.rs
+++ b/src/templates/utils.rs
@@ -139,3 +139,210 @@ impl<'a> ToHtmlEscapingWriter<'a> {
Ok(1)
}
}
+
+/// Adapter interface providing `join_html` method.
+pub trait JoinHtml {
+ /// Format the items of the given iterator, separated by `sep`.
+ ///
+ /// The formatting is done by a given template (or template-like function).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ructe::templates::{JoinHtml, Html};
+ /// # fn main() -> std::io::Result<()> {
+ /// assert_eq!(
+ /// [("Rasmus", "kaj"), ("Kalle", "karl")]
+ /// .iter()
+ /// .join_html(
+ /// |o, (name, user)| {
+ /// write!(o, "{}", user, name)
+ /// },
+ /// Html("
\n"),
+ /// )
+ /// .to_buffer()?,
+ /// "Rasmus
\
+ /// \nKalle"
+ /// );
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// Note that the callback function is responsible for any html
+ /// escaping of the argument.
+ /// The closure with the write function above don't do any
+ /// escaping, it worked fine only because the names and user-names
+ /// in the example did not contain any characters requireing escaping.
+ ///
+ /// One nice way to get a function that handles escaping is to use
+ /// a template function as the formatting callback.
+ ///
+ /// If the the following template is `link.rs.html`:
+ /// ```ructe
+ /// @((title, slug): &(&str, &str))
+ /// @title
+ /// ```
+ ///
+ /// It can be used like this in rust code:
+ /// ```
+ /// # // Mock the above template
+ /// # use std::io;
+ /// # use ructe::templates::ToHtml;
+ /// # fn link(o: &mut dyn io::Write, (title, slug): &(&str, &str)) -> io::Result<()> {
+ /// # o.write_all(b"")?;
+ /// # title.to_html(o)?;
+ /// # o.write_all(b"")
+ /// # }
+ /// use ructe::templates::{Html, JoinHtml};
+ /// # fn main() -> std::io::Result<()> {
+ /// assert_eq!(
+ /// [("Spirou & Fantasio", "spirou"), ("Tom & Jerry", "tom_jerry")]
+ /// .iter()
+ /// .join_html(link, Html("
\n"))
+ /// .to_buffer()?,
+ /// "Spirou & Fantasio
\
+ /// \nTom & Jerry"
+ /// );
+ /// # Ok(())
+ /// # }
+ /// ```
+ ///
+ /// Or like this in a template, giving similar result:
+ /// ```ructe
+ /// @use super::{link, Html, JoinHtml};
+ ///
+ /// @(comics: &[(&str, &str)])
+ ///
+ /// @comics.iter().to_html(link, Html("
"))
+ ///
+ /// ```
+ fn join_html<
+ F: 'static + Fn(&mut dyn Write, I::Item) -> io::Result<()>,
+ Sep: 'static + ToHtml,
+ >(
+ self,
+ item_template: F,
+ sep: Sep,
+ ) -> Box;
+}
+
+/// Adapter interface providing `join_to_html` method.
+pub trait JoinToHtml> {
+ /// Format the items of the given iterator, separated by `sep`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use ructe::templates::JoinToHtml;
+ /// # fn main() -> std::io::Result<()> {
+ /// assert_eq!(
+ /// ["foo", "b(self, sep: Sep)
+ -> Box;
+}
+
+impl JoinHtml for I {
+ fn join_html<
+ F: 'static + Fn(&mut dyn Write, I::Item) -> io::Result<()>,
+ Sep: 'static + ToHtml,
+ >(
+ self,
+ item_template: F,
+ sep: Sep,
+ ) -> Box {
+ Box::new(HtmlJoiner {
+ items: self,
+ f: item_template,
+ sep,
+ })
+ }
+}
+
+impl + Clone>
+ JoinToHtml- for Iter
+{
+ fn join_to_html(
+ self,
+ sep: Sep,
+ ) -> Box {
+ Box::new(HtmlJoiner {
+ items: self,
+ f: |o, i| i.to_html(o),
+ sep,
+ })
+ }
+}
+
+struct HtmlJoiner<
+ Items: Iterator + Clone,
+ F: Fn(&mut dyn Write, Items::Item) -> io::Result<()>,
+ Sep: ToHtml,
+> {
+ items: Items,
+ f: F,
+ sep: Sep,
+}
+
+impl<
+ Items: Iterator + Clone,
+ F: Fn(&mut dyn Write, Items::Item) -> io::Result<()>,
+ Sep: ToHtml,
+ > ToHtml for HtmlJoiner
+{
+ fn to_html(&self, out: &mut dyn Write) -> io::Result<()> {
+ let mut iter = self.items.clone();
+ if let Some(first) = iter.next() {
+ (self.f)(out, first)?;
+ } else {
+ return Ok(());
+ }
+ for item in iter {
+ self.sep.to_html(out)?;
+ (self.f)(out, item)?;
+ }
+ Ok(())
+ }
+}
+
+#[test]
+fn test_join_to_html() {
+ assert_eq!(
+ ["foo", "b().join_to_html(", ").to_buffer().unwrap(), "")
+}
+
+#[test]
+fn test_join_html_empty() {
+ use std::iter::empty;
+ assert_eq!(
+ empty::<&str>()
+ .join_html(
+ |_o, _s| panic!("The callback should never be called"),
+ ", ",
+ )
+ .to_buffer()
+ .unwrap(),
+ ""
+ )
+}