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(), + "" + ) +}