Skip to content

Commit 5123fb9

Browse files
committed
WIP: Provide join_html and join_to_html.
1 parent 99af711 commit 5123fb9

File tree

1 file changed

+207
-0
lines changed

1 file changed

+207
-0
lines changed

src/templates/utils.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,210 @@ impl<'a> ToHtmlEscapingWriter<'a> {
139139
Ok(1)
140140
}
141141
}
142+
143+
/// Adapter interface providing `join_html` method.
144+
pub trait JoinHtml<I: Iterator> {
145+
/// Format the items of the given iterator, separated by `sep`.
146+
///
147+
/// The formatting is done by a given template (or template-like function).
148+
///
149+
/// # Examples
150+
///
151+
/// ```
152+
/// use ructe::templates::{JoinHtml, Html};
153+
/// # fn main() -> std::io::Result<()> {
154+
/// assert_eq!(
155+
/// [("Rasmus", "kaj"), ("Kalle", "karl")]
156+
/// .iter()
157+
/// .join_html(
158+
/// |o, (name, user)| {
159+
/// write!(o, "<a href=\"/profile/{}\">{}</a>", user, name)
160+
/// },
161+
/// Html("<br/>\n"),
162+
/// )
163+
/// .to_buffer()?,
164+
/// "<a href=\"/profile/kaj\">Rasmus</a><br/>\
165+
/// \n<a href=\"/profile/karl\">Kalle</a>"
166+
/// );
167+
/// # Ok(())
168+
/// # }
169+
/// ```
170+
///
171+
/// Note that the callback function is responsible for any html
172+
/// escaping of the argument.
173+
/// The closure with the write function above don't do any
174+
/// escaping, it worked fine only because the names and user-names
175+
/// in the example did not contain any characters requireing escaping.
176+
///
177+
/// One nice way to get a function that handles escaping is to use
178+
/// a template function as the formatting callback.
179+
///
180+
/// If the the following template is `link.rs.html`:
181+
/// ```ructe
182+
/// @((title, slug): &(&str, &str))
183+
/// <a href=\"/album/@slug\">@title</a>
184+
/// ```
185+
///
186+
/// It can be used like this in rust code:
187+
/// ```
188+
/// # // Mock the above template
189+
/// # use std::io;
190+
/// # use ructe::templates::ToHtml;
191+
/// # fn link(o: &mut dyn io::Write, (title, slug): &(&str, &str)) -> io::Result<()> {
192+
/// # o.write_all(b"<a href=\"/album/")?;
193+
/// # slug.to_html(o)?;
194+
/// # o.write_all(b"\">")?;
195+
/// # title.to_html(o)?;
196+
/// # o.write_all(b"</a>")
197+
/// # }
198+
/// use ructe::templates::{Html, JoinHtml};
199+
/// # fn main() -> std::io::Result<()> {
200+
/// assert_eq!(
201+
/// [("Spirou & Fantasio", "spirou"), ("Tom & Jerry", "tom_jerry")]
202+
/// .iter()
203+
/// .join_html(link, Html("<br/>\n"))
204+
/// .to_buffer()?,
205+
/// "<a href=\"/album/spirou\">Spirou &amp; Fantasio</a><br/>\
206+
/// \n<a href=\"/album/tom_jerry\">Tom &amp; Jerry</a>"
207+
/// );
208+
/// # Ok(())
209+
/// # }
210+
/// ```
211+
///
212+
/// Or like this in a template, giving similar result:
213+
/// ```ructe
214+
/// @use super::{link, Html, JoinHtml};
215+
///
216+
/// @(comics: &[(&str, &str)])
217+
/// <div class="containing markup">
218+
/// @comics.iter().to_html(link, Html("<br/>"))
219+
/// </div>
220+
/// ```
221+
fn join_html<
222+
F: 'static + Fn(&mut dyn Write, I::Item) -> io::Result<()>,
223+
Sep: 'static + ToHtml,
224+
>(
225+
self,
226+
item_template: F,
227+
sep: Sep,
228+
) -> Box<dyn ToHtml>;
229+
}
230+
231+
/// Adapter interface providing `join_to_html` method.
232+
pub trait JoinToHtml<Item: ToHtml, I: Iterator<Item = Item>> {
233+
/// Format the items of the given iterator, separated by `sep`.
234+
///
235+
/// # Example
236+
///
237+
/// ```
238+
/// use ructe::templates::JoinToHtml;
239+
/// # fn main() -> std::io::Result<()> {
240+
/// assert_eq!(
241+
/// ["foo", "b<a", "baz"]
242+
/// .iter()
243+
/// .join_to_html(" & ")
244+
/// .to_buffer()?,
245+
/// "foo &amp; b&lt;a &amp; baz"
246+
/// );
247+
/// # Ok(())
248+
/// # }
249+
/// ```
250+
fn join_to_html<Sep: 'static + ToHtml>(self, sep: Sep)
251+
-> Box<dyn ToHtml>;
252+
}
253+
254+
impl<I: 'static + Iterator + Clone> JoinHtml<I> for I {
255+
fn join_html<
256+
F: 'static + Fn(&mut dyn Write, I::Item) -> io::Result<()>,
257+
Sep: 'static + ToHtml,
258+
>(
259+
self,
260+
item_template: F,
261+
sep: Sep,
262+
) -> Box<dyn ToHtml> {
263+
Box::new(HtmlJoiner {
264+
items: self,
265+
f: item_template,
266+
sep,
267+
})
268+
}
269+
}
270+
271+
impl<Item: ToHtml, Iter: 'static + Iterator<Item = Item> + Clone>
272+
JoinToHtml<Item, Iter> for Iter
273+
{
274+
fn join_to_html<Sep: 'static + ToHtml>(
275+
self,
276+
sep: Sep,
277+
) -> Box<dyn ToHtml> {
278+
Box::new(HtmlJoiner {
279+
items: self,
280+
f: |o, i| i.to_html(o),
281+
sep,
282+
})
283+
}
284+
}
285+
286+
struct HtmlJoiner<
287+
Items: Iterator + Clone,
288+
F: Fn(&mut dyn Write, Items::Item) -> io::Result<()>,
289+
Sep: ToHtml,
290+
> {
291+
items: Items,
292+
f: F,
293+
sep: Sep,
294+
}
295+
296+
impl<
297+
Items: Iterator + Clone,
298+
F: Fn(&mut dyn Write, Items::Item) -> io::Result<()>,
299+
Sep: ToHtml,
300+
> ToHtml for HtmlJoiner<Items, F, Sep>
301+
{
302+
fn to_html(&self, out: &mut dyn Write) -> io::Result<()> {
303+
let mut iter = self.items.clone();
304+
if let Some(first) = iter.next() {
305+
(self.f)(out, first)?;
306+
} else {
307+
return Ok(());
308+
}
309+
for item in iter {
310+
self.sep.to_html(out)?;
311+
(self.f)(out, item)?;
312+
}
313+
Ok(())
314+
}
315+
}
316+
317+
#[test]
318+
fn test_join_to_html() {
319+
assert_eq!(
320+
["foo", "b<a", "baz"]
321+
.iter()
322+
.join_to_html(", ")
323+
.to_buffer()
324+
.unwrap(),
325+
"foo, b&lt;a, baz"
326+
)
327+
}
328+
329+
#[test]
330+
fn test_join_to_html_empty() {
331+
use std::iter::empty;
332+
assert_eq!(empty::<&str>().join_to_html(", ").to_buffer().unwrap(), "")
333+
}
334+
335+
#[test]
336+
fn test_join_html_empty() {
337+
use std::iter::empty;
338+
assert_eq!(
339+
empty::<&str>()
340+
.join_html(
341+
|_o, _s| panic!("The callback should never be called"),
342+
", ",
343+
)
344+
.to_buffer()
345+
.unwrap(),
346+
""
347+
)
348+
}

0 commit comments

Comments
 (0)