From e0c7fcf055be89f7fc0dfd2857c1f957bfd58989 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Fri, 7 Jul 2017 22:59:20 +1200 Subject: [PATCH 01/17] Initial stab at porting to new proc macro interface --- maud/Cargo.toml | 1 + maud/src/lib.rs | 5 + {maud_macros => maud}/tests/basic_syntax.rs | 5 +- .../tests/control_structures.rs | 5 +- {maud_macros => maud}/tests/misc.rs | 5 +- {maud_macros => maud}/tests/splices.rs | 5 +- maud_macros/Cargo.toml | 4 +- maud_macros/src/lib.rs | 49 +- maud_macros/src/parse.rs | 577 ++++++------------ maud_macros/src/render.rs | 131 ++-- 10 files changed, 300 insertions(+), 487 deletions(-) rename {maud_macros => maud}/tests/basic_syntax.rs (98%) rename {maud_macros => maud}/tests/control_structures.rs (98%) rename {maud_macros => maud}/tests/misc.rs (96%) rename {maud_macros => maud}/tests/splices.rs (97%) diff --git a/maud/Cargo.toml b/maud/Cargo.toml index 9349900..bfcb7bf 100644 --- a/maud/Cargo.toml +++ b/maud/Cargo.toml @@ -13,6 +13,7 @@ description = "Compile-time HTML templates." categories = ["template-engine"] [dependencies] +maud_macros = { version = "0.16.3", path = "../maud_macros" } iron = { version = "0.5.1", optional = true } rocket = { version = "0.3", optional = true } diff --git a/maud/src/lib.rs b/maud/src/lib.rs index 4e9dec2..95b3d71 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(proc_macro)] #![feature(specialization)] //! A macro for writing HTML templates. @@ -12,8 +13,12 @@ #[cfg(feature = "iron")] extern crate iron; #[cfg(feature = "rocket")] extern crate rocket; +extern crate maud_macros; + use std::fmt::{self, Write}; +pub use maud_macros::{html, html_debug}; + /// Represents a type that can be rendered as HTML. /// /// If your type implements [`Display`][1], then it will implement this diff --git a/maud_macros/tests/basic_syntax.rs b/maud/tests/basic_syntax.rs similarity index 98% rename from maud_macros/tests/basic_syntax.rs rename to maud/tests/basic_syntax.rs index 30ccaff..a31cd39 100644 --- a/maud_macros/tests/basic_syntax.rs +++ b/maud/tests/basic_syntax.rs @@ -1,9 +1,8 @@ -#![feature(plugin)] -#![plugin(maud_macros)] +#![feature(proc_macro)] extern crate maud; -use maud::Markup; +use maud::{Markup, html}; #[test] fn literals() { diff --git a/maud_macros/tests/control_structures.rs b/maud/tests/control_structures.rs similarity index 98% rename from maud_macros/tests/control_structures.rs rename to maud/tests/control_structures.rs index 7d574f5..891d10e 100644 --- a/maud_macros/tests/control_structures.rs +++ b/maud/tests/control_structures.rs @@ -1,8 +1,9 @@ -#![feature(conservative_impl_trait, plugin)] -#![plugin(maud_macros)] +#![feature(conservative_impl_trait, proc_macro)] extern crate maud; +use maud::html; + #[test] fn if_expr() { for (number, &name) in (1..4).zip(["one", "two", "three"].iter()) { diff --git a/maud_macros/tests/misc.rs b/maud/tests/misc.rs similarity index 96% rename from maud_macros/tests/misc.rs rename to maud/tests/misc.rs index 8928379..c367db3 100644 --- a/maud_macros/tests/misc.rs +++ b/maud/tests/misc.rs @@ -1,8 +1,9 @@ -#![feature(plugin)] -#![plugin(maud_macros)] +#![feature(proc_macro)] extern crate maud; +use maud::html; + #[test] fn issue_13() { let owned = String::from("yay"); diff --git a/maud_macros/tests/splices.rs b/maud/tests/splices.rs similarity index 97% rename from maud_macros/tests/splices.rs rename to maud/tests/splices.rs index b5dc006..4a221f0 100644 --- a/maud_macros/tests/splices.rs +++ b/maud/tests/splices.rs @@ -1,8 +1,9 @@ -#![feature(plugin)] -#![plugin(maud_macros)] +#![feature(proc_macro)] extern crate maud; +use maud::html; + #[test] fn literals() { let s = html!(("<pinkie>")).into_string(); diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml index e4ff337..6c43639 100644 --- a/maud_macros/Cargo.toml +++ b/maud_macros/Cargo.toml @@ -13,11 +13,11 @@ description = "Compile-time HTML templates." [dependencies] if_chain = "0.1" -maud = { path = "../maud", version = "0.16.0" } +literalext = { git = "https://github.com/mystor/literalext.git", default-features = false, features = ["proc-macro"] } [lib] name = "maud_macros" -plugin = true +proc-macro = true [badges] travis-ci = { repository = "lfairy/maud" } diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs index b580f47..dcc70c0 100644 --- a/maud_macros/src/lib.rs +++ b/maud_macros/src/lib.rs @@ -1,53 +1,46 @@ -#![crate_type = "dylib"] -#![feature(plugin_registrar, quote)] -#![feature(slice_patterns)] -#![feature(rustc_private)] +#![feature(proc_macro)] #![recursion_limit = "1000"] // if_chain #![doc(html_root_url = "https://docs.rs/maud_macros/0.16.3")] #[macro_use] extern crate if_chain; -#[macro_use] -extern crate rustc; -extern crate rustc_plugin; -extern crate syntax; -extern crate maud; +extern crate literalext; +extern crate proc_macro; -use rustc_plugin::Registry; -use syntax::codemap::Span; -use syntax::ext::base::{DummyResult, ExtCtxt, MacEager, MacResult}; -use syntax::print::pprust; -use syntax::tokenstream::TokenTree; - -mod lints; +// TODO move lints into their own `maud_lints` crate +// mod lints; mod parse; mod render; -type ParseResult<T> = Result<T, ()>; +use proc_macro::TokenStream; -fn expand_html<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'cx> { - match parse::parse(cx, sp, args) { - Ok(expr) => MacEager::expr(quote_expr!(cx, $expr)), - Err(..) => DummyResult::expr(sp), +type ParseResult<T> = Result<T, String>; + +#[proc_macro] +pub fn html(args: TokenStream) -> TokenStream { + match parse::parse(args) { + Ok(expr) => expr, + Err(e) => panic!(e), } } -fn expand_html_debug<'cx>(cx: &'cx mut ExtCtxt, sp: Span, args: &[TokenTree]) -> Box<MacResult + 'cx> { - match parse::parse(cx, sp, args) { +#[proc_macro] +pub fn html_debug(args: TokenStream) -> TokenStream { + match parse::parse(args) { Ok(expr) => { - let expr = quote_expr!(cx, $expr); - cx.span_warn(sp, &format!("expansion:\n{}", - pprust::expr_to_string(&expr))); - MacEager::expr(expr) + println!("expansion:\n{}", expr); + expr }, - Err(..) => DummyResult::expr(sp), + Err(e) => panic!(e), } } +/* #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { reg.register_macro("html", expand_html); reg.register_macro("html_debug", expand_html_debug); lints::register_lints(reg); } +*/ diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 06accaf..362f066 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -1,484 +1,246 @@ +use proc_macro::{Delimiter, Literal, TokenNode, TokenStream, TokenTree, TokenTreeIter}; use std::mem; -use syntax::ast::LitKind; -use syntax::codemap::Span; -use syntax::ext::base::ExtCtxt; -use syntax::parse; -use syntax::parse::token::{BinOpToken, DelimToken, Token}; -use syntax::print::pprust; -use syntax::symbol::keywords; -use syntax::tokenstream::{Delimited, TokenStream, TokenTree}; + +use literalext::LiteralExt; use super::render::Renderer; use super::ParseResult; -macro_rules! at { - () => (TokenTree::Token(_, Token::At)) -} -macro_rules! dot { - () => (TokenTree::Token(_, Token::Dot)) -} -macro_rules! eq { - () => (TokenTree::Token(_, Token::Eq)) -} -macro_rules! pound { - () => (TokenTree::Token(_, Token::Pound)) -} -macro_rules! question { - () => (TokenTree::Token(_, Token::Question)) -} -macro_rules! semi { - () => (TokenTree::Token(_, Token::Semi)) -} -macro_rules! colon { - () => (TokenTree::Token(_, Token::Colon)) -} -macro_rules! comma { - () => (TokenTree::Token(_, Token::Comma)) -} -macro_rules! fat_arrow { - () => (TokenTree::Token(_, Token::FatArrow)) -} -macro_rules! minus { - () => (TokenTree::Token(_, Token::BinOp(BinOpToken::Minus))) -} -macro_rules! slash { - () => (TokenTree::Token(_, Token::BinOp(BinOpToken::Slash))) -} -macro_rules! literal { - () => (TokenTree::Token(_, Token::Literal(..))) -} -macro_rules! ident { - ($sp:pat, $x:pat) => (TokenTree::Token($sp, Token::Ident($x))) -} -macro_rules! keyword { - ($sp:pat, $x:ident) => (TokenTree::Token($sp, ref $x @ Token::Ident(..))) -} - -pub fn parse(cx: &ExtCtxt, sp: Span, input: &[TokenTree]) -> ParseResult<Vec<TokenTree>> { - let mut render = Renderer::new(cx); - Parser { - cx, +pub fn parse(input: TokenStream) -> ParseResult<TokenStream> { + let mut render = Renderer::new(); + let _ = Parser { in_attr: false, - input: input, - span: sp, + input: Lookahead::new(input.clone()), + }.markups(&mut render); + /* + Parser { + in_attr: false, + input: Lookahead::new(input.clone()), }.markups(&mut render)?; + */ // Heuristic: the size of the resulting markup tends to correlate with the // code size of the template itself - let size_hint = pprust::tts_to_string(input).len(); - Ok(render.into_expr(size_hint).into_trees().collect()) + let size_hint = input.to_string().len(); + Ok(render.into_expr(size_hint)) } -struct Parser<'cx, 'a: 'cx, 'i> { - cx: &'cx ExtCtxt<'a>, +struct Parser { in_attr: bool, - input: &'i [TokenTree], - span: Span, + input: Lookahead<TokenTree>, } -impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> { - /// Consumes `n` items from the input. - fn shift(&mut self, n: usize) { - self.input = &self.input[n..]; +impl Parser { + fn next(&mut self) -> Option<TokenNode> { + self.input.next().map(|tt| tt.kind) + } + + fn peek(&mut self) -> Option<TokenNode> { + self.input.peek().map(|tt| tt.kind) + } + + fn advance(&mut self) { + self.next(); } /// Attaches an error message to the span and returns `Err`. - fn error<T>(&self, span: Span, message: &str) -> ParseResult<T> { - self.cx.span_err(span, message); - Err(()) + fn error<T, E: Into<String>>(&self, message: E) -> ParseResult<T> { + Err(message.into()) } /// Parses and renders multiple blocks of markup. fn markups(&mut self, render: &mut Renderer) -> ParseResult<()> { loop { - match *self.input { - [] => return Ok(()), - [semi!(), ..] => self.shift(1), - [_, ..] => self.markup(render)?, + match self.peek() { + None => return Ok(()), + Some(TokenNode::Op(';', _)) => self.advance(), + _ => self.markup(render)?, } } } /// Parses and renders a single block of markup. fn markup(&mut self, render: &mut Renderer) -> ParseResult<()> { - match *self.input { + let token = match self.peek() { + Some(token) => token, + None => return self.error("unexpected end of input"), + }; + match token { // Literal - [ref tt @ literal!(), ..] => { - self.shift(1); - self.literal(tt, render)?; + TokenNode::Literal(lit) => { + self.advance(); + self.literal(lit, render)?; }, - // If - [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::If) => { - self.shift(2); - self.if_expr(sp, render)?; - }, - // While - [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::While) => { - self.shift(2); - self.while_expr(sp, render)?; - }, - // For - [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::For) => { - self.shift(2); - self.for_expr(sp, render)?; - }, - // Match - [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::Match) => { - self.shift(2); - self.match_expr(sp, render)?; - }, - // Let - [at!(), keyword!(sp, k), ..] if k.is_keyword(keywords::Let) => { - self.shift(2); - self.let_expr(sp, render)?; + // Special form + TokenNode::Op('@', _) => { + self.advance(); + match self.next() { + Some(TokenNode::Term(term)) => match term.as_str() { + "if" => self.if_expr(render)?, + "while" => self.while_expr(render)?, + "for" => self.for_expr(render)?, + "match" => self.match_expr(render)?, + "let" => self.let_expr(render)?, + other => return self.error(format!("unknown keyword `@{}`", other)), + }, + _ => return self.error("expected keyword after `@`"), + } } // Element - [ident!(sp, _), ..] => { - let name = self.namespaced_name().unwrap(); - self.element(sp, &name, render)?; + TokenNode::Term(_) => { + let name = self.namespaced_name()?; + self.element(&name, render)?; }, // Splice - [TokenTree::Delimited(_, ref d), ..] if d.delim == DelimToken::Paren => { - self.shift(1); - render.splice(d.stream()); + TokenNode::Group(Delimiter::Parenthesis, expr) => { + self.advance(); + render.splice(expr); } // Block - [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => { - self.shift(1); + TokenNode::Group(Delimiter::Brace, block) => { + self.advance(); Parser { - cx: self.cx, in_attr: self.in_attr, - input: &d.stream().into_trees().collect::<Vec<_>>(), - span: sp, + input: Lookahead::new(block), }.markups(render)?; }, // ??? - _ => { - if let [ref tt, ..] = *self.input { - return self.error(tt.span(), "invalid syntax"); - } else { - return self.error(self.span, "unexpected end of block"); - } - }, + _ => return self.error("invalid syntax"), } Ok(()) } /// Parses and renders a literal string. - fn literal(&mut self, tt: &TokenTree, render: &mut Renderer) -> ParseResult<()> { - let mut rust_parser = parse::stream_to_parser(self.cx.parse_sess, tt.clone().into()); - let lit = rust_parser.parse_lit().map_err(|mut e| e.emit())?; - if let LitKind::Str(s, _) = lit.node { - render.string(&s.as_str()); + fn literal(&mut self, lit: Literal, render: &mut Renderer) -> ParseResult<()> { + if let Some(s) = lit.parse_string() { + render.string(&s); Ok(()) } else { - return self.error(lit.span, "literal strings must be surrounded by quotes (\"like this\")") + self.error("expected string") } } /// Parses and renders an `@if` expression. /// /// The leading `@if` should already be consumed. - fn if_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> { - // Parse the initial if - let mut if_cond = vec![]; - let if_body; - loop { match *self.input { - [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => { - self.shift(1); - if_body = self.block(sp, d.stream(), render)?; - break; - }, - [ref tt, ..] => { - self.shift(1); - if_cond.push(tt.clone()); - }, - [] => return self.error(sp, "expected body for this @if"), - }} - // Parse the (optional) @else - let else_body = match *self.input { - [at!(), keyword!(_, k), ..] if k.is_keyword(keywords::Else) => { - self.shift(2); - match *self.input { - [keyword!(sp, k), ..] if k.is_keyword(keywords::If) => { - self.shift(1); - let mut render = render.fork(); - self.if_expr(sp, &mut render)?; - Some(render.into_stmts()) - }, - [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => { - self.shift(1); - Some(self.block(sp, d.stream(), render)?) - }, - _ => return self.error(sp, "expected body for this @else"), - } - }, - _ => None, - }; - render.emit_if(if_cond.into_iter().collect(), if_body, else_body); - Ok(()) + fn if_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { + self.error("unimplemented") } /// Parses and renders an `@while` expression. /// /// The leading `@while` should already be consumed. - fn while_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> { - let mut cond = vec![]; - let body; - loop { match *self.input { - [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => { - self.shift(1); - body = self.block(sp, d.stream(), render)?; - break; - }, - [ref tt, ..] => { - self.shift(1); - cond.push(tt.clone()); - }, - [] => return self.error(sp, "expected body for this @while"), - }} - render.emit_while(cond.into_iter().collect(), body); - Ok(()) + fn while_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { + self.error("unimplemented") } /// Parses and renders a `@for` expression. /// /// The leading `@for` should already be consumed. - fn for_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> { - let mut pattern = vec![]; - loop { match *self.input { - [keyword!(_, k), ..] if k.is_keyword(keywords::In) => { - self.shift(1); - break; - }, - [ref tt, ..] => { - self.shift(1); - pattern.push(tt.clone()); - }, - _ => return self.error(sp, "invalid @for"), - }} - let mut iterable = vec![]; - let body; - loop { match *self.input { - [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => { - self.shift(1); - body = self.block(sp, d.stream(), render)?; - break; - }, - [ref tt, ..] => { - self.shift(1); - iterable.push(tt.clone()); - }, - _ => return self.error(sp, "invalid @for"), - }} - render.emit_for(pattern.into_iter().collect(), iterable.into_iter().collect(), body); - Ok(()) + fn for_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { + self.error("unimplemented") } /// Parses and renders a `@match` expression. /// /// The leading `@match` should already be consumed. - fn match_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> { - // Parse the initial match - let mut match_var = vec![]; - let match_bodies; - loop { match *self.input { - [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => { - self.shift(1); - match_bodies = Parser { - cx: self.cx, - in_attr: self.in_attr, - input: &d.stream().into_trees().collect::<Vec<_>>(), - span: sp, - }.match_bodies(render)?; - break; - }, - [ref tt, ..] => { - self.shift(1); - match_var.push(tt.clone()); - }, - [] => return self.error(sp, "expected body for this @match"), - }} - render.emit_match(match_var.into_iter().collect(), match_bodies.into_iter().collect()); - Ok(()) + fn match_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { + self.error("unimplemented") } fn match_bodies(&mut self, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> { - let mut bodies = Vec::new(); - loop { match *self.input { - [] => break, - [ref tt @ comma!(), ..] => { - self.shift(1); - bodies.push(tt.clone()); - }, - [ref tt, ..] => bodies.append(&mut self.match_body(tt.span(), render)?), - }} - Ok(bodies) + self.error("unimplemented") } - fn match_body(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> { - let mut body = vec![]; - loop { match *self.input { - [ref tt @ fat_arrow!(), ..] => { - self.shift(1); - body.push(tt.clone()); - break; - }, - [ref tt, ..] => { - self.shift(1); - body.push(tt.clone()); - }, - _ => return self.error(sp, "invalid @match pattern"), - }} - let mut expr = Vec::new(); - loop { match *self.input { - [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => { - if expr.is_empty() { - self.shift(1); - expr = self.block(sp, d.stream(), render)?.into_trees().collect(); - break; - } else { - self.shift(1); - expr.push(TokenTree::Delimited(sp, d.clone())); - } - }, - [comma!(), ..] | [] => { - if expr.is_empty() { - return self.error(sp, "expected body for this @match arm"); - } else { - expr = self.block(sp, expr.into_iter().collect(), render)?.into_trees().collect(); - break; - } - }, - [ref tt, ..] => { - self.shift(1); - expr.push(tt.clone()); - }, - }} - body.push(TokenTree::Delimited(sp, Delimited { - delim: DelimToken::Brace, - tts: expr.into_iter().collect::<TokenStream>().into(), - })); - Ok(body) + fn match_body(&mut self, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> { + self.error("unimplemented") } /// Parses and renders a `@let` expression. /// /// The leading `@let` should already be consumed. - fn let_expr(&mut self, sp: Span, render: &mut Renderer) -> ParseResult<()> { - let mut pattern = vec![]; - loop { match *self.input { - [eq!(), ..] => { - self.shift(1); - break; - }, - [ref tt, ..] => { - self.shift(1); - pattern.push(tt.clone()); - }, - _ => return self.error(sp, "invalid @let"), - }} - let mut rhs = vec![]; - let body; - loop { match *self.input { - [TokenTree::Delimited(sp, ref d), ..] if d.delim == DelimToken::Brace => { - self.shift(1); - body = self.block(sp, d.stream(), render)?; - break; - }, - [ref tt, ..] => { - self.shift(1); - rhs.push(tt.clone()); - }, - _ => return self.error(sp, "invalid @let"), - }} - render.emit_let(pattern.into_iter().collect(), rhs.into_iter().collect(), body); - Ok(()) + fn let_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { + self.error("unimplemented") } /// Parses and renders an element node. /// /// The element name should already be consumed. - fn element(&mut self, sp: Span, name: &str, render: &mut Renderer) -> ParseResult<()> { + fn element(&mut self, name: &str, render: &mut Renderer) -> ParseResult<()> { if self.in_attr { - return self.error(sp, "unexpected element, you silly bumpkin"); + return self.error("unexpected element, you silly bumpkin"); } render.element_open_start(name); self.attrs(render)?; render.element_open_end(); - if let [slash!(), ..] = *self.input { - self.shift(1); - } else { - self.markup(render)?; - render.element_close(name); + match self.peek() { + Some(TokenNode::Op('/', _)) => { + // Void element + self.advance(); + }, + _ => { + self.markup(render)?; + render.element_close(name); + }, } Ok(()) } /// Parses and renders the attributes of an element. fn attrs(&mut self, render: &mut Renderer) -> ParseResult<()> { - let mut classes_static = Vec::new(); - let mut classes_toggled = Vec::new(); - let mut ids = Vec::new(); + let mut classes_static: Vec<String> = Vec::new(); + let mut classes_toggled: Vec<(TokenStream, String)> = Vec::new(); + let mut ids: Vec<String> = Vec::new(); loop { - let old_input = self.input; + let start_position = self.input.save(); let maybe_name = self.namespaced_name(); - match (maybe_name, self.input) { - (Ok(name), &[eq!(), ..]) => { - // Non-empty attribute - self.shift(1); + let token_after = self.next(); + match (maybe_name, token_after) { + // Non-empty attribute + (Ok(name), Some(TokenNode::Op('=', _))) => { render.attribute_start(&name); { // Parse a value under an attribute context - let mut in_attr = true; - mem::swap(&mut self.in_attr, &mut in_attr); + let in_attr = mem::replace(&mut self.in_attr, true); self.markup(render)?; - mem::swap(&mut self.in_attr, &mut in_attr); + self.in_attr = in_attr; } render.attribute_end(); }, - (Ok(name), &[question!(), ..]) => { - // Empty attribute - self.shift(1); - match *self.input { - [TokenTree::Delimited(_, ref d), ..] if d.delim == DelimToken::Bracket => { - // Toggle the attribute based on a boolean expression - self.shift(1); - let cond = d.stream(); - let body = { - let mut render = render.fork(); - render.attribute_empty(&name); - render.into_stmts() - }; - render.emit_if(cond, body, None); - }, - _ => { - // Write the attribute unconditionally + // Empty attribute + (Ok(name), Some(TokenNode::Op('?', _))) => match self.peek() { + // Toggle the attribute based on a boolean expression + Some(TokenNode::Group(Delimiter::Bracket, cond)) => { + self.advance(); + let body = { + let mut render = render.fork(); render.attribute_empty(&name); - }, - } + render.into_stmts() + }; + render.emit_if(cond, body, None); + }, + // Write the attribute unconditionally + _ => render.attribute_empty(&name), }, - (Err(_), &[dot!(), ident!(_, _), ..]) => { - // Class shorthand - self.shift(1); - let class_name = self.name().unwrap(); - match *self.input { - [TokenTree::Delimited(_, ref d), ..] if d.delim == DelimToken::Bracket => { - // Toggle the class based on a boolean expression - self.shift(1); - let cond = d.stream(); + // Class shorthand + (Err(_), Some(TokenNode::Op('.', _))) => { + let class_name = self.name()?; + match self.peek() { + // Toggle the class based on a boolean expression + Some(TokenNode::Group(Delimiter::Bracket, cond)) => { + self.advance(); classes_toggled.push((cond, class_name)); }, // Emit the class unconditionally _ => classes_static.push(class_name), } }, - (Err(_), &[pound!(), ident!(_, _), ..]) => { - // ID shorthand - self.shift(1); - ids.push(self.name().unwrap()); + // ID shorthand + (Err(_), Some(TokenNode::Op('#', _))) => { + ids.push(self.name()?); }, + // If it's not a valid attribute, backtrack and bail out _ => { - self.input = old_input; + self.input.restore(start_position); break; }, } @@ -511,24 +273,23 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> { /// Parses an identifier, without dealing with namespaces. fn name(&mut self) -> ParseResult<String> { - let mut s = match *self.input { - [ident!(_, name), ..] => { - self.shift(1); - String::from(&name.name.as_str() as &str) - }, - _ => return Err(()), + let mut s = if let Some(TokenNode::Term(term)) = self.peek() { + self.advance(); + String::from(term.as_str()) + } else { + return self.error("expected identifier"); }; let mut expect_ident = false; loop { - expect_ident = match *self.input { - [minus!(), ..] => { - self.shift(1); + expect_ident = match self.peek() { + Some(TokenNode::Op('-', _)) => { + self.advance(); s.push('-'); true }, - [ident!(_, name), ..] if expect_ident => { - self.shift(1); - s.push_str(&name.name.as_str()); + Some(TokenNode::Term(term)) if expect_ident => { + self.advance(); + s.push_str(term.as_str()); false }, _ => break, @@ -541,24 +302,68 @@ impl<'cx, 'a, 'i> Parser<'cx, 'a, 'i> { /// if necessary. fn namespaced_name(&mut self) -> ParseResult<String> { let mut s = self.name()?; - if let [colon!(), ident!(_, _), ..] = *self.input { - self.shift(1); + if let Some(TokenNode::Op(':', _)) = self.peek() { + self.advance(); s.push(':'); - s.push_str(&self.name().unwrap()); + s.push_str(&self.name()?); } Ok(s) } /// Parses the given token tree, returning a vector of statements. - fn block(&mut self, sp: Span, tts: TokenStream, render: &mut Renderer) -> ParseResult<TokenStream> { + fn block(&mut self, body: TokenStream, render: &mut Renderer) -> ParseResult<TokenStream> { let mut render = render.fork(); let mut parse = Parser { - cx: self.cx, in_attr: self.in_attr, - input: &tts.into_trees().collect::<Vec<_>>(), - span: sp, + input: Lookahead::new(body), }; parse.markups(&mut render)?; Ok(render.into_stmts()) } } + +struct Lookahead<T> { + buffer: Vec<T>, + index: usize, +} + +impl<T> Lookahead<T> { + fn new<I: IntoIterator<Item=T>>(items: I) -> Self { + Lookahead { + buffer: items.into_iter().collect(), + index: 0, + } + } + + fn save(&self) -> Position { + Position { index: self.index } + } + + fn restore(&mut self, Position { index }: Position) { + self.index = index; + } +} + +impl<T> Lookahead<T> where T: Clone { + fn peek(&mut self) -> Option<T> { + let position = self.save(); + let result = self.next(); + self.restore(position); + result + } +} + +impl<T> Iterator for Lookahead<T> where T: Clone { + type Item = T; + fn next(&mut self) -> Option<T> { + let result = self.buffer.get(self.index).cloned(); + if result.is_some() { + self.index += 1; + } + result + } +} + +struct Position { + index: usize, +} diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs index 7d296c8..04b5100 100644 --- a/maud_macros/src/render.rs +++ b/maud_macros/src/render.rs @@ -1,38 +1,28 @@ -use syntax::ast::Ident; -use syntax::ext::base::ExtCtxt; -use syntax::symbol::Symbol; -use syntax::tokenstream::{TokenStream, TokenTree}; +use proc_macro::{Literal, Term, TokenNode, TokenStream}; +use proc_macro::quote; +use std::fmt; -use maud::Escaper; - -// FIXME(rust-lang/rust#40939): -// * Use `TokenStreamBuilder` instead of `Vec<TokenStream>` -// * Use `quote!()` instead of `quote_tokens!()` - -pub struct Renderer<'cx, 'a: 'cx> { - cx: &'cx ExtCtxt<'a>, - writer: Ident, +pub struct Renderer { + output: TokenNode, stmts: Vec<TokenStream>, tail: String, } -impl<'cx, 'a> Renderer<'cx, 'a> { - /// Creates a new `Renderer` using the given extension context. - pub fn new(cx: &'cx ExtCtxt<'a>) -> Renderer<'cx, 'a> { - let writer = Ident::with_empty_ctxt(Symbol::gensym("__maud_output")); +impl Renderer { + /// Creates a new `Renderer`. + pub fn new() -> Renderer { + let output = TokenNode::Term(Term::intern("__maud_output")); Renderer { - cx: cx, - writer: writer, + output: output, stmts: Vec::new(), tail: String::new(), } } /// Creates a new `Renderer` under the same context as `self`. - pub fn fork(&self) -> Renderer<'cx, 'a> { + pub fn fork(&self) -> Renderer { Renderer { - cx: self.cx, - writer: self.writer, + output: self.output.clone(), stmts: Vec::new(), tail: String::new(), } @@ -42,36 +32,38 @@ impl<'cx, 'a> Renderer<'cx, 'a> { fn flush(&mut self) { if !self.tail.is_empty() { let expr = { - let w = self.writer; - let s = &*self.tail; - quote_tokens!(self.cx, $w.push_str($s);) + let output = self.output.clone(); + let string = TokenNode::Literal(Literal::string(&self.tail)); + quote!($output.push_str($string);) }; - self.stmts.push(expr.into_iter().collect()); + self.stmts.push(expr); self.tail.clear(); } } /// Reifies the `Renderer` into a block of markup. pub fn into_expr(mut self, size_hint: usize) -> TokenStream { - let Renderer { cx, writer, stmts, .. } = { self.flush(); self }; - let stmts: Vec<TokenTree> = TokenStream::concat(stmts).into_trees().collect(); - quote_tokens!(cx, { - let mut $writer = ::std::string::String::with_capacity($size_hint); + let Renderer { output, stmts, .. } = { self.flush(); self }; + let size_hint = TokenNode::Literal(Literal::u64(size_hint as u64)); + let stmts = stmts.into_iter().collect::<TokenStream>(); + quote!({ + extern crate maud; + let mut $output = String::with_capacity($size_hint as usize); $stmts - ::maud::PreEscaped($writer) - }).into_iter().collect() + maud::PreEscaped($output) + }) } /// Reifies the `Renderer` into a raw list of statements. pub fn into_stmts(mut self) -> TokenStream { let Renderer { stmts, .. } = { self.flush(); self }; - TokenStream::concat(stmts) + stmts.into_iter().collect() } /// Pushes a statement, flushing the tail buffer in the process. - fn push<T>(&mut self, stmt: T) where T: IntoIterator<Item=TokenTree> { + fn push<T>(&mut self, stmt: T) where T: Into<TokenStream> { self.flush(); - self.stmts.push(stmt.into_iter().collect()) + self.stmts.push(stmt.into()) } /// Pushes a literal string to the tail buffer. @@ -86,12 +78,17 @@ impl<'cx, 'a> Renderer<'cx, 'a> { /// Appends the result of an expression. pub fn splice(&mut self, expr: TokenStream) { - let w = self.writer; - let expr: Vec<TokenTree> = expr.into_trees().collect(); - self.push(quote_tokens!(self.cx, { - #[allow(unused_imports)] - use ::maud::Render as __maud_Render; - $expr.render_to(&mut $w); + let output = self.output.clone(); + self.push(quote!({ + extern crate maud; + // Create a local trait alias so that autoref works + trait Render: maud::Render { + fn render_to(&self, output: &mut String) { + maud::Render::render_to(self, output); + } + } + impl<T: maud::Render> Render for T {} + $expr.render_to(&mut $output); })); } @@ -131,14 +128,9 @@ impl<'cx, 'a> Renderer<'cx, 'a> { /// need to special-case `if let`. pub fn emit_if(&mut self, if_cond: TokenStream, if_body: TokenStream, else_body: Option<TokenStream>) { - let if_cond: Vec<TokenTree> = if_cond.into_trees().collect(); - let if_body: Vec<TokenTree> = if_body.into_trees().collect(); let stmt = match else_body { - None => quote_tokens!(self.cx, if $if_cond { $if_body }), - Some(else_body) => { - let else_body: Vec<TokenTree> = else_body.into_trees().collect(); - quote_tokens!(self.cx, if $if_cond { $if_body } else { $else_body }) - }, + None => quote!(if $if_cond { $if_body }), + Some(else_body) => quote!(if $if_cond { $if_body } else { $else_body }), }; self.push(stmt); } @@ -148,32 +140,22 @@ impl<'cx, 'a> Renderer<'cx, 'a> { /// The condition is a token tree (not an expression) so we don't /// need to special-case `while let`. pub fn emit_while(&mut self, cond: TokenStream, body: TokenStream) { - let cond: Vec<TokenTree> = cond.into_trees().collect(); - let body: Vec<TokenTree> = body.into_trees().collect(); - let stmt = quote_tokens!(self.cx, while $cond { $body }); + let stmt = quote!(while $cond { $body }); self.push(stmt); } pub fn emit_for(&mut self, pattern: TokenStream, iterable: TokenStream, body: TokenStream) { - let pattern: Vec<TokenTree> = pattern.into_trees().collect(); - let iterable: Vec<TokenTree> = iterable.into_trees().collect(); - let body: Vec<TokenTree> = body.into_trees().collect(); - let stmt = quote_tokens!(self.cx, for $pattern in $iterable { $body }); + let stmt = quote!(for $pattern in $iterable { $body }); self.push(stmt); } pub fn emit_match(&mut self, match_var: TokenStream, match_body: TokenStream) { - let match_var: Vec<TokenTree> = match_var.into_trees().collect(); - let match_body: Vec<TokenTree> = match_body.into_trees().collect(); - let stmt = quote_tokens!(self.cx, match $match_var { $match_body }); + let stmt = quote!(match $match_var { $match_body }); self.push(stmt); } pub fn emit_let(&mut self, pattern: TokenStream, rhs: TokenStream, body: TokenStream) { - let pattern: Vec<TokenTree> = pattern.into_trees().collect(); - let rhs: Vec<TokenTree> = rhs.into_trees().collect(); - let body: Vec<TokenTree> = body.into_trees().collect(); - let stmt = quote_tokens!(self.cx, { let $pattern = $rhs; $body }); + let stmt = quote!({ let $pattern = $rhs; $body }); self.push(stmt); } } @@ -184,3 +166,28 @@ fn html_escape(s: &str) -> String { Escaper::new(&mut buffer).write_str(s).unwrap(); buffer } + +// TODO move this into a common `maud_htmlescape` crate +struct Escaper<'a>(&'a mut String); + +impl<'a> Escaper<'a> { + /// Creates an `Escaper` from a `String`. + pub fn new(buffer: &'a mut String) -> Escaper<'a> { + Escaper(buffer) + } +} + +impl<'a> fmt::Write for Escaper<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + for b in s.bytes() { + match b { + b'&' => self.0.push_str("&"), + b'<' => self.0.push_str("<"), + b'>' => self.0.push_str(">"), + b'"' => self.0.push_str("""), + _ => unsafe { self.0.as_mut_vec().push(b) }, + } + } + Ok(()) + } +} From 6a593388ae63faa37b72920b9a4c85612fc64794 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sat, 8 Jul 2017 20:27:14 +1200 Subject: [PATCH 02/17] Fix maud_extras --- maud_extras/Cargo.toml | 1 - maud_extras/lib.rs | 29 ++++++++++++++--------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/maud_extras/Cargo.toml b/maud_extras/Cargo.toml index 01f883b..a733309 100644 --- a/maud_extras/Cargo.toml +++ b/maud_extras/Cargo.toml @@ -10,7 +10,6 @@ description = "Extra bits and pieces for use in Maud templates." [dependencies] maud = { path = "../maud", features = ["iron"] } -maud_macros = { path = "../maud_macros" } [lib] path = "lib.rs" diff --git a/maud_extras/lib.rs b/maud_extras/lib.rs index 1f2aec7..2756d68 100644 --- a/maud_extras/lib.rs +++ b/maud_extras/lib.rs @@ -1,19 +1,18 @@ -#![feature(plugin)] -#![plugin(maud_macros)] +#![feature(proc_macro)] extern crate maud; -use maud::{Markup, Render}; +use maud::{Markup, Render, html}; /// Links to an external stylesheet. /// /// # Example /// /// ```rust -/// # #![feature(plugin)] -/// # #![plugin(maud_macros)] +/// # #![feature(proc_macro)] /// # extern crate maud; /// # extern crate maud_extras; +/// # use maud::html; /// # use maud_extras::*; /// # fn main() { /// let markup = html! { (Css("styles.css")) }; @@ -36,10 +35,10 @@ impl<T: AsRef<str>> Render for Css<T> { /// # Example /// /// ```rust -/// # #![feature(plugin)] -/// # #![plugin(maud_macros)] +/// # #![feature(proc_macro)] /// # extern crate maud; /// # extern crate maud_extras; +/// # use maud::html; /// # use maud_extras::*; /// # fn main() { /// let markup = html! { (Js("app.js")) }; @@ -62,10 +61,10 @@ impl<T: AsRef<str>> Render for Js<T> { /// # Example /// /// ```rust -/// # #![feature(plugin)] -/// # #![plugin(maud_macros)] +/// # #![feature(proc_macro)] /// # extern crate maud; /// # extern crate maud_extras; +/// # use maud::html; /// # use maud_extras::*; /// # fn main() { /// let markup = html! { (Meta("description", "test description")) }; @@ -88,10 +87,10 @@ impl<T: AsRef<str>, U: AsRef<str>> Render for Meta<T, U> { /// # Example /// /// ```rust -/// # #![feature(plugin)] -/// # #![plugin(maud_macros)] +/// # #![feature(proc_macro)] /// # extern crate maud; /// # extern crate maud_extras; +/// # use maud::html; /// # use maud_extras::*; /// # fn main() { /// let markup = html! { (Title("Maud")) }; @@ -114,10 +113,10 @@ impl<T: AsRef<str>> Render for Title<T> { /// # Example /// /// ```rust -/// # #![feature(plugin)] -/// # #![plugin(maud_macros)] +/// # #![feature(proc_macro)] /// # extern crate maud; /// # extern crate maud_extras; +/// # use maud::html; /// # use maud_extras::*; /// # fn main() { /// let markup = html! { (Charset("utf-8")) }; @@ -140,10 +139,10 @@ impl<T: AsRef<str>> Render for Charset<T> { /// # Example /// /// ```rust -/// # #![feature(plugin)] -/// # #![plugin(maud_macros)] +/// # #![feature(proc_macro)] /// # extern crate maud; /// # extern crate maud_extras; +/// # use maud::html; /// # use maud_extras::*; /// # fn main() { /// let markup = html! { (MetaProperty("og:description", "test description")) }; From e61385645e27682be9cc4acc9cfde5be32d31d45 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 16 Jul 2017 22:11:10 +1200 Subject: [PATCH 03/17] Don't throw away spans --- maud_macros/src/parse.rs | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 362f066..f00f8fb 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -30,12 +30,12 @@ struct Parser { } impl Parser { - fn next(&mut self) -> Option<TokenNode> { - self.input.next().map(|tt| tt.kind) + fn next(&mut self) -> Option<TokenTree> { + self.input.next() } - fn peek(&mut self) -> Option<TokenNode> { - self.input.peek().map(|tt| tt.kind) + fn peek(&mut self) -> Option<TokenTree> { + self.input.peek() } fn advance(&mut self) { @@ -52,7 +52,7 @@ impl Parser { loop { match self.peek() { None => return Ok(()), - Some(TokenNode::Op(';', _)) => self.advance(), + Some(TokenTree { kind: TokenNode::Op(';', _), .. }) => self.advance(), _ => self.markup(render)?, } } @@ -66,15 +66,15 @@ impl Parser { }; match token { // Literal - TokenNode::Literal(lit) => { + TokenTree { kind: TokenNode::Literal(lit), .. } => { self.advance(); self.literal(lit, render)?; }, // Special form - TokenNode::Op('@', _) => { + TokenTree { kind: TokenNode::Op('@', _), .. } => { self.advance(); match self.next() { - Some(TokenNode::Term(term)) => match term.as_str() { + Some(TokenTree { kind: TokenNode::Term(term), .. }) => match term.as_str() { "if" => self.if_expr(render)?, "while" => self.while_expr(render)?, "for" => self.for_expr(render)?, @@ -86,17 +86,17 @@ impl Parser { } } // Element - TokenNode::Term(_) => { + TokenTree { kind: TokenNode::Term(_), .. } => { let name = self.namespaced_name()?; self.element(&name, render)?; }, // Splice - TokenNode::Group(Delimiter::Parenthesis, expr) => { + TokenTree { kind: TokenNode::Group(Delimiter::Parenthesis, expr), .. } => { self.advance(); render.splice(expr); } // Block - TokenNode::Group(Delimiter::Brace, block) => { + TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. } => { self.advance(); Parser { in_attr: self.in_attr, @@ -173,7 +173,7 @@ impl Parser { self.attrs(render)?; render.element_open_end(); match self.peek() { - Some(TokenNode::Op('/', _)) => { + Some(TokenTree { kind: TokenNode::Op('/', _), .. }) => { // Void element self.advance(); }, @@ -196,7 +196,7 @@ impl Parser { let token_after = self.next(); match (maybe_name, token_after) { // Non-empty attribute - (Ok(name), Some(TokenNode::Op('=', _))) => { + (Ok(name), Some(TokenTree { kind: TokenNode::Op('=', _), .. })) => { render.attribute_start(&name); { // Parse a value under an attribute context @@ -207,9 +207,9 @@ impl Parser { render.attribute_end(); }, // Empty attribute - (Ok(name), Some(TokenNode::Op('?', _))) => match self.peek() { + (Ok(name), Some(TokenTree { kind: TokenNode::Op('?', _), .. })) => match self.peek() { // Toggle the attribute based on a boolean expression - Some(TokenNode::Group(Delimiter::Bracket, cond)) => { + Some(TokenTree { kind: TokenNode::Group(Delimiter::Bracket, cond), .. }) => { self.advance(); let body = { let mut render = render.fork(); @@ -222,11 +222,11 @@ impl Parser { _ => render.attribute_empty(&name), }, // Class shorthand - (Err(_), Some(TokenNode::Op('.', _))) => { + (Err(_), Some(TokenTree { kind: TokenNode::Op('.', _), .. })) => { let class_name = self.name()?; match self.peek() { // Toggle the class based on a boolean expression - Some(TokenNode::Group(Delimiter::Bracket, cond)) => { + Some(TokenTree { kind: TokenNode::Group(Delimiter::Bracket, cond), .. }) => { self.advance(); classes_toggled.push((cond, class_name)); }, @@ -235,7 +235,7 @@ impl Parser { } }, // ID shorthand - (Err(_), Some(TokenNode::Op('#', _))) => { + (Err(_), Some(TokenTree { kind: TokenNode::Op('#', _), .. })) => { ids.push(self.name()?); }, // If it's not a valid attribute, backtrack and bail out @@ -273,7 +273,7 @@ impl Parser { /// Parses an identifier, without dealing with namespaces. fn name(&mut self) -> ParseResult<String> { - let mut s = if let Some(TokenNode::Term(term)) = self.peek() { + let mut s = if let Some(TokenTree { kind: TokenNode::Term(term), .. }) = self.peek() { self.advance(); String::from(term.as_str()) } else { @@ -282,12 +282,12 @@ impl Parser { let mut expect_ident = false; loop { expect_ident = match self.peek() { - Some(TokenNode::Op('-', _)) => { + Some(TokenTree { kind: TokenNode::Op('-', _), .. }) => { self.advance(); s.push('-'); true }, - Some(TokenNode::Term(term)) if expect_ident => { + Some(TokenTree { kind: TokenNode::Term(term), .. }) if expect_ident => { self.advance(); s.push_str(term.as_str()); false @@ -302,7 +302,7 @@ impl Parser { /// if necessary. fn namespaced_name(&mut self) -> ParseResult<String> { let mut s = self.name()?; - if let Some(TokenNode::Op(':', _)) = self.peek() { + if let Some(TokenTree { kind: TokenNode::Op(':', _), .. }) = self.peek() { self.advance(); s.push(':'); s.push_str(&self.name()?); From 3d1e82b20ea784c2abb1440e73d53ddb58c7ea5d Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 16 Jul 2017 22:13:26 +1200 Subject: [PATCH 04/17] Implement let expressions --- maud_macros/src/parse.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index f00f8fb..f2aa746 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -159,7 +159,28 @@ impl Parser { /// /// The leading `@let` should already be consumed. fn let_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { - self.error("unimplemented") + let mut pat = Vec::new(); + loop { + match self.next() { + Some(TokenTree { kind: TokenNode::Op('=', _), .. }) => break, + Some(token) => pat.push(token), + None => return self.error("unexpected end of @let expression"), + } + } + let mut expr = Vec::new(); + let body; + loop { + match self.next() { + Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => { + body = self.block(block, render)?; + break; + }, + Some(token) => expr.push(token), + None => return self.error("unexpected end of @let expression"), + } + } + render.emit_let(pat.into_iter().collect(), expr.into_iter().collect(), body); + Ok(()) } /// Parses and renders an element node. @@ -187,9 +208,9 @@ impl Parser { /// Parses and renders the attributes of an element. fn attrs(&mut self, render: &mut Renderer) -> ParseResult<()> { - let mut classes_static: Vec<String> = Vec::new(); - let mut classes_toggled: Vec<(TokenStream, String)> = Vec::new(); - let mut ids: Vec<String> = Vec::new(); + let mut classes_static = Vec::new(); + let mut classes_toggled = Vec::new(); + let mut ids = Vec::new(); loop { let start_position = self.input.save(); let maybe_name = self.namespaced_name(); From 88c8a7e8c988c6f38b33c689c008e5805819ea85 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Mon, 17 Jul 2017 21:13:01 +1200 Subject: [PATCH 05/17] Revert "Silence `collapsible_if` warnings (#93)" This reverts commit a72877cb5524af7d8e56d0ecb70de13980503639. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9f9b00f..e190a34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,6 @@ script: - ( cd maud && cargo test --all-features ) - if command -v cargo-clippy > /dev/null; then ( cd maud && cargo clippy -- -D warnings ); fi - ( cd maud_macros && cargo test --all-features ) - # Silence `collapsible_if` warnings for now -- see https://github.com/lfairy/maud/issues/93 - - if command -v cargo-clippy > /dev/null; then ( cd maud_macros && cargo clippy -- -D warnings -A collapsible_if ); fi + - if command -v cargo-clippy > /dev/null; then ( cd maud_macros && cargo clippy -- -D warnings ); fi - ( cd maud_extras && cargo test --all-features ) - if command -v cargo-clippy > /dev/null; then ( cd maud_extras && cargo clippy -- -D warnings ); fi From 748ce69f4d3bad2b58b59e866aff041722d9d21c Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 23 Jul 2017 13:27:18 +1200 Subject: [PATCH 06/17] Use loop-break-value It's stable now! --- maud_macros/src/parse.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index f2aa746..9130fc8 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -168,17 +168,15 @@ impl Parser { } } let mut expr = Vec::new(); - let body; - loop { + let body = loop { match self.next() { Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => { - body = self.block(block, render)?; - break; + break self.block(block, render)?; }, Some(token) => expr.push(token), None => return self.error("unexpected end of @let expression"), } - } + }; render.emit_let(pat.into_iter().collect(), expr.into_iter().collect(), body); Ok(()) } @@ -343,6 +341,7 @@ impl Parser { } } +// TODO use rust-lang/rust#43280 instead struct Lookahead<T> { buffer: Vec<T>, index: usize, From af57ac41ce9fc9595c902c452d069c9d49960e5a Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 23 Jul 2017 13:49:02 +1200 Subject: [PATCH 07/17] Refactor backtracking mechanism --- maud_macros/src/parse.rs | 119 +++++++++++++++------------------------ 1 file changed, 45 insertions(+), 74 deletions(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 9130fc8..91be5cc 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -10,12 +10,14 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> { let mut render = Renderer::new(); let _ = Parser { in_attr: false, - input: Lookahead::new(input.clone()), + input: input.clone().into_iter().collect(), + index: 0, }.markups(&mut render); /* Parser { in_attr: false, - input: Lookahead::new(input.clone()), + input: input.clone().into_iter().collect(), + index: 0, }.markups(&mut render)?; */ // Heuristic: the size of the resulting markup tends to correlate with the @@ -24,24 +26,35 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> { Ok(render.into_expr(size_hint)) } +#[derive(Clone)] struct Parser { in_attr: bool, - input: Lookahead<TokenTree>, + // FIXME(rust-lang/rust#43280) use TokenTreeIter instead of tracking indices manually + input: Vec<TokenTree>, + index: usize, } impl Parser { fn next(&mut self) -> Option<TokenTree> { - self.input.next() + let result = self.input.get(self.index).cloned(); + if result.is_some() { + self.index += 1; + } + result } fn peek(&mut self) -> Option<TokenTree> { - self.input.peek() + self.input.get(self.index).cloned() } fn advance(&mut self) { self.next(); } + fn commit(&mut self, attempt: Parser) { + *self = attempt; + } + /// Attaches an error message to the span and returns `Err`. fn error<T, E: Into<String>>(&self, message: E) -> ParseResult<T> { Err(message.into()) @@ -100,7 +113,8 @@ impl Parser { self.advance(); Parser { in_attr: self.in_attr, - input: Lookahead::new(block), + input: block.into_iter().collect(), + index: 0, }.markups(render)?; }, // ??? @@ -210,12 +224,13 @@ impl Parser { let mut classes_toggled = Vec::new(); let mut ids = Vec::new(); loop { - let start_position = self.input.save(); - let maybe_name = self.namespaced_name(); - let token_after = self.next(); + let mut attempt = self.clone(); + let maybe_name = attempt.namespaced_name(); + let token_after = attempt.next(); match (maybe_name, token_after) { // Non-empty attribute (Ok(name), Some(TokenTree { kind: TokenNode::Op('=', _), .. })) => { + self.commit(attempt); render.attribute_start(&name); { // Parse a value under an attribute context @@ -226,22 +241,26 @@ impl Parser { render.attribute_end(); }, // Empty attribute - (Ok(name), Some(TokenTree { kind: TokenNode::Op('?', _), .. })) => match self.peek() { - // Toggle the attribute based on a boolean expression - Some(TokenTree { kind: TokenNode::Group(Delimiter::Bracket, cond), .. }) => { - self.advance(); - let body = { - let mut render = render.fork(); - render.attribute_empty(&name); - render.into_stmts() - }; - render.emit_if(cond, body, None); - }, - // Write the attribute unconditionally - _ => render.attribute_empty(&name), + (Ok(name), Some(TokenTree { kind: TokenNode::Op('?', _), .. })) => { + self.commit(attempt); + match self.peek() { + // Toggle the attribute based on a boolean expression + Some(TokenTree { kind: TokenNode::Group(Delimiter::Bracket, cond), .. }) => { + self.advance(); + let body = { + let mut render = render.fork(); + render.attribute_empty(&name); + render.into_stmts() + }; + render.emit_if(cond, body, None); + }, + // Write the attribute unconditionally + _ => render.attribute_empty(&name), + } }, // Class shorthand (Err(_), Some(TokenTree { kind: TokenNode::Op('.', _), .. })) => { + self.commit(attempt); let class_name = self.name()?; match self.peek() { // Toggle the class based on a boolean expression @@ -255,13 +274,11 @@ impl Parser { }, // ID shorthand (Err(_), Some(TokenTree { kind: TokenNode::Op('#', _), .. })) => { + self.commit(attempt); ids.push(self.name()?); }, // If it's not a valid attribute, backtrack and bail out - _ => { - self.input.restore(start_position); - break; - }, + _ => break, } } if !classes_static.is_empty() || !classes_toggled.is_empty() { @@ -334,56 +351,10 @@ impl Parser { let mut render = render.fork(); let mut parse = Parser { in_attr: self.in_attr, - input: Lookahead::new(body), + input: body.into_iter().collect(), + index: 0, }; parse.markups(&mut render)?; Ok(render.into_stmts()) } } - -// TODO use rust-lang/rust#43280 instead -struct Lookahead<T> { - buffer: Vec<T>, - index: usize, -} - -impl<T> Lookahead<T> { - fn new<I: IntoIterator<Item=T>>(items: I) -> Self { - Lookahead { - buffer: items.into_iter().collect(), - index: 0, - } - } - - fn save(&self) -> Position { - Position { index: self.index } - } - - fn restore(&mut self, Position { index }: Position) { - self.index = index; - } -} - -impl<T> Lookahead<T> where T: Clone { - fn peek(&mut self) -> Option<T> { - let position = self.save(); - let result = self.next(); - self.restore(position); - result - } -} - -impl<T> Iterator for Lookahead<T> where T: Clone { - type Item = T; - fn next(&mut self) -> Option<T> { - let result = self.buffer.get(self.index).cloned(); - if result.is_some() { - self.index += 1; - } - result - } -} - -struct Position { - index: usize, -} From 798f9baee889d3f0a4de5539a0a8e6c994adeb38 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 23 Jul 2017 13:56:09 +1200 Subject: [PATCH 08/17] Implement if expressions --- maud_macros/src/parse.rs | 46 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 91be5cc..3bb009b 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -137,7 +137,51 @@ impl Parser { /// /// The leading `@if` should already be consumed. fn if_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { - self.error("unimplemented") + let mut if_cond = Vec::new(); + let if_body = loop { + match self.next() { + Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => { + break self.block(block, render)?; + }, + Some(token) => if_cond.push(token), + None => return self.error("unexpected end of @if expression"), + } + }; + let mut attempt = self.clone(); + let else_body = if_chain! { + // Try to match an `@else` after this + if let Some(TokenTree { kind: TokenNode::Op('@', _), .. }) = attempt.next(); + if let Some(TokenTree { kind: TokenNode::Term(else_keyword), .. }) = attempt.next(); + if else_keyword.as_str() == "else"; + then { + self.commit(attempt); + if_chain! { + // `@else if` + if let Some(TokenTree { kind: TokenNode::Term(if_keyword), .. }) = self.peek(); + if if_keyword.as_str() == "if"; + then { + self.advance(); + let mut render = render.fork(); + self.if_expr(&mut render)?; + Some(render.into_stmts()) + } + // Just an `@else` + else { + if let Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) = self.next() { + Some(self.block(block, render)?) + } else { + return self.error("expected body for @else"); + } + } + } + } + else { + // We didn't find an `@else`; backtrack + None + } + }; + render.emit_if(if_cond.into_iter().collect(), if_body, else_body); + Ok(()) } /// Parses and renders an `@while` expression. From 62bdd4b06afd7fd7f89d1330eedd223846d3df98 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 23 Jul 2017 14:00:49 +1200 Subject: [PATCH 09/17] Use literalext from crates.io --- maud_macros/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml index 6c43639..37e2379 100644 --- a/maud_macros/Cargo.toml +++ b/maud_macros/Cargo.toml @@ -13,7 +13,7 @@ description = "Compile-time HTML templates." [dependencies] if_chain = "0.1" -literalext = { git = "https://github.com/mystor/literalext.git", default-features = false, features = ["proc-macro"] } +literalext = { version = "0.1", default-features = false, features = ["proc-macro"] } [lib] name = "maud_macros" From cd113be71b8f832d360145227bc8c9b35e47d57a Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 23 Jul 2017 14:03:23 +1200 Subject: [PATCH 10/17] Implement while expressions --- maud_macros/src/parse.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 3bb009b..4c53292 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -188,7 +188,18 @@ impl Parser { /// /// The leading `@while` should already be consumed. fn while_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { - self.error("unimplemented") + let mut cond = Vec::new(); + let body = loop { + match self.next() { + Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => { + break self.block(block, render)?; + }, + Some(token) => cond.push(token), + None => return self.error("unexpected end of @while expression"), + } + }; + render.emit_while(cond.into_iter().collect(), body); + Ok(()) } /// Parses and renders a `@for` expression. From 513d7f3d73b8820843ce888c25dfcb9a51a25632 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 23 Jul 2017 14:08:19 +1200 Subject: [PATCH 11/17] Implement for expressions --- maud_macros/src/parse.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 4c53292..5b6acde 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -206,7 +206,26 @@ impl Parser { /// /// The leading `@for` should already be consumed. fn for_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { - self.error("unimplemented") + let mut pat = Vec::new(); + loop { + match self.next() { + Some(TokenTree { kind: TokenNode::Term(in_keyword), .. }) if in_keyword.as_str() == "in" => break, + Some(token) => pat.push(token), + None => return self.error("unexpected end of @for expression"), + } + } + let mut expr = Vec::new(); + let body = loop { + match self.next() { + Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) => { + break self.block(block, render)?; + }, + Some(token) => expr.push(token), + None => return self.error("unexpected end of @for expression"), + } + }; + render.emit_for(pat.into_iter().collect(), expr.into_iter().collect(), body); + Ok(()) } /// Parses and renders a `@match` expression. From fd7e000cda1a900e7d96b2a05d13f3d5538b4edb Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sat, 29 Jul 2017 13:24:40 +1200 Subject: [PATCH 12/17] TokenTreeIter values can be cloned now --- maud_macros/Cargo.toml | 1 - maud_macros/src/lib.rs | 2 -- maud_macros/src/parse.rs | 66 +++++++++++++++++++--------------------- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml index 37e2379..5765624 100644 --- a/maud_macros/Cargo.toml +++ b/maud_macros/Cargo.toml @@ -12,7 +12,6 @@ repository = "https://github.com/lfairy/maud" description = "Compile-time HTML templates." [dependencies] -if_chain = "0.1" literalext = { version = "0.1", default-features = false, features = ["proc-macro"] } [lib] diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs index dcc70c0..cbdbb18 100644 --- a/maud_macros/src/lib.rs +++ b/maud_macros/src/lib.rs @@ -3,8 +3,6 @@ #![doc(html_root_url = "https://docs.rs/maud_macros/0.16.3")] -#[macro_use] -extern crate if_chain; extern crate literalext; extern crate proc_macro; diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 5b6acde..1f7066a 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -10,14 +10,12 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> { let mut render = Renderer::new(); let _ = Parser { in_attr: false, - input: input.clone().into_iter().collect(), - index: 0, + input: input.clone().into_iter(), }.markups(&mut render); /* Parser { in_attr: false, - input: input.clone().into_iter().collect(), - index: 0, + input: input.clone().into_iter(), }.markups(&mut render)?; */ // Heuristic: the size of the resulting markup tends to correlate with the @@ -29,28 +27,32 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> { #[derive(Clone)] struct Parser { in_attr: bool, - // FIXME(rust-lang/rust#43280) use TokenTreeIter instead of tracking indices manually - input: Vec<TokenTree>, - index: usize, + input: TokenTreeIter, } impl Parser { fn next(&mut self) -> Option<TokenTree> { - let result = self.input.get(self.index).cloned(); - if result.is_some() { - self.index += 1; - } - result + self.input.next() } fn peek(&mut self) -> Option<TokenTree> { - self.input.get(self.index).cloned() + self.clone().next() + } + + fn peek2(&mut self) -> Option<(TokenTree, Option<TokenTree>)> { + let mut clone = self.clone(); + clone.next().map(|first| (first, clone.next())) } fn advance(&mut self) { self.next(); } + fn advance2(&mut self) { + self.next(); + self.next(); + } + fn commit(&mut self, attempt: Parser) { *self = attempt; } @@ -113,8 +115,7 @@ impl Parser { self.advance(); Parser { in_attr: self.in_attr, - input: block.into_iter().collect(), - index: 0, + input: block.into_iter(), }.markups(render)?; }, // ??? @@ -147,38 +148,36 @@ impl Parser { None => return self.error("unexpected end of @if expression"), } }; - let mut attempt = self.clone(); - let else_body = if_chain! { + let else_body = match self.peek2() { // Try to match an `@else` after this - if let Some(TokenTree { kind: TokenNode::Op('@', _), .. }) = attempt.next(); - if let Some(TokenTree { kind: TokenNode::Term(else_keyword), .. }) = attempt.next(); - if else_keyword.as_str() == "else"; - then { - self.commit(attempt); - if_chain! { + Some(( + TokenTree { kind: TokenNode::Op('@', _), .. }, + Some(TokenTree { kind: TokenNode::Term(else_keyword), .. }), + )) if else_keyword.as_str() == "else" => { + self.advance2(); + match self.peek() { // `@else if` - if let Some(TokenTree { kind: TokenNode::Term(if_keyword), .. }) = self.peek(); - if if_keyword.as_str() == "if"; - then { + Some(TokenTree { kind: TokenNode::Term(if_keyword), .. }) + if if_keyword.as_str() == "if" => { self.advance(); let mut render = render.fork(); self.if_expr(&mut render)?; Some(render.into_stmts()) - } + }, // Just an `@else` - else { + _ => { if let Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, block), .. }) = self.next() { Some(self.block(block, render)?) } else { return self.error("expected body for @else"); } - } + }, } - } - else { + }, + _ => { // We didn't find an `@else`; backtrack None - } + }, }; render.emit_if(if_cond.into_iter().collect(), if_body, else_body); Ok(()) @@ -425,8 +424,7 @@ impl Parser { let mut render = render.fork(); let mut parse = Parser { in_attr: self.in_attr, - input: body.into_iter().collect(), - index: 0, + input: body.into_iter(), }; parse.markups(&mut render)?; Ok(render.into_stmts()) From 37419d9781f3257a35025868b9c8ad23d950f9a8 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sat, 29 Jul 2017 14:30:52 +1200 Subject: [PATCH 13/17] Implement match expressions and remove debugging stuff --- maud_macros/src/parse.rs | 97 ++++++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 13 deletions(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 1f7066a..023de7e 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -1,4 +1,4 @@ -use proc_macro::{Delimiter, Literal, TokenNode, TokenStream, TokenTree, TokenTreeIter}; +use proc_macro::{Delimiter, Literal, Spacing, TokenNode, TokenStream, TokenTree, TokenTreeIter, quote}; use std::mem; use literalext::LiteralExt; @@ -11,13 +11,7 @@ pub fn parse(input: TokenStream) -> ParseResult<TokenStream> { let _ = Parser { in_attr: false, input: input.clone().into_iter(), - }.markups(&mut render); - /* - Parser { - in_attr: false, - input: input.clone().into_iter(), }.markups(&mut render)?; - */ // Heuristic: the size of the resulting markup tends to correlate with the // code size of the template itself let size_hint = input.to_string().len(); @@ -57,7 +51,7 @@ impl Parser { *self = attempt; } - /// Attaches an error message to the span and returns `Err`. + /// Returns an `Err` with the given message. fn error<T, E: Into<String>>(&self, message: E) -> ParseResult<T> { Err(message.into()) } @@ -231,15 +225,92 @@ impl Parser { /// /// The leading `@match` should already be consumed. fn match_expr(&mut self, render: &mut Renderer) -> ParseResult<()> { - self.error("unimplemented") + let mut head = Vec::new(); + let body = loop { + match self.next() { + Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, body), .. }) => { + let mut parse = Parser { + in_attr: self.in_attr, + input: body.into_iter(), + }; + break parse.match_arms(render)?; + }, + Some(token) => head.push(token), + None => return self.error("unexpected end of @match expression"), + } + }; + render.emit_match(head.into_iter().collect(), body); + Ok(()) } - fn match_bodies(&mut self, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> { - self.error("unimplemented") + fn match_arms(&mut self, render: &mut Renderer) -> ParseResult<TokenStream> { + let mut arms = Vec::new(); + while let Some(arm) = self.match_arm(render)? { + arms.push(arm); + } + Ok(arms.into_iter().collect()) } - fn match_body(&mut self, render: &mut Renderer) -> ParseResult<Vec<TokenTree>> { - self.error("unimplemented") + fn match_arm(&mut self, render: &mut Renderer) -> ParseResult<Option<TokenStream>> { + let mut pat = Vec::new(); + loop { + match self.peek2() { + Some(( + eq @ TokenTree { kind: TokenNode::Op('=', Spacing::Joint), .. }, + Some(gt @ TokenTree { kind: TokenNode::Op('>', _), .. }), + )) => { + self.advance2(); + pat.push(eq); + pat.push(gt); + break; + }, + Some((token, _)) => { + self.advance(); + pat.push(token); + }, + None => + if pat.is_empty() { + return Ok(None); + } else { + return self.error("unexpected end of @match pattern"); + }, + } + } + let body = match self.next() { + // $pat => { $stmts } + Some(TokenTree { kind: TokenNode::Group(Delimiter::Brace, body), span }) => { + let body = self.block(body, render)?; + // Trailing commas are optional if the match arm is a braced block + if let Some(TokenTree { kind: TokenNode::Op(',', _), .. }) = self.peek() { + self.advance(); + } + // Re-use the span from the original block + TokenTree { + kind: TokenNode::Group(Delimiter::Brace, body), + span, + }.into() + }, + // $pat => $expr + Some(first_token) => { + let mut body = vec![first_token]; + loop { + match self.next() { + Some(TokenTree { kind: TokenNode::Op(',', _), .. }) => break, + Some(token) => { + body.push(token); + }, + None => return self.error("unexpected end of @match arm"), + } + } + let body = self.block(body.into_iter().collect(), render)?; + // The generated code may have multiple statements, unlike the + // original expression. So wrap the whole thing in a block just + // in case. + quote!({ $body }) + }, + None => return self.error("unexpected end of @match arm"), + }; + Ok(Some(pat.into_iter().chain(body.into_iter()).collect())) } /// Parses and renders a `@let` expression. From 2213c0dc32ca86ebceb1e80ec307477406778237 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sat, 29 Jul 2017 19:20:33 +1200 Subject: [PATCH 14/17] Move escaping routines into a shared maud_htmlescape crate --- .travis.yml | 2 ++ Cargo.toml | 3 +- maud/Cargo.toml | 1 + maud/src/lib.rs | 48 ++------------------------- maud_htmlescape/Cargo.toml | 15 +++++++++ maud_htmlescape/lib.rs | 68 ++++++++++++++++++++++++++++++++++++++ maud_macros/Cargo.toml | 1 + maud_macros/src/lib.rs | 1 + maud_macros/src/render.rs | 28 ++-------------- 9 files changed, 94 insertions(+), 73 deletions(-) create mode 100644 maud_htmlescape/Cargo.toml create mode 100644 maud_htmlescape/lib.rs diff --git a/.travis.yml b/.travis.yml index e190a34..93cbc23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,8 @@ script: printf 'Checking for tabs in %s\n' "$TRAVIS_COMMIT_RANGE" ! git diff --name-only --diff-filter=ACMR "$TRAVIS_COMMIT_RANGE" | xargs grep $'\t' fi + - ( cd maud_htmlescape && cargo test --all-features ) + - if command -v cargo-clippy > /dev/null; then ( cd maud_htmlescape && cargo clippy -- -D warnings ); fi - ( cd maud && cargo test --all-features ) - if command -v cargo-clippy > /dev/null; then ( cd maud && cargo clippy -- -D warnings ); fi - ( cd maud_macros && cargo test --all-features ) diff --git a/Cargo.toml b/Cargo.toml index 123dc41..87876cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "maud", + "maud_htmlescape", "maud_macros", + "maud", "maud_extras", ] diff --git a/maud/Cargo.toml b/maud/Cargo.toml index bfcb7bf..3001991 100644 --- a/maud/Cargo.toml +++ b/maud/Cargo.toml @@ -13,6 +13,7 @@ description = "Compile-time HTML templates." categories = ["template-engine"] [dependencies] +maud_htmlescape = { version = "0.17.0", path = "../maud_htmlescape" } maud_macros = { version = "0.16.3", path = "../maud_macros" } iron = { version = "0.5.1", optional = true } rocket = { version = "0.3", optional = true } diff --git a/maud/src/lib.rs b/maud/src/lib.rs index 95b3d71..115d7df 100644 --- a/maud/src/lib.rs +++ b/maud/src/lib.rs @@ -13,6 +13,7 @@ #[cfg(feature = "iron")] extern crate iron; #[cfg(feature = "rocket")] extern crate rocket; +extern crate maud_htmlescape; extern crate maud_macros; use std::fmt::{self, Write}; @@ -121,52 +122,7 @@ impl<T: AsRef<str> + Into<String>> Into<String> for PreEscaped<T> { } } -/// An adapter that escapes HTML special characters. -/// -/// The following characters are escaped: -/// -/// * `&` is escaped as `&` -/// * `<` is escaped as `<` -/// * `>` is escaped as `>` -/// * `"` is escaped as `"` -/// -/// All other characters are passed through unchanged. -/// -/// **Note:** In versions prior to 0.13, the single quote (`'`) was -/// escaped as well. -/// -/// # Example -/// -/// ``` -/// # use maud::Escaper; -/// use std::fmt::Write; -/// let mut s = String::new(); -/// write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap(); -/// assert_eq!(s, "<script>launchMissiles()</script>"); -/// ``` -pub struct Escaper<'a>(&'a mut String); - -impl<'a> Escaper<'a> { - /// Creates an `Escaper` from a `String`. - pub fn new(buffer: &'a mut String) -> Escaper<'a> { - Escaper(buffer) - } -} - -impl<'a> fmt::Write for Escaper<'a> { - fn write_str(&mut self, s: &str) -> fmt::Result { - for b in s.bytes() { - match b { - b'&' => self.0.push_str("&"), - b'<' => self.0.push_str("<"), - b'>' => self.0.push_str(">"), - b'"' => self.0.push_str("""), - _ => unsafe { self.0.as_mut_vec().push(b) }, - } - } - Ok(()) - } -} +pub use maud_htmlescape::Escaper; /// The literal string `<!DOCTYPE html>`. /// diff --git a/maud_htmlescape/Cargo.toml b/maud_htmlescape/Cargo.toml new file mode 100644 index 0000000..a5caae0 --- /dev/null +++ b/maud_htmlescape/Cargo.toml @@ -0,0 +1,15 @@ +[package] + +name = "maud_htmlescape" +# When releasing a new version, please update html_root_url in lib.rs +version = "0.17.0" +authors = ["Chris Wong <lambda.fairy@gmail.com>"] + +license = "MIT/Apache-2.0" +documentation = "https://docs.rs/maud_htmlescape/" +homepage = "https://maud.lambda.xyz/" +repository = "https://github.com/lfairy/maud" +description = "Internal support code used by Maud." + +[lib] +path = "lib.rs" diff --git a/maud_htmlescape/lib.rs b/maud_htmlescape/lib.rs new file mode 100644 index 0000000..873df13 --- /dev/null +++ b/maud_htmlescape/lib.rs @@ -0,0 +1,68 @@ +//! Internal support code used by the [Maud] template engine. +//! +//! You should not need to depend on this crate directly. +//! +//! [Maud]: https://maud.lambda.xyz + +#![doc(html_root_url = "https://docs.rs/maud_htmlescape/0.17.0")] + +use std::fmt; + +/// An adapter that escapes HTML special characters. +/// +/// The following characters are escaped: +/// +/// * `&` is escaped as `&` +/// * `<` is escaped as `<` +/// * `>` is escaped as `>` +/// * `"` is escaped as `"` +/// +/// All other characters are passed through unchanged. +/// +/// **Note:** In versions prior to 0.13, the single quote (`'`) was +/// escaped as well. +/// +/// # Example +/// +/// ```rust,ignore +/// use std::fmt::Write; +/// let mut s = String::new(); +/// write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap(); +/// assert_eq!(s, "<script>launchMissiles()</script>"); +/// ``` +pub struct Escaper<'a>(&'a mut String); + +impl<'a> Escaper<'a> { + /// Creates an `Escaper` from a `String`. + pub fn new(buffer: &'a mut String) -> Escaper<'a> { + Escaper(buffer) + } +} + +impl<'a> fmt::Write for Escaper<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + for b in s.bytes() { + match b { + b'&' => self.0.push_str("&"), + b'<' => self.0.push_str("<"), + b'>' => self.0.push_str(">"), + b'"' => self.0.push_str("""), + _ => unsafe { self.0.as_mut_vec().push(b) }, + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::fmt::Write; + use Escaper; + + #[test] + fn it_works() { + let mut s = String::new(); + write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap(); + assert_eq!(s, "<script>launchMissiles()</script>"); + } +} diff --git a/maud_macros/Cargo.toml b/maud_macros/Cargo.toml index 5765624..40907eb 100644 --- a/maud_macros/Cargo.toml +++ b/maud_macros/Cargo.toml @@ -13,6 +13,7 @@ description = "Compile-time HTML templates." [dependencies] literalext = { version = "0.1", default-features = false, features = ["proc-macro"] } +maud_htmlescape = { version = "0.17.0", path = "../maud_htmlescape" } [lib] name = "maud_macros" diff --git a/maud_macros/src/lib.rs b/maud_macros/src/lib.rs index cbdbb18..409c7ab 100644 --- a/maud_macros/src/lib.rs +++ b/maud_macros/src/lib.rs @@ -4,6 +4,7 @@ #![doc(html_root_url = "https://docs.rs/maud_macros/0.16.3")] extern crate literalext; +extern crate maud_htmlescape; extern crate proc_macro; // TODO move lints into their own `maud_lints` crate diff --git a/maud_macros/src/render.rs b/maud_macros/src/render.rs index 04b5100..b286502 100644 --- a/maud_macros/src/render.rs +++ b/maud_macros/src/render.rs @@ -1,6 +1,7 @@ use proc_macro::{Literal, Term, TokenNode, TokenStream}; use proc_macro::quote; -use std::fmt; + +use maud_htmlescape::Escaper; pub struct Renderer { output: TokenNode, @@ -166,28 +167,3 @@ fn html_escape(s: &str) -> String { Escaper::new(&mut buffer).write_str(s).unwrap(); buffer } - -// TODO move this into a common `maud_htmlescape` crate -struct Escaper<'a>(&'a mut String); - -impl<'a> Escaper<'a> { - /// Creates an `Escaper` from a `String`. - pub fn new(buffer: &'a mut String) -> Escaper<'a> { - Escaper(buffer) - } -} - -impl<'a> fmt::Write for Escaper<'a> { - fn write_str(&mut self, s: &str) -> fmt::Result { - for b in s.bytes() { - match b { - b'&' => self.0.push_str("&"), - b'<' => self.0.push_str("<"), - b'>' => self.0.push_str(">"), - b'"' => self.0.push_str("""), - _ => unsafe { self.0.as_mut_vec().push(b) }, - } - } - Ok(()) - } -} From 7dcc7d7063dcaa5fe46a79a554ab22d18a53517d Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sat, 29 Jul 2017 19:41:22 +1200 Subject: [PATCH 15/17] Fix Clippy warnings --- maud_macros/src/parse.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 023de7e..73a1bb6 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -7,14 +7,14 @@ use super::render::Renderer; use super::ParseResult; pub fn parse(input: TokenStream) -> ParseResult<TokenStream> { - let mut render = Renderer::new(); - let _ = Parser { - in_attr: false, - input: input.clone().into_iter(), - }.markups(&mut render)?; // Heuristic: the size of the resulting markup tends to correlate with the // code size of the template itself let size_hint = input.to_string().len(); + let mut render = Renderer::new(); + Parser { + in_attr: false, + input: input.into_iter(), + }.markups(&mut render)?; Ok(render.into_expr(size_hint)) } @@ -24,11 +24,15 @@ struct Parser { input: TokenTreeIter, } -impl Parser { +impl Iterator for Parser { + type Item = TokenTree; + fn next(&mut self) -> Option<TokenTree> { self.input.next() } +} +impl Parser { fn peek(&mut self) -> Option<TokenTree> { self.clone().next() } From f1fbea181ad22b5e92541b24021f109f8d414de2 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sat, 29 Jul 2017 20:01:14 +1200 Subject: [PATCH 16/17] Reduce duplication in Travis config --- .travis.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 93cbc23..e9f1b18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,11 +12,12 @@ script: printf 'Checking for tabs in %s\n' "$TRAVIS_COMMIT_RANGE" ! git diff --name-only --diff-filter=ACMR "$TRAVIS_COMMIT_RANGE" | xargs grep $'\t' fi - - ( cd maud_htmlescape && cargo test --all-features ) - - if command -v cargo-clippy > /dev/null; then ( cd maud_htmlescape && cargo clippy -- -D warnings ); fi - - ( cd maud && cargo test --all-features ) - - if command -v cargo-clippy > /dev/null; then ( cd maud && cargo clippy -- -D warnings ); fi - - ( cd maud_macros && cargo test --all-features ) - - if command -v cargo-clippy > /dev/null; then ( cd maud_macros && cargo clippy -- -D warnings ); fi - - ( cd maud_extras && cargo test --all-features ) - - if command -v cargo-clippy > /dev/null; then ( cd maud_extras && cargo clippy -- -D warnings ); fi + - cargo test --all --all-features + - | + if command -v cargo-clippy > /dev/null; then + CLIPPY_STATUS=0 + for package in maud_htmlescape maud_macros maud maud_extras; do + cargo clippy --manifest-path $package/Cargo.toml -- -D warnings || CLIPPY_STATUS=$? + done + (exit $CLIPPY_STATUS) + fi From 4f7189375e8a248f1f33de780de241de6dc1e580 Mon Sep 17 00:00:00 2001 From: Chris Wong <lambda.fairy@gmail.com> Date: Sun, 30 Jul 2017 13:59:35 +1200 Subject: [PATCH 17/17] Avoid using `quote!` in parser --- maud_macros/src/parse.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 73a1bb6..fb82f29 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -1,4 +1,14 @@ -use proc_macro::{Delimiter, Literal, Spacing, TokenNode, TokenStream, TokenTree, TokenTreeIter, quote}; +use proc_macro::{ + Delimiter, + Literal, + Spacing, + Span, + TokenNode, + TokenStream, + TokenTree, + TokenTreeIter, +}; +use std::iter; use std::mem; use literalext::LiteralExt; @@ -292,7 +302,7 @@ impl Parser { TokenTree { kind: TokenNode::Group(Delimiter::Brace, body), span, - }.into() + } }, // $pat => $expr Some(first_token) => { @@ -310,11 +320,14 @@ impl Parser { // The generated code may have multiple statements, unlike the // original expression. So wrap the whole thing in a block just // in case. - quote!({ $body }) + TokenTree { + kind: TokenNode::Group(Delimiter::Brace, body), + span: Span::default(), + } }, None => return self.error("unexpected end of @match arm"), }; - Ok(Some(pat.into_iter().chain(body.into_iter()).collect())) + Ok(Some(pat.into_iter().chain(iter::once(body)).collect())) } /// Parses and renders a `@let` expression.