From 1badbd9df299daf559d90012cbbf57d682b4cd84 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Tue, 1 Sep 2015 18:26:50 +1200 Subject: [PATCH] De-closureify the API Closes #18 --- maud/src/lib.rs | 60 ------------------------- maud_macros/src/lib.rs | 4 +- maud_macros/src/parse.rs | 16 ++++++- maud_macros/src/render.rs | 52 +++++++++++++++++----- maud_macros/tests/tests.rs | 90 ++++++++++++++++++++++---------------- 5 files changed, 109 insertions(+), 113 deletions(-) diff --git a/maud/src/lib.rs b/maud/src/lib.rs index f884d71..b08d53a 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -5,9 +5,6 @@ //! //! [book]: http://lfairy.gitbooks.io/maud/content/ -use std::fmt; -use std::io; - /// Escapes an HTML value. pub fn escape(s: &str) -> String { use std::fmt::Write; @@ -16,68 +13,11 @@ pub fn escape(s: &str) -> String { buf } -/// A block of HTML markup, as returned by the `html!` macro. -/// -/// Use `.to_string()` to convert it to a `String`, or `.render()` to -/// write it directly to a handle. -pub struct Markup<F> { - callback: F, -} - -impl<F> Markup<F> where F: Fn(&mut fmt::Write) -> fmt::Result { - /// Renders the markup to a `std::io::Write`. - pub fn render<W: io::Write>(&self, w: &mut W) -> io::Result<()> { - struct Adaptor<'a, W: ?Sized + 'a> { - inner: &'a mut W, - error: io::Result<()>, - } - - impl<'a, W: ?Sized + io::Write> fmt::Write for Adaptor<'a, W> { - fn write_str(&mut self, s: &str) -> fmt::Result { - match self.inner.write_all(s.as_bytes()) { - Ok(()) => Ok(()), - Err(e) => { - self.error = Err(e); - Err(fmt::Error) - }, - } - } - } - - let mut output = Adaptor { inner: w, error: Ok(()) }; - match self.render_fmt(&mut output) { - Ok(()) => Ok(()), - Err(_) => output.error, - } - } - - /// Renders the markup to a `std::fmt::Write`. - pub fn render_fmt(&self, w: &mut fmt::Write) -> fmt::Result { - (self.callback)(w) - } -} - -impl<F> ToString for Markup<F> where F: Fn(&mut fmt::Write) -> fmt::Result { - fn to_string(&self) -> String { - let mut buf = String::new(); - self.render_fmt(&mut buf).unwrap(); - buf - } -} - /// Internal functions used by the `maud_macros` package. You should /// never need to call these directly. #[doc(hidden)] pub mod rt { use std::fmt; - use super::Markup; - - #[inline] - pub fn make_markup<F>(f: F) -> Markup<F> where - F: Fn(&mut fmt::Write) -> fmt::Result - { - Markup { callback: f } - } pub struct Escaper<'a, 'b: 'a> { pub inner: &'a mut (fmt::Write + 'b), diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs index 2f27d9b..87695fd 100644 --- a/maud_macros/src/lib.rs +++ b/maud_macros/src/lib.rs @@ -36,6 +36,6 @@ fn expand_html_debug<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) -> #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { - reg.register_macro("html", expand_html); - reg.register_macro("html_debug", expand_html_debug); + reg.register_macro("write_html", expand_html); + reg.register_macro("write_html_debug", expand_html_debug); } diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 9071018..35b352c 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -43,16 +43,30 @@ macro_rules! ident { } pub fn parse(cx: &ExtCtxt, input: &[TokenTree], sp: Span) -> P<Expr> { + let (write, input) = split_comma(cx, input, sp); let mut parser = Parser { in_attr: false, input: input, span: sp, - render: Renderer::new(cx), + render: Renderer::new(cx, write.to_vec()), }; parser.markups(); parser.into_render().into_expr() } +fn split_comma<'a>(cx: &ExtCtxt, input: &'a [TokenTree], sp: Span) -> (&'a [TokenTree], &'a [TokenTree]) { + fn is_comma(t: &TokenTree) -> bool { + match *t { + TtToken(_, token::Comma) => true, + _ => false, + } + } + match input.iter().position(is_comma) { + Some(i) => (&input[..i], &input[1+i..]), + None => cx.span_fatal(sp, "expected two arguments to write_html!()"), + } +} + struct Parser<'cx, 'i> { in_attr: bool, input: &'i [TokenTree], diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs index 5294ed9..e57f8de 100644 --- a/maud_macros/src/render.rs +++ b/maud_macros/src/render.rs @@ -1,6 +1,6 @@ -use syntax::ast::{Expr, Ident, Pat, Stmt, TokenTree}; +use syntax::ast::{Expr, Ident, Pat, Stmt, TokenTree, TtToken}; +use syntax::codemap::DUMMY_SP; use syntax::ext::base::ExtCtxt; -use syntax::ext::build::AstBuilder; use syntax::parse::token; use syntax::ptr::P; @@ -15,17 +15,25 @@ pub enum Escape { pub struct Renderer<'cx> { pub cx: &'cx ExtCtxt<'cx>, w: Ident, + r: Ident, + loop_label: Vec<TokenTree>, stmts: Vec<P<Stmt>>, tail: String, } impl<'cx> Renderer<'cx> { /// Creates a new `Renderer` using the given extension context. - pub fn new(cx: &'cx ExtCtxt<'cx>) -> Renderer<'cx> { + pub fn new(cx: &'cx ExtCtxt<'cx>, writer_expr: Vec<TokenTree>) -> Renderer<'cx> { + let w = token::gensym_ident("__maud_writer"); + let r = token::gensym_ident("__maud_result"); + let loop_label = token::gensym_ident("__maud_loop_label"); + let writer_stmt = quote_stmt!(cx, let $w = &mut $writer_expr).unwrap(); Renderer { cx: cx, - w: Ident::new(token::intern("w")), - stmts: Vec::new(), + w: w, + r: r, + loop_label: vec![TtToken(DUMMY_SP, token::Lifetime(loop_label))], + stmts: vec![writer_stmt], tail: String::new(), } } @@ -35,6 +43,8 @@ impl<'cx> Renderer<'cx> { Renderer { cx: self.cx, w: self.w, + r: self.r, + loop_label: self.loop_label.clone(), stmts: Vec::new(), tail: String::new(), } @@ -48,7 +58,7 @@ impl<'cx> Renderer<'cx> { let s = &*self.tail; quote_expr!(self.cx, $w.write_str($s)) }; - let stmt = self.cx.stmt_expr(self.cx.expr_try(expr.span, expr)); + let stmt = self.wrap_try(expr); self.stmts.push(stmt); self.tail.clear(); } @@ -56,13 +66,16 @@ impl<'cx> Renderer<'cx> { /// Reifies the `Renderer` into a block of markup. pub fn into_expr(mut self) -> P<Expr> { - let Renderer { cx, w, stmts, .. } = { self.flush(); self }; - quote_expr!(cx, - ::maud::rt::make_markup(|$w: &mut ::std::fmt::Write| -> Result<(), ::std::fmt::Error> { + let Renderer { cx, r, loop_label, stmts, .. } = { self.flush(); self }; + quote_expr!(cx, { + let mut $r = Ok(()); + $loop_label: loop { use ::std::fmt::Write; $stmts - Ok(()) - })) + break $loop_label; + } + $r + }) } /// Reifies the `Renderer` into a raw list of statements. @@ -82,6 +95,21 @@ impl<'cx> Renderer<'cx> { self.tail.push_str(s); } + /// Wraps an expression in a `try!` call. + fn wrap_try(&self, expr: P<Expr>) -> P<Stmt> { + let r = self.r; + let loop_label = &self.loop_label; + quote_stmt!( + self.cx, + match $expr { + Ok(()) => {}, + Err(e) => { + $r = Err(e); + break $loop_label; + } + }).unwrap() + } + /// Appends a literal string, with the specified escaping method. pub fn string(&mut self, s: &str, escape: Escape) { let escaped; @@ -105,7 +133,7 @@ impl<'cx> Renderer<'cx> { "{}", $expr)), }; - let stmt = self.cx.stmt_expr(self.cx.expr_try(expr.span, expr)); + let stmt = self.wrap_try(expr); self.push(stmt); } diff --git a/maud_macros/tests/tests.rs b/maud_macros/tests/tests.rs index 3c7b1fa..91cf0ee 100644 --- a/maud_macros/tests/tests.rs +++ b/maud_macros/tests/tests.rs @@ -5,69 +5,75 @@ extern crate maud; #[test] fn literals() { - let s = html!("du\tcks" -23 3.14 '\n' "geese").to_string(); + let mut s = String::new(); + write_html!(s, "du\tcks" -23 3.14 '\n' "geese").unwrap(); assert_eq!(s, "du\tcks-233.14\ngeese"); } #[test] fn escaping() { - let s = html!("<flim&flam>").to_string(); + let mut s = String::new(); + write_html!(s, "<flim&flam>").unwrap(); assert_eq!(s, "<flim&flam>"); } #[test] fn semicolons() { - let s = html! { + let mut s = String::new(); + write_html!(s, { "one"; "two"; "three"; ;;;;;;;;;;;;;;;;;;;;;;;; "four"; - }.to_string(); + }).unwrap(); assert_eq!(s, "onetwothreefour"); } #[test] fn blocks() { - let s = html! { + let mut s = String::new(); + write_html!(s, { "hello" { " ducks" " geese" } " swans" - }.to_string(); + }).unwrap(); assert_eq!(s, "hello ducks geese swans"); } mod elements { #[test] fn simple() { - let s = html! { - p { b { "pickle" } "barrel" i { "kumquat" } } - }.to_string(); + let mut s = String::new(); + write_html!(s, p { b { "pickle" } "barrel" i { "kumquat" } }).unwrap(); assert_eq!(s, "<p><b>pickle</b>barrel<i>kumquat</i></p>"); } #[test] fn nesting() { - let s = html!(html body div p sup "butts").to_string(); + let mut s = String::new(); + write_html!(s, html body div p sup "butts").unwrap(); assert_eq!(s, "<html><body><div><p><sup>butts</sup></p></div></body></html>"); } #[test] fn empty() { - let s = html!("pinkie" br/ "pie").to_string(); + let mut s = String::new(); + write_html!(s, "pinkie" br/ "pie").unwrap(); assert_eq!(s, "pinkie<br>pie"); } #[test] fn attributes() { - let s = html! { + let mut s = String::new(); + write_html!(s, { link rel="stylesheet" href="styles.css"/ section id="midriff" { p class="hotpink" "Hello!" } - }.to_string(); + }).unwrap(); assert_eq!(s, concat!( r#"<link rel="stylesheet" href="styles.css">"#, r#"<section id="midriff"><p class="hotpink">Hello!</p></section>"#)); @@ -75,7 +81,8 @@ mod elements { #[test] fn empty_attributes() { - let s = html! { div readonly? input type="checkbox" checked? / }.to_string(); + let mut s = String::new(); + write_html!(s, div readonly? input type="checkbox" checked? /).unwrap(); assert_eq!(s, r#"<div readonly><input type="checkbox" checked></div>"#); } } @@ -83,19 +90,22 @@ mod elements { mod splices { #[test] fn literals() { - let s = html! { $"<pinkie>" }.to_string(); + let mut s = String::new(); + write_html!(s, $"<pinkie>").unwrap(); assert_eq!(s, "<pinkie>"); } #[test] fn raw_literals() { - let s = html! { $$"<pinkie>" }.to_string(); + let mut s = String::new(); + write_html!(s, $$"<pinkie>").unwrap(); assert_eq!(s, "<pinkie>"); } #[test] fn blocks() { - let s = html! { + let mut s = String::new(); + write_html!(s, { ${ let mut result = 1i32; for i in 2..11 { @@ -103,19 +113,20 @@ mod splices { } result } - }.to_string(); + }).unwrap(); assert_eq!(s, "3628800"); } #[test] fn attributes() { let rocks = true; - let s = html! { + let mut s = String::new(); + write_html!(s, { input checked?=true / input checked?=false / input checked?=rocks / input checked?=(!rocks) / - }.to_string(); + }).unwrap(); assert_eq!(s, concat!( r#"<input checked>"#, r#"<input>"#, @@ -127,14 +138,16 @@ mod splices { #[test] fn statics() { - let s = html! { $BEST_PONY }.to_string(); + let mut s = String::new(); + write_html!(s, $BEST_PONY).unwrap(); assert_eq!(s, "Pinkie Pie"); } #[test] fn closures() { let best_pony = "Pinkie Pie"; - let s = html! { $best_pony }.to_string(); + let mut s = String::new(); + write_html!(s, $best_pony).unwrap(); assert_eq!(s, "Pinkie Pie"); } @@ -159,37 +172,36 @@ mod splices { name: "Pinkie Pie", adorableness: 9, }; - let s = html! { + let mut s = String::new(); + write_html!(s, { "Name: " $pinkie.name ". Rating: " $pinkie.repugnance() - }.to_string(); + }).unwrap(); assert_eq!(s, "Name: Pinkie Pie. Rating: 1"); } #[test] fn nested_macro_invocation() { let best_pony = "Pinkie Pie"; - let s = html! { $(format!("{}", best_pony)) }.to_string(); + let mut s = String::new(); + write_html!(s, $(format!("{}", best_pony))).unwrap(); assert_eq!(s, "Pinkie Pie"); } } -#[test] -fn issue_1() { - let markup = html! { "Test" }; - let _ = markup.to_string(); -} - #[test] fn issue_13() { let owned = String::from("yay"); - let _ = html! { $owned }.to_string(); + let mut s = String::new(); + write_html!(s, $owned).unwrap(); + let _ = owned; } mod control { #[test] fn if_expr() { for (number, &name) in (1..4).zip(["one", "two", "three"].iter()) { - let s = html! { + let mut s = String::new(); + write_html!(s, { $if number == 1 { "one" } $else if number == 2 { @@ -199,7 +211,7 @@ mod control { } $else { "oh noes" } - }.to_string(); + }).unwrap(); assert_eq!(s, name); } } @@ -207,13 +219,14 @@ mod control { #[test] fn if_let() { for &(input, output) in [(Some("yay"), "yay"), (None, "oh noes")].iter() { - let s = html! { + let mut s = String::new(); + write_html!(s, { $if let Some(value) = input { $value } $else { "oh noes" } - }.to_string(); + }).unwrap(); assert_eq!(s, output); } } @@ -221,11 +234,12 @@ mod control { #[test] fn for_expr() { let ponies = ["Apple Bloom", "Scootaloo", "Sweetie Belle"]; - let s = html! { + let mut s = String::new(); + write_html!(s, { ul $for pony in &ponies { li $pony } - }.to_string(); + }).unwrap(); assert_eq!(s, concat!( "<ul>", "<li>Apple Bloom</li>",