From 663247aef3618c3e35e6cf7d4f4f59bdc7ec729a Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Mon, 12 Jan 2015 15:52:11 +1300
Subject: [PATCH] Use an opaque Markup type instead of a bare closure

---
 maud/src/lib.rs            | 58 ++++++++++++++++++++++++++++++--------
 maud_macros/src/render.rs  |  9 +++---
 maud_macros/tests/tests.rs | 24 +++++++---------
 3 files changed, 62 insertions(+), 29 deletions(-)

diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index 7fdd9dc..54b79a8 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -4,12 +4,52 @@
 
 use std::fmt;
 use std::fmt::Writer as FmtWriter;
+use std::io::{IoError, IoErrorKind, IoResult};
 
 pub type FmtResult<T> = Result<T, fmt::Error>;
 
 /// Escape an HTML value.
 pub fn escape(s: &str) -> String {
-    render(|w| rt::escape(w, |w| w.write_str(s)))
+    let mut buf = String::new();
+    rt::escape(&mut buf, |w| w.write_str(s)).unwrap();
+    buf
+}
+
+/// A block of HTML markup, as returned by the `html!` macro.
+pub struct Markup<'a, 'b: 'a> {
+    callback: &'a (Fn(&mut FmtWriter) -> FmtResult<()> + 'b),
+}
+
+impl<'a, 'b> Markup<'a, 'b> {
+    /// Render the markup to a `String`.
+    pub fn render(&self) -> String {
+        let mut buf = String::new();
+        self.render_fmt(&mut buf).unwrap();
+        buf
+    }
+
+    /// Render the markup to a `std::io::Writer`.
+    pub fn render_to(&self, w: &mut Writer) -> IoResult<()> {
+        struct WriterWrapper<'a, 'b: 'a> {
+            inner: &'a mut (Writer + 'b),
+        }
+        impl<'a, 'b> FmtWriter for WriterWrapper<'a, 'b> {
+            fn write_str(&mut self, s: &str) -> FmtResult<()> {
+                self.inner.write_str(s).map_err(|_| fmt::Error)
+            }
+        }
+        self.render_fmt(&mut WriterWrapper { inner: w })
+            .map_err(|_| IoError {
+                kind: IoErrorKind::OtherIoError,
+                desc: "formatting error",
+                detail: None,
+            })
+    }
+
+    /// Render the markup to a `std::fmt::Writer`.
+    pub fn render_fmt(&self, w: &mut FmtWriter) -> FmtResult<()> {
+        (self.callback)(w)
+    }
 }
 
 /// Internal functions used by the `maud_macros` package. You should
@@ -18,7 +58,12 @@ pub fn escape(s: &str) -> String {
 Use the macros in `maud_macros` instead."]
 pub mod rt {
     use std::fmt::Writer as FmtWriter;
-    use super::FmtResult;
+    use super::{FmtResult, Markup};
+
+    #[inline]
+    pub fn make_markup<'a, 'b>(f: &'a (Fn(&mut FmtWriter) -> FmtResult<()> + 'b)) -> Markup<'a, 'b> {
+        Markup { callback: f }
+    }
 
     struct Escaper<'a, 'b: 'a> {
         inner: &'a mut (FmtWriter + 'b),
@@ -47,12 +92,3 @@ pub mod rt {
         f(&mut Escaper { inner: w })
     }
 }
-
-/// Render a template into a `String`.
-pub fn render<F>(template: F) -> String where
-    F: FnOnce(&mut FmtWriter) -> FmtResult<()>
-{
-    let mut buf = String::new();
-    template(&mut buf).unwrap();
-    buf
-}
diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs
index 3fc0c2e..d26d5d2 100644
--- a/maud_macros/src/render.rs
+++ b/maud_macros/src/render.rs
@@ -33,10 +33,11 @@ impl<'cx, 's: 'cx, 'o> Renderer<'cx, 's, 'o> {
             f(&mut render);
             render.cx
         };
-        quote_expr!(cx, |&: $w: &mut ::std::fmt::Writer| -> ::std::result::Result<(), ::std::fmt::Error> {
-            $stmts
-            Ok(())
-        })
+        quote_expr!(cx,
+            ::maud::rt::make_markup(&|&: $w: &mut ::std::fmt::Writer| -> Result<(), ::std::fmt::Error> {
+                $stmts
+                Ok(())
+            }))
     }
 
     /// Append a literal pre-escaped string.
diff --git a/maud_macros/tests/tests.rs b/maud_macros/tests/tests.rs
index 62f9e7b..07838ac 100644
--- a/maud_macros/tests/tests.rs
+++ b/maud_macros/tests/tests.rs
@@ -6,49 +6,45 @@ extern crate maud;
 
 #[test]
 fn it_works() {
-    let template = html!("du\tcks" -23 3.14 '\n' "geese");
-    let s = maud::render(template);
+    let s = html!("du\tcks" -23 3.14 '\n' "geese").render();
     assert_eq!(s, "du\tcks-233.14\ngeese");
 }
 
 #[test]
 fn escaping() {
-    let template = html!("<flim&flam>");
-    let s = maud::render(template);
+    let s = html!("<flim&flam>").render();
     assert_eq!(s, "&lt;flim&amp;flam&gt;");
 }
 
 #[test]
 fn blocks() {
-    let s = maud::render(html! {
+    let s = html! {
         "hello"
         {
             " ducks";
             " geese";
         }
         " swans"
-    });
+    }.render();
     assert_eq!(s, "hello ducks geese swans");
 }
 
 mod splice {
-    use super::maud;  // lol
-
     #[test]
     fn literal() {
-        let s = maud::render(html! { $"<pinkie>" });
+        let s = html! { $"<pinkie>" }.render();
         assert_eq!(s, "&lt;pinkie&gt;");
     }
 
     #[test]
     fn raw_literal() {
-        let s = maud::render(html! { $$"<pinkie>" });
+        let s = html! { $$"<pinkie>" }.render();
         assert_eq!(s, "<pinkie>");
     }
 
     #[test]
     fn block() {
-        let s = maud::render(html! {
+        let s = html! {
             ${
                 let mut result = 1i32;
                 for i in range(2, 11) {
@@ -56,7 +52,7 @@ mod splice {
                 }
                 result
             }
-        });
+        }.render();
         assert_eq!(s, "3628800");
     }
 
@@ -64,7 +60,7 @@ mod splice {
 
     #[test]
     fn statics() {
-        let s = maud::render(html! { $BEST_PONY });
+        let s = html! { $BEST_PONY }.render();
         assert_eq!(s, "Pinkie Pie");
     }
 
@@ -74,7 +70,7 @@ mod splice {
     #[test]
     fn closure() {
         let best_pony = "Pinkie Pie";
-        let s = maud::render(html! { $best_pony });
+        let s = html! { $best_pony }.render();
         assert_eq!(s, "Pinkie Pie");
     }
     */