From 905edd0ae01d1df756af3b7629b87864d6b7d163 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sat, 10 Jan 2015 21:29:58 +1300 Subject: [PATCH] Rewrite parser again Now it's some one-pass monstrosity oh noes --- maud/src/lib.rs | 92 +++++++++++------ maud_macros/src/lib.rs | 8 +- maud_macros/src/parse.rs | 203 +++++++++++++++++++------------------ maud_macros/src/render.rs | 138 ++++++++++++++----------- maud_macros/tests/tests.rs | 1 + 5 files changed, 245 insertions(+), 197 deletions(-) diff --git a/maud/src/lib.rs b/maud/src/lib.rs index 60ecf0e..8eeeea7 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -1,6 +1,9 @@ //! Super fast HTML template engine. -use std::io::IoResult; +use std::fmt; +use std::fmt::Writer as FmtWriter; + +pub type FmtResult<T> = Result<T, fmt::Error>; /// Utilities for escaping HTML5 markup. /// @@ -9,27 +12,19 @@ use std::io::IoResult; /// /// [1]: http://www.w3.org/TR/html51/syntax.html#escapingString pub mod escape { + use std::fmt::Writer as FmtWriter; + use super::render; use super::rt; /// Escape a double-quoted attribute value, as per HTML5 rules. pub fn attribute(s: &str) -> String { - render(|w| { - for c in s.chars() { - try!(rt::escape_attribute(c, w)); - } - Ok(()) - }) + render(|w| rt::escape_attribute(w, |w| w.write_str(s))) } /// Escape non-attribute text content, as per HTML5 rules. pub fn non_attribute(s: &str) -> String { - render(|w| { - for c in s.chars() { - try!(rt::escape_non_attribute(c, w)); - } - Ok(()) - }) + render(|w| rt::escape_non_attribute(w, |w| w.write_str(s))) } } @@ -38,33 +33,66 @@ pub mod escape { #[experimental = "These functions should not be called directly. Use the macros in `maud_macros` instead."] pub mod rt { - use std::io::IoResult; + use std::fmt::Writer as FmtWriter; + use super::FmtResult; - #[inline] - pub fn escape_attribute(c: char, w: &mut Writer) -> IoResult<()> { - match c { - '&' => w.write_str("&"), - '\u{A0}' => w.write_str(" "), - '"' => w.write_str("""), - _ => w.write_char(c), + struct AttrEscaper<'a, 'b: 'a> { + inner: &'a mut (FmtWriter + 'b), + } + + impl<'a, 'b> FmtWriter for AttrEscaper<'a, 'b> { + fn write_str(&mut self, s: &str) -> FmtResult<()> { + for c in s.chars() { + try!(match c { + '&' => self.inner.write_str("&"), + '\u{A0}' => self.inner.write_str(" "), + '"' => self.inner.write_str("""), + _ => write!(self.inner, "{}", c), + }); + } + Ok(()) + } + } + + struct NonAttrEscaper<'a, 'b: 'a> { + inner: &'a mut (FmtWriter + 'b), + } + + impl<'a, 'b> FmtWriter for NonAttrEscaper<'a, 'b> { + fn write_str(&mut self, s: &str) -> FmtResult<()> { + for c in s.chars() { + try!(match c { + '&' => self.inner.write_str("&"), + '\u{A0}' => self.inner.write_str(" "), + '<' => self.inner.write_str("<"), + '>' => self.inner.write_str(">"), + _ => write!(self.inner, "{}", c), + }); + } + Ok(()) } } #[inline] - pub fn escape_non_attribute(c: char, w: &mut Writer) -> IoResult<()> { - match c { - '&' => w.write_str("&"), - '\u{A0}' => w.write_str(" "), - '<' => w.write_str("<"), - '>' => w.write_str(">"), - _ => w.write_char(c), - } + pub fn escape_attribute<F>(w: &mut FmtWriter, f: F) -> FmtResult<()> where + F: FnOnce(&mut FmtWriter) -> FmtResult<()> + { + f(&mut AttrEscaper { inner: w }) + } + + #[inline] + pub fn escape_non_attribute<F>(w: &mut FmtWriter, f: F) -> FmtResult<()> where + F: FnOnce(&mut FmtWriter) -> FmtResult<()> + { + f(&mut NonAttrEscaper { inner: w }) } } /// Render a template into a `String`. -pub fn render<F: FnOnce(&mut Writer) -> IoResult<()>>(template: F) -> String { - let mut buf = vec![]; +pub fn render<F>(template: F) -> String where + F: FnOnce(&mut FmtWriter) -> FmtResult<()> +{ + let mut buf = String::new(); template(&mut buf).unwrap(); - String::from_utf8(buf).unwrap() + buf } diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs index 5dac7eb..d9bdccd 100644 --- a/maud_macros/src/lib.rs +++ b/maud_macros/src/lib.rs @@ -1,5 +1,6 @@ #![crate_type = "dylib"] -#![feature(plugin_registrar, quote, slicing_syntax)] +#![feature(plugin_registrar, quote)] +#![allow(unstable)] extern crate syntax; extern crate rustc; @@ -15,10 +16,7 @@ mod render; fn expand_html<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'cx> { match parse::parse(cx, args) { - Some(markups) => { - let expr = render::render(cx, markups[]); - MacExpr::new(expr) - }, + Some(expr) => MacExpr::new(expr), None => DummyResult::any(sp), } } diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 6ff0dab..da183f4 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -5,56 +5,34 @@ use syntax::parse::parser::Parser as RustParser; use syntax::parse::token; use syntax::ptr::P; -#[derive(Show)] -pub enum Markup { - Element(String, Vec<(String, Value)>, Option<Box<Markup>>), - Block(Vec<Markup>), - Value(Value), -} - -#[derive(Show)] -pub struct Value { - pub value: Value_, - pub escape: Escape, -} - -#[derive(Show)] -pub enum Value_ { - Literal(String), - Splice(P<Expr>), -} +use super::render::Renderer; #[derive(Copy, PartialEq, Show)] pub enum Escape { - NoEscape, - Escape, + None, + Attr, + Body, } -macro_rules! some { - ($e:expr) => ( - match $e { - Some(x) => x, - None => return None, - } - ) +macro_rules! guard { + ($e:expr) => (if !$e { return false; }) } -macro_rules! any { - ($self_:expr;) => (None); - ($self_:expr; $e:expr) => (any!($self_; $e,)); +macro_rules! branch { + ($self_:expr;) => (return false); + ($self_:expr; $e:expr) => (branch!($self_; $e,)); ($self_:expr; $e:expr, $($es:expr),*) => ({ let start_ptr = $self_.input.as_ptr(); - match $e { - Some(x) => Some(x), - None => { - if $self_.input.as_ptr() == start_ptr { - // Parsing failed, but did not consume input. - // Keep going. - any!($self_; $($es),*) - } else { - return None; - } - }, + if $e { + true + } else { + if $self_.input.as_ptr() == start_ptr { + // Parsing failed, but did not consume input. + // Keep going. + branch!($self_; $($es),*) + } else { + return false; + } } }) } @@ -78,59 +56,72 @@ macro_rules! ident { ($x:pat) => (TtToken(_, token::Ident($x, token::IdentStyle::Plain))) } -pub fn parse(cx: &mut ExtCtxt, input: &[TokenTree]) -> Option<Vec<Markup>> { - Parser { cx: cx, input: input }.markups() +pub fn parse(cx: &mut ExtCtxt, input: &[TokenTree]) -> Option<P<Expr>> { + let mut success = true; + let expr = Renderer::with(cx, |render| { + let mut parser = Parser { + in_attr: false, + input: input, + render: render, + }; + success = parser.markups(); + }); + if success { + Some(expr) + } else { + None + } } -struct Parser<'cx, 's: 'cx, 'i> { - cx: &'cx mut ExtCtxt<'s>, +struct Parser<'cx: 'r, 's: 'cx, 'i, 'r, 'o: 'r> { + in_attr: bool, input: &'i [TokenTree], + render: &'r mut Renderer<'cx, 's, 'o>, } -impl<'cx, 's, 'i> Parser<'cx, 's, 'i> { +impl<'cx: 'r, 's: 'cx, 'i, 'r, 'o: 'r> Parser<'cx, 's, 'i, 'r, 'o> { /// Consume `n` items from the input. - fn shift(&mut self, n: uint) { + fn shift(&mut self, n: usize) { self.input = self.input.slice_from(n); } + fn choose_escape(&self) -> Escape { + if self.in_attr { + Escape::Attr + } else { + Escape::Body + } + } + /// Construct a Rust AST parser from the given token tree. fn new_rust_parser(&self, tts: Vec<TokenTree>) -> RustParser<'s> { - parse::tts_to_parser(self.cx.parse_sess, tts, self.cx.cfg.clone()) + parse::tts_to_parser(self.render.cx.parse_sess, tts, self.render.cx.cfg.clone()) } - fn markups(&mut self) -> Option<Vec<Markup>> { - let mut result = vec![]; + fn markups(&mut self) -> bool { loop { match self.input { - [] => return Some(result), + [] => return true, [semi!(), ..] => self.shift(1), [ref tt, ..] => { - match self.markup() { - Some(markup) => result.push(markup), - None => { - self.cx.span_err(tt.get_span(), "invalid syntax"); - return None; - }, + if !self.markup() { + self.render.cx.span_err(tt.get_span(), "invalid syntax"); + return false; } } } } } - fn markup(&mut self) -> Option<Markup> { - any!(self; - self.value().map(Markup::Value), - self.block(), - self.element()) - } - - fn value(&mut self) -> Option<Value> { - any!(self; + fn markup(&mut self) -> bool { + branch!(self; self.literal(), - self.splice()) + self.splice(), + self.block(), + !self.in_attr && self.element()) } - fn literal(&mut self) -> Option<Value> { + fn literal(&mut self) -> bool { let (tt, minus) = match self.input { [minus!(), ref tt @ literal!(), ..] => { self.shift(2); @@ -140,27 +131,30 @@ impl<'cx, 's, 'i> Parser<'cx, 's, 'i> { self.shift(1); (tt, false) }, - _ => return None, + _ => return false, }; let lit = self.new_rust_parser(vec![tt.clone()]).parse_lit(); - lit_to_string(self.cx, lit, minus) - .map(|s| Value { - value: Value_::Literal(s), - escape: Escape::Escape, - }) + match lit_to_string(self.render.cx, lit, minus) { + Some(s) => { + let escape = self.choose_escape(); + self.render.string(s.as_slice(), escape); + }, + None => return false, + } + true } - fn splice(&mut self) -> Option<Value> { + fn splice(&mut self) -> bool { let (escape, sp) = match self.input { [ref tt @ dollar!(), dollar!(), ..] => { self.shift(2); - (Escape::NoEscape, tt.get_span()) + (Escape::None, tt.get_span()) }, [ref tt @ dollar!(), ..] => { self.shift(1); - (Escape::Escape, tt.get_span()) + (self.choose_escape(), tt.get_span()) }, - _ => return None, + _ => return false, }; let tt = match self.input { [ref tt, ..] => { @@ -168,48 +162,57 @@ impl<'cx, 's, 'i> Parser<'cx, 's, 'i> { self.new_rust_parser(vec![tt.clone()]).parse_expr() }, _ => { - self.cx.span_err(sp, "expected expression for this splice"); - return None; + self.render.cx.span_err(sp, "expected expression for this splice"); + return false; }, }; - Some(Value { - value: Value_::Splice(tt), - escape: escape, - }) + self.render.splice(tt, escape); + true } - fn element(&mut self) -> Option<Markup> { + fn element(&mut self) -> bool { let name = match self.input { [ident!(name), ..] => { self.shift(1); name.as_str().to_string() }, - _ => return None, + _ => return false, }; - let attrs = some!(self.attrs()); - let body = any!(self; self.markup()); - Some(Markup::Element(name, attrs, body.map(|body| box body))) + let name = name.as_slice(); + self.render.element_open_start(name); + guard!(self.attrs()); + self.render.element_open_end(); + guard!(self.markup()); + self.render.element_close(name); + true } - fn attrs(&mut self) -> Option<Vec<(String, Value)>> { - let mut attrs = vec![]; + fn attrs(&mut self) -> bool { while let [ident!(name), eq!(), ..] = self.input { self.shift(2); - let name = name.as_str().to_string(); - let value = some!(self.value()); - attrs.push((name, value)); + self.render.attribute_start(name.as_str()); + { + let old_in_attr = self.in_attr; + self.in_attr = true; + guard!(self.markup()); + self.in_attr = old_in_attr; + } + self.render.attribute_end(); } - Some(attrs) + true } - fn block(&mut self) -> Option<Markup> { + fn block(&mut self) -> bool { match self.input { [TtDelimited(_, ref d), ..] if d.delim == token::DelimToken::Brace => { self.shift(1); - Parser { cx: self.cx, input: d.tts[] }.markups() - .map(Markup::Block) + Parser { + in_attr: self.in_attr, + input: d.tts.as_slice(), + render: self.render, + }.markups() }, - _ => None, + _ => false, } } } @@ -228,7 +231,7 @@ fn lit_to_string(cx: &mut ExtCtxt, lit: Lit, minus: bool) -> Option<String> { return None; }, LitChar(c) => result.push(c), - LitInt(x, _) => result.push_str(x.to_string()[]), + LitInt(x, _) => result.push_str(x.to_string().as_slice()), LitFloat(s, _) | LitFloatUnsuffixed(s) => result.push_str(s.get()), LitBool(b) => result.push_str(if b { "true" } else { "false" }), }; diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs index ed6db94..22225f4 100644 --- a/maud_macros/src/render.rs +++ b/maud_macros/src/render.rs @@ -4,70 +4,88 @@ use syntax::ext::base::ExtCtxt; use syntax::parse::token; use syntax::ptr::P; -use super::parse::{Markup, Value}; +use super::parse::Escape; use maud::escape; -pub fn render(cx: &mut ExtCtxt, markups: &[Markup]) -> P<Expr> { - let w = Ident::new(token::intern("w")); - let mut stmts = vec![]; - for markup in markups.iter() { - render_markup(cx, markup, w, &mut stmts); - } - quote_expr!(cx, |&: $w: &mut ::std::io::Writer| -> ::std::io::IoResult<()> { - $stmts - Ok(()) - }) +pub struct Renderer<'cx, 's: 'cx, 'o> { + pub cx: &'cx mut ExtCtxt<'s>, + stmts: &'o mut Vec<P<Stmt>>, + w: Ident, } -fn render_markup(cx: &mut ExtCtxt, markup: &Markup, w: Ident, out: &mut Vec<P<Stmt>>) { - use super::parse::Markup::*; - match *markup { - Element(..) => unimplemented!(), - Block(ref markups) => { - for markup in markups.iter() { - render_markup(cx, markup, w, out); - } - }, - Value(ref value) => { - let expr = render_value(cx, value, w, false); - out.push(quote_stmt!(cx, $expr)); - }, - } -} - -fn render_value(cx: &mut ExtCtxt, value: &Value, w: Ident, is_attr: bool) -> P<Expr> { - use super::parse::Escape::*; - use super::parse::Value_::*; - let &Value { ref value, escape } = value; - match *value { - Literal(ref s) => { - let s = match escape { - NoEscape => s[].into_cow(), - Escape => if is_attr { - escape::attribute(s[]).into_cow() - } else { - escape::non_attribute(s[]).into_cow() - }, +impl<'cx, 's: 'cx, 'o> Renderer<'cx, 's, 'o> { + pub fn with<F>(cx: &'cx mut ExtCtxt<'s>, f: F) -> P<Expr> where + F: for<'o_> FnOnce(&mut Renderer<'cx, 's, 'o_>) + { + let mut stmts = vec![]; + let w = Ident::new(token::intern("w")); + let cx = { + let mut render = Renderer { + cx: cx, + stmts: &mut stmts, + w: w, }; - let s = s[]; - quote_expr!(cx, { - try!($w.write_str($s)) - }) - }, - Splice(ref expr) => match escape { - NoEscape => quote_expr!(cx, { - try!(write!($w, "{}", $expr)); - }), - Escape => quote_expr!(cx, { - let s = $expr.to_string(); - for c in s.chars() { - try!(if $is_attr { - ::maud::rt::escape_attribute(c, $w) - } else { - ::maud::rt::escape_non_attribute(c, $w) - }); - } - }), - }, + f(&mut render); + render.cx + }; + quote_expr!(cx, |&: $w: &mut ::std::fmt::Writer| -> ::std::result::Result<(), ::std::fmt::Error> { + $stmts + Ok(()) + }) + } + + /// Append a literal pre-escaped string. + pub fn write(&mut self, s: &str) { + let w = self.w; + self.stmts.push(quote_stmt!(self.cx, try!($w.write_str($s)))); + } + + /// Append a literal string, with the specified escaping method. + pub fn string(&mut self, s: &str, escape: Escape) { + let s = match escape { + Escape::None => s.into_cow(), + Escape::Attr => escape::attribute(s).into_cow(), + Escape::Body => escape::non_attribute(s).into_cow(), + }; + self.write(s.as_slice()); + } + + /// Append the result of an expression, with the specified escaping method. + pub fn splice(&mut self, expr: P<Expr>, escape: Escape) { + let w = self.w; + self.stmts.push(match escape { + Escape::None => quote_stmt!(self.cx, try!(write!($w, "{}", $expr))), + Escape::Attr => + quote_stmt!(self.cx, + try!(::maud::rt::escape_attribute($w, |w| write!(w, "{}", $expr)))), + Escape::Body => + quote_stmt!(self.cx, + try!(::maud::rt::escape_non_attribute($w, |w| write!(w, "{}", $expr)))), + }); + } + + pub fn element_open_start(&mut self, name: &str) { + self.write("<"); + self.write(name); + } + + pub fn attribute_start(&mut self, name: &str) { + self.write(" "); + self.write(name); + self.write("=\""); + } + + pub fn attribute_end(&mut self) { + self.write("\""); + } + + pub fn element_open_end(&mut self) { + self.write(">"); + } + + pub fn element_close(&mut self, name: &str) { + self.write("</"); + self.write(name); + self.write(">"); } } diff --git a/maud_macros/tests/tests.rs b/maud_macros/tests/tests.rs index 7ff4d7e..62f9e7b 100644 --- a/maud_macros/tests/tests.rs +++ b/maud_macros/tests/tests.rs @@ -1,4 +1,5 @@ #![feature(plugin)] +#![allow(unstable)] extern crate maud; #[plugin] #[no_link] extern crate maud_macros;