De-closureify the API

Closes 
This commit is contained in:
Chris Wong 2015-09-01 18:26:50 +12:00
parent abbb565412
commit 1badbd9df2
5 changed files with 109 additions and 113 deletions
maud/src
maud_macros

View file

@ -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),

View file

@ -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);
}

View file

@ -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],

View file

@ -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);
}

View file

@ -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, "&lt;flim&amp;flam&gt;");
}
#[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, "&lt;pinkie&gt;");
}
#[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>",