Change macro internals to use an explicit AST ()

This commit is contained in:
Chris Wong 2018-05-17 20:38:44 +12:00 committed by GitHub
parent 7c63fc138a
commit 9d90e94c87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 526 additions and 333 deletions

View file

@ -9,7 +9,7 @@ Great to see that you want to help out! Here are some tips for getting started:
* Most contributions should include tests see the [existing test code] for how to write these. * Most contributions should include tests see the [existing test code] for how to write these.
* Documentation on the proc macro interface can be found on [docs.rust-lang.org][proc_macro]. * Documentation on the proc macro interface can be found on [docs.rust-lang.org][proc_macro].
- As for lints: [Manishearth's site] provides documentation for compiler internals. - The `maud_lints` crate uses internal compiler APIs; the documentation for these can be found on [Manishearth's site].
Have fun! ☺️ Have fun! ☺️

View file

@ -191,5 +191,5 @@ fn ids_shorthand() {
#[test] #[test]
fn classes_attrs_ids_mixed_up() { fn classes_attrs_ids_mixed_up() {
let s = html!(p { "Hi, " span.name.here lang="en" #thing { "Lyra" } "!" }).into_string(); let s = html!(p { "Hi, " span.name.here lang="en" #thing { "Lyra" } "!" }).into_string();
assert_eq!(s, "<p>Hi, <span lang=\"en\" class=\"name here\" id=\"thing\">Lyra</span>!</p>"); assert_eq!(s, r#"<p>Hi, <span class="name here" id="thing" lang="en">Lyra</span>!</p>"#);
} }

View file

@ -1,8 +1,9 @@
use rustc::hir::{Expr, ExprCall, ExprLit, ExprPath}; use rustc::hir::{Expr, ExprCall, ExprLit, ExprPath};
use rustc::lint::{LateContext, LateLintPass, LintArray, LintContext, LintPass}; use rustc::lint::{LateContext, LateLintPass, LintArray, LintContext, LintPass};
use super::util::match_def_path;
use syntax::ast::LitKind; use syntax::ast::LitKind;
use util::match_def_path;
declare_lint! { declare_lint! {
pub MAUD_DOCTYPE, pub MAUD_DOCTYPE,
Warn, Warn,

View file

@ -11,6 +11,7 @@ extern crate if_chain;
extern crate rustc; extern crate rustc;
extern crate rustc_plugin; extern crate rustc_plugin;
extern crate syntax; extern crate syntax;
extern crate syntax_pos;
use rustc_plugin::Registry; use rustc_plugin::Registry;

View file

@ -5,7 +5,7 @@
use rustc::hir::def_id::DefId; use rustc::hir::def_id::DefId;
use rustc::lint::LateContext; use rustc::lint::LateContext;
use rustc::ty; use rustc::ty;
use syntax::symbol::{LocalInternedString, Symbol}; use syntax_pos::symbol::{LocalInternedString, Symbol};
/// Check if a `DefId`'s path matches the given absolute type path usage. /// Check if a `DefId`'s path matches the given absolute type path usage.
/// ///

View file

@ -13,6 +13,7 @@ description = "Compile-time HTML templates."
[dependencies] [dependencies]
literalext = { version = "0.1", default-features = false, features = ["proc-macro"] } literalext = { version = "0.1", default-features = false, features = ["proc-macro"] }
matches = "0.1.6"
maud_htmlescape = { version = "0.17.0", path = "../maud_htmlescape" } maud_htmlescape = { version = "0.17.0", path = "../maud_htmlescape" }
[lib] [lib]

77
maud_macros/src/ast.rs Normal file
View file

@ -0,0 +1,77 @@
use proc_macro::{Span, TokenStream};
#[derive(Debug)]
pub enum Markup {
Block(Block),
Literal {
content: String,
span: Span,
},
Symbol {
symbol: TokenStream,
},
Splice {
expr: TokenStream,
},
Element {
name: TokenStream,
attrs: Attrs,
body: Option<Box<Markup>>,
},
Let {
tokens: TokenStream,
},
If {
segments: Vec<Special>,
},
Special(Special),
Match {
head: TokenStream,
arms: Vec<Special>,
arms_span: Span,
},
}
#[derive(Debug)]
pub struct Attrs {
pub classes_static: Vec<ClassOrId>,
pub classes_toggled: Vec<(ClassOrId, Toggler)>,
pub ids: Vec<ClassOrId>,
pub attrs: Vec<Attribute>,
}
pub type ClassOrId = TokenStream;
#[derive(Debug)]
pub struct Block {
pub markups: Vec<Markup>,
pub span: Span,
}
#[derive(Debug)]
pub struct Special {
pub head: TokenStream,
pub body: Block,
}
#[derive(Debug)]
pub struct Attribute {
pub name: TokenStream,
pub attr_type: AttrType,
}
#[derive(Debug)]
pub enum AttrType {
Normal {
value: Markup,
},
Empty {
toggler: Option<Toggler>,
},
}
#[derive(Debug)]
pub struct Toggler {
pub cond: TokenStream,
pub cond_span: Span,
}

View file

@ -1,132 +0,0 @@
use proc_macro::{Delimiter, Group, Literal, Span, TokenStream, TokenTree};
use proc_macro::quote;
use maud_htmlescape::Escaper;
pub struct Builder {
output_ident: TokenTree,
stmts: Vec<TokenStream>,
tail: String,
}
impl Builder {
/// Creates a new `Builder`.
pub fn new(output_ident: TokenTree) -> Builder {
Builder {
output_ident,
stmts: Vec::new(),
tail: String::new(),
}
}
/// Flushes the tail buffer, emitting a single `.push_str()` call.
fn flush(&mut self) {
if !self.tail.is_empty() {
let expr = {
let output_ident = self.output_ident.clone();
let string = TokenTree::Literal(Literal::string(&self.tail));
quote!($output_ident.push_str($string);)
};
self.stmts.push(expr);
self.tail.clear();
}
}
/// Reifies the `Builder` into a raw list of statements.
pub fn build(mut self) -> TokenStream {
let Builder { stmts, .. } = { self.flush(); self };
stmts.into_iter().collect()
}
/// Pushes a statement, flushing the tail buffer in the process.
pub fn push<T>(&mut self, stmt: T) where T: Into<TokenStream> {
self.flush();
self.stmts.push(stmt.into())
}
/// Pushes a literal string to the tail buffer.
fn push_str(&mut self, s: &str) {
self.tail.push_str(s);
}
/// Appends a literal string.
pub fn string(&mut self, s: &str) {
self.push_str(&html_escape(s));
}
/// Appends the result of an expression.
pub fn splice(&mut self, expr: TokenStream) {
let output_ident = self.output_ident.clone();
self.push(quote!({
// Create a local trait alias so that autoref works
trait Render: maud::Render {
fn __maud_render_to(&self, output_ident: &mut String) {
maud::Render::render_to(self, output_ident);
}
}
impl<T: maud::Render> Render for T {}
$expr.__maud_render_to(&mut $output_ident);
}));
}
pub fn element_open_start(&mut self, name: &str) {
self.push_str("<");
self.push_str(name);
}
pub fn attribute_start(&mut self, name: &str) {
self.push_str(" ");
self.push_str(name);
self.push_str("=\"");
}
pub fn attribute_empty(&mut self, name: &str) {
self.push_str(" ");
self.push_str(name);
}
pub fn attribute_end(&mut self) {
self.push_str("\"");
}
pub fn element_open_end(&mut self) {
self.push_str(">");
}
pub fn element_close(&mut self, name: &str) {
self.push_str("</");
self.push_str(name);
self.push_str(">");
}
/// Emits an `if` expression.
///
/// The condition is a token stream (not an expression) so we don't
/// need to special-case `if let`.
pub fn emit_if(
&mut self,
mut cond: TokenStream,
cond_span: Span,
body: TokenStream,
) {
// If the condition contains an opening brace `{`,
// wrap it in parentheses to avoid parse errors
if cond.clone().into_iter().any(|token| match token {
TokenTree::Group(group) => group.delimiter() == Delimiter::Brace,
_ => false,
}) {
let mut group = Group::new(Delimiter::Parenthesis, cond);
// NOTE: Do we need to do this?
group.set_span(cond_span);
cond = TokenStream::from(TokenTree::Group(group));
}
self.push(quote!(if $cond { $body }));
}
}
fn html_escape(s: &str) -> String {
use std::fmt::Write;
let mut buffer = String::new();
Escaper::new(&mut buffer).write_str(s).unwrap();
buffer
}

279
maud_macros/src/generate.rs Normal file
View file

@ -0,0 +1,279 @@
use maud_htmlescape::Escaper;
use proc_macro::{
Delimiter,
Group,
Literal,
quote,
Span,
Term,
TokenStream,
TokenTree,
};
use ast::*;
pub fn generate(markups: Vec<Markup>, output_ident: TokenTree) -> TokenStream {
let mut build = Builder::new(output_ident.clone());
Generator::new(output_ident).markups(markups, &mut build);
build.finish()
}
struct Generator {
output_ident: TokenTree,
}
impl Generator {
fn new(output_ident: TokenTree) -> Generator {
Generator { output_ident }
}
fn builder(&self) -> Builder {
Builder::new(self.output_ident.clone())
}
fn markups(&self, markups: Vec<Markup>, build: &mut Builder) {
for markup in markups {
self.markup(markup, build);
}
}
fn markup(&self, markup: Markup, build: &mut Builder) {
match markup {
Markup::Block(Block { markups, span }) => {
if markups.iter().any(|markup| matches!(*markup, Markup::Let { .. })) {
build.push_tokens(self.block(Block { markups, span }));
} else {
self.markups(markups, build);
}
},
Markup::Literal { content, .. } => build.push_escaped(&content),
Markup::Symbol { symbol } => self.name(symbol, build),
Markup::Splice { expr } => build.push_tokens(self.splice(expr)),
Markup::Element { name, attrs, body } => self.element(name, attrs, body, build),
Markup::Let { tokens } => build.push_tokens(tokens),
Markup::If { segments } => {
for segment in segments {
build.push_tokens(self.special(segment));
}
},
Markup::Special(special) => build.push_tokens(self.special(special)),
Markup::Match { head, arms, arms_span } => {
build.push_tokens({
let body = arms
.into_iter()
.map(|arm| self.special(arm))
.collect();
let mut body = TokenTree::Group(Group::new(Delimiter::Brace, body));
body.set_span(arms_span);
quote!($head $body)
});
},
}
}
fn block(&self, Block { markups, span }: Block) -> TokenStream {
let mut build = self.builder();
self.markups(markups, &mut build);
let mut block = TokenTree::Group(Group::new(Delimiter::Brace, build.finish()));
block.set_span(span);
TokenStream::from(block)
}
fn splice(&self, expr: TokenStream) -> TokenStream {
let output_ident = self.output_ident.clone();
quote!({
// Create a local trait alias so that autoref works
trait Render: maud::Render {
fn __maud_render_to(&self, output_ident: &mut String) {
maud::Render::render_to(self, output_ident);
}
}
impl<T: maud::Render> Render for T {}
$expr.__maud_render_to(&mut $output_ident);
})
}
fn element(
&self,
name: TokenStream,
attrs: Attrs,
body: Option<Box<Markup>>,
build: &mut Builder,
) {
build.push_str("<");
self.name(name.clone(), build);
self.attrs(attrs, build);
build.push_str(">");
if let Some(body) = body {
self.markup(*body, build);
build.push_str("</");
self.name(name, build);
build.push_str(">");
}
}
fn name(&self, name: TokenStream, build: &mut Builder) {
let string = name.into_iter().map(|token| token.to_string()).collect::<String>();
build.push_escaped(&string);
}
fn attrs(&self, attrs: Attrs, build: &mut Builder) {
for Attribute { name, attr_type } in desugar_attrs(attrs) {
match attr_type {
AttrType::Normal { value } => {
build.push_str(" ");
self.name(name, build);
build.push_str("=\"");
self.markup(value, build);
build.push_str("\"");
},
AttrType::Empty { toggler: None } => {
build.push_str(" ");
self.name(name, build);
},
AttrType::Empty { toggler: Some(toggler) } => {
let head = desugar_toggler(toggler);
build.push_tokens({
let mut build = self.builder();
build.push_str(" ");
self.name(name, &mut build);
let body = build.finish();
quote!($head { $body })
})
},
}
}
}
fn special(&self, Special { head, body }: Special) -> TokenStream {
let body = self.block(body);
quote!($head $body)
}
}
////////////////////////////////////////////////////////
fn desugar_attrs(Attrs { classes_static, classes_toggled, ids, attrs }: Attrs) -> Vec<Attribute> {
let classes = desugar_classes_or_ids("class", classes_static, classes_toggled);
let ids = desugar_classes_or_ids("id", ids, vec![]);
classes.into_iter().chain(ids).chain(attrs).collect()
}
fn desugar_classes_or_ids(
attr_name: &'static str,
values_static: Vec<ClassOrId>,
values_toggled: Vec<(ClassOrId, Toggler)>,
) -> Option<Attribute> {
if values_static.is_empty() && values_toggled.is_empty() {
return None;
}
let mut markups = Vec::new();
let mut leading_space = false;
for symbol in values_static {
markups.extend(prepend_leading_space(symbol, &mut leading_space));
}
for (symbol, toggler) in values_toggled {
let body = Block {
markups: prepend_leading_space(symbol, &mut leading_space),
span: toggler.cond_span,
};
let head = desugar_toggler(toggler);
markups.push(Markup::Special(Special { head, body }));
}
Some(Attribute {
name: TokenStream::from(TokenTree::Term(Term::new(attr_name, Span::call_site()))),
attr_type: AttrType::Normal {
value: Markup::Block(Block {
markups,
span: Span::call_site(),
}),
},
})
}
fn prepend_leading_space(symbol: TokenStream, leading_space: &mut bool) -> Vec<Markup> {
let mut markups = Vec::new();
if *leading_space {
markups.push(Markup::Literal {
content: " ".to_owned(),
span: span_tokens(symbol.clone()),
});
}
*leading_space = true;
markups.push(Markup::Symbol { symbol });
markups
}
fn desugar_toggler(Toggler { mut cond, cond_span }: Toggler) -> TokenStream {
// If the expression contains an opening brace `{`,
// wrap it in parentheses to avoid parse errors
if cond.clone().into_iter().any(|token| match token {
TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => true,
_ => false,
}) {
let mut wrapped_cond = TokenTree::Group(Group::new(Delimiter::Parenthesis, cond));
wrapped_cond.set_span(cond_span);
cond = TokenStream::from(wrapped_cond);
}
quote!(if $cond)
}
fn span_tokens<I: IntoIterator<Item=TokenTree>>(tokens: I) -> Span {
tokens
.into_iter()
.fold(None, |span: Option<Span>, token| Some(match span {
None => token.span(),
Some(span) => span.join(token.span()).unwrap_or(span),
}))
.unwrap_or_else(Span::def_site)
}
////////////////////////////////////////////////////////
struct Builder {
output_ident: TokenTree,
tokens: Vec<TokenTree>,
tail: String,
}
impl Builder {
fn new(output_ident: TokenTree) -> Builder {
Builder {
output_ident,
tokens: Vec::new(),
tail: String::new(),
}
}
fn push_str(&mut self, string: &str) {
self.tail.push_str(string);
}
fn push_escaped(&mut self, string: &str) {
use std::fmt::Write;
Escaper::new(&mut self.tail).write_str(string).unwrap();
}
fn push_tokens<T: IntoIterator<Item=TokenTree>>(&mut self, tokens: T) {
self.cut();
self.tokens.extend(tokens);
}
fn cut(&mut self) {
if self.tail.is_empty() {
return;
}
let push_str_expr = {
let output_ident = self.output_ident.clone();
let string = TokenTree::Literal(Literal::string(&self.tail));
quote!($output_ident.push_str($string);)
};
self.tail.clear();
self.tokens.extend(push_str_expr);
}
fn finish(mut self) -> TokenStream {
self.cut();
self.tokens.into_iter().collect()
}
}

View file

@ -8,11 +8,13 @@
#![cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] #![cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))]
extern crate literalext; extern crate literalext;
#[macro_use] extern crate matches;
extern crate maud_htmlescape; extern crate maud_htmlescape;
extern crate proc_macro; extern crate proc_macro;
mod ast;
mod generate;
mod parse; mod parse;
mod build;
use proc_macro::{Literal, Span, Term, TokenStream, TokenTree}; use proc_macro::{Literal, Span, Term, TokenStream, TokenTree};
use proc_macro::quote; use proc_macro::quote;
@ -37,10 +39,11 @@ fn expand(input: TokenStream) -> TokenStream {
// code size of the template itself // code size of the template itself
let size_hint = input.to_string().len(); let size_hint = input.to_string().len();
let size_hint = TokenTree::Literal(Literal::u64_unsuffixed(size_hint as u64)); let size_hint = TokenTree::Literal(Literal::u64_unsuffixed(size_hint as u64));
let stmts = match parse::parse(input, output_ident.clone()) { let markups = match parse::parse(input) {
Ok(stmts) => stmts, Ok(markups) => markups,
Err(e) => panic!(e), Err(e) => panic!(e),
}; };
let stmts = generate::generate(markups, output_ident.clone());
quote!({ quote!({
extern crate maud; extern crate maud;
let mut $output_ident = String::with_capacity($size_hint); let mut $output_ident = String::with_capacity($size_hint);

View file

@ -1,30 +1,24 @@
use proc_macro::{ use proc_macro::{
Delimiter, Delimiter,
Group,
Literal, Literal,
Spacing, Spacing,
Span, Span,
TokenStream, TokenStream,
TokenTree, TokenTree,
}; };
use std::iter;
use std::mem; use std::mem;
use literalext::LiteralExt; use literalext::LiteralExt;
use super::build::Builder; use ast;
use super::ParseResult; use ParseResult;
pub fn parse(input: TokenStream, output_ident: TokenTree) -> ParseResult<TokenStream> { pub fn parse(input: TokenStream) -> ParseResult<Vec<ast::Markup>> {
let mut parser = Parser::new(input, output_ident); Parser::new(input).markups()
let mut builder = parser.builder();
parser.markups(&mut builder)?;
Ok(builder.build())
} }
#[derive(Clone)] #[derive(Clone)]
struct Parser { struct Parser {
output_ident: TokenTree,
/// Indicates whether we're inside an attribute node. /// Indicates whether we're inside an attribute node.
in_attr: bool, in_attr: bool,
input: <TokenStream as IntoIterator>::IntoIter, input: <TokenStream as IntoIterator>::IntoIter,
@ -39,9 +33,8 @@ impl Iterator for Parser {
} }
impl Parser { impl Parser {
fn new(input: TokenStream, output_ident: TokenTree) -> Parser { fn new(input: TokenStream) -> Parser {
Parser { Parser {
output_ident,
in_attr: false, in_attr: false,
input: input.into_iter(), input: input.into_iter(),
} }
@ -49,16 +42,11 @@ impl Parser {
fn with_input(&self, input: TokenStream) -> Parser { fn with_input(&self, input: TokenStream) -> Parser {
Parser { Parser {
output_ident: self.output_ident.clone(),
in_attr: self.in_attr, in_attr: self.in_attr,
input: input.into_iter(), input: input.into_iter(),
} }
} }
fn builder(&self) -> Builder {
Builder::new(self.output_ident.clone())
}
/// Returns the next token in the stream without consuming it. /// Returns the next token in the stream without consuming it.
fn peek(&mut self) -> Option<TokenTree> { fn peek(&mut self) -> Option<TokenTree> {
self.clone().next() self.clone().next()
@ -92,52 +80,54 @@ impl Parser {
} }
/// Parses and renders multiple blocks of markup. /// Parses and renders multiple blocks of markup.
fn markups(&mut self, builder: &mut Builder) -> ParseResult<()> { fn markups(&mut self) -> ParseResult<Vec<ast::Markup>> {
let mut result = Vec::new();
loop { loop {
match self.peek2() { match self.peek2() {
None => return Ok(()), None => break,
Some((TokenTree::Op(op), _)) if op.op() == ';' => self.advance(), Some((TokenTree::Op(op), _)) if op.op() == ';' => self.advance(),
Some((TokenTree::Op(op), Some(TokenTree::Term(term)))) if op.op() == '@' && term.as_str() == "let" => { Some((
// When emitting a `@let`, wrap the rest of the block in a TokenTree::Op(op),
// new block to avoid scoping issues Some(TokenTree::Term(term)),
)) if op.op() == '@' && term.as_str() == "let" => {
self.advance2(); self.advance2();
builder.push({ let keyword = TokenTree::Term(term);
let mut builder = self.builder(); result.push(self.let_expr(keyword)?);
builder.push(TokenTree::Term(term));
self.let_expr(&mut builder)?;
self.markups(&mut builder)?;
TokenTree::Group(Group::new(Delimiter::Brace, builder.build()))
});
}, },
_ => self.markup(builder)?, _ => result.push(self.markup()?),
} }
} }
Ok(result)
} }
/// Parses and renders a single block of markup. /// Parses and renders a single block of markup.
fn markup(&mut self, builder: &mut Builder) -> ParseResult<()> { fn markup(&mut self) -> ParseResult<ast::Markup> {
let token = match self.peek() { let token = match self.peek() {
Some(token) => token, Some(token) => token,
None => return self.error("unexpected end of input"), None => return self.error("unexpected end of input"),
}; };
match token { let markup = match token {
// Literal // Literal
TokenTree::Literal(lit) => { TokenTree::Literal(lit) => {
self.advance(); self.advance();
self.literal(lit, builder)?; self.literal(&lit)?
}, },
// Special form // Special form
TokenTree::Op(op) if op.op() == '@' => { TokenTree::Op(op) if op.op() == '@' => {
self.advance(); self.advance();
match self.next() { match self.next() {
Some(TokenTree::Term(term)) => { Some(TokenTree::Term(term)) => {
builder.push(TokenTree::Term(term)); let keyword = TokenTree::Term(term);
match term.as_str() { match term.as_str() {
"if" => self.if_expr(builder)?, "if" => {
"while" => self.while_expr(builder)?, let mut segments = Vec::new();
"for" => self.for_expr(builder)?, self.if_expr(vec![keyword], &mut segments)?;
"match" => self.match_expr(builder)?, ast::Markup::If { segments }
"let" => return self.error("let only works inside a block"), },
"while" => self.while_expr(keyword)?,
"for" => self.for_expr(keyword)?,
"match" => self.match_expr(keyword)?,
"let" => return self.error("@let only works inside a block"),
other => return self.error(format!("unknown keyword `@{}`", other)), other => return self.error(format!("unknown keyword `@{}`", other)),
} }
}, },
@ -147,82 +137,91 @@ impl Parser {
// Element // Element
TokenTree::Term(_) => { TokenTree::Term(_) => {
let name = self.namespaced_name()?; let name = self.namespaced_name()?;
self.element(&name, builder)?; self.element(name)?
}, },
// Splice // Splice
TokenTree::Group(ref group) if group.delimiter() == Delimiter::Parenthesis => { TokenTree::Group(ref group) if group.delimiter() == Delimiter::Parenthesis => {
self.advance(); self.advance();
builder.splice(group.stream()); ast::Markup::Splice { expr: group.stream() }
}, }
// Block // Block
TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => { TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => {
self.advance(); self.advance();
self.with_input(group.stream()).markups(builder)?; ast::Markup::Block(self.block(group.stream(), group.span())?)
}, },
// ??? // ???
_ => return self.error("invalid syntax"), _ => return self.error("invalid syntax"),
} };
Ok(()) Ok(markup)
} }
/// Parses and renders a literal string. /// Parses and renders a literal string.
fn literal(&mut self, lit: Literal, builder: &mut Builder) -> ParseResult<()> { fn literal(&mut self, lit: &Literal) -> ParseResult<ast::Markup> {
if let Some(s) = lit.parse_string() { if let Some(s) = lit.parse_string() {
builder.string(&s); Ok(ast::Markup::Literal {
Ok(()) content: s.to_string(),
span: lit.span(),
})
} else { } else {
self.error("expected string") self.error("expected string")
} }
} }
/// Parses and renders an `@if` expression. /// Parses an `@if` expression.
/// ///
/// The leading `@if` should already be consumed. /// The leading `@if` should already be consumed.
fn if_expr(&mut self, builder: &mut Builder) -> ParseResult<()> { fn if_expr(
loop { &mut self,
prefix: Vec<TokenTree>,
segments: &mut Vec<ast::Special>,
) -> ParseResult<()> {
let mut head = prefix;
let body = loop {
match self.next() { match self.next() {
Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => { Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
let block = self.block(block.stream(), block.span())?; break self.block(block.stream(), block.span())?;
builder.push(block);
break;
}, },
Some(token) => builder.push(token), Some(token) => head.push(token),
None => return self.error("unexpected end of @if expression"), None => return self.error("unexpected end of @if expression"),
} }
} };
self.else_if_expr(builder) segments.push(ast::Special { head: head.into_iter().collect(), body });
self.else_if_expr(segments)
} }
/// Parses and renders an optional `@else if` or `@else`. /// Parses an optional `@else if` or `@else`.
/// ///
/// The leading `@else if` or `@else` should *not* already be consumed. /// The leading `@else if` or `@else` should *not* already be consumed.
fn else_if_expr(&mut self, builder: &mut Builder) -> ParseResult<()> { fn else_if_expr(&mut self, segments: &mut Vec<ast::Special>) -> ParseResult<()> {
match self.peek2() { match self.peek2() {
Some(( Some((
TokenTree::Op(op), TokenTree::Op(op),
Some(TokenTree::Term(else_keyword)), Some(TokenTree::Term(else_keyword)),
)) if op.op() == '@' && else_keyword.as_str() == "else" => { )) if op.op() == '@' && else_keyword.as_str() == "else" => {
self.advance2(); self.advance2();
builder.push(TokenTree::Term(else_keyword)); let else_keyword = TokenTree::Term(else_keyword);
match self.peek() { match self.peek() {
// `@else if` // `@else if`
Some(TokenTree::Term(if_keyword)) if if_keyword.as_str() == "if" => { Some(TokenTree::Term(if_keyword)) if if_keyword.as_str() == "if" => {
self.advance(); self.advance();
builder.push(TokenTree::Term(if_keyword)); let if_keyword = TokenTree::Term(if_keyword);
self.if_expr(builder)?; self.if_expr(vec![else_keyword, if_keyword], segments)
}, },
// Just an `@else` // Just an `@else`
_ => { _ => {
match self.next() { match self.next() {
Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Brace => { Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Brace => {
let block = self.block(group.stream(), group.span())?; let body = self.block(group.stream(), group.span())?;
builder.push(block); segments.push(ast::Special {
head: vec![else_keyword].into_iter().collect(),
body,
});
Ok(())
}, },
_ => return self.error("expected body for @else"), _ => self.error("expected body for @else"),
} }
}, },
} }
self.else_if_expr(builder)
}, },
// We didn't find an `@else`; stop // We didn't find an `@else`; stop
_ => Ok(()), _ => Ok(()),
@ -232,95 +231,90 @@ impl Parser {
/// Parses and renders an `@while` expression. /// Parses and renders an `@while` expression.
/// ///
/// The leading `@while` should already be consumed. /// The leading `@while` should already be consumed.
fn while_expr(&mut self, builder: &mut Builder) -> ParseResult<()> { fn while_expr(&mut self, keyword: TokenTree) -> ParseResult<ast::Markup> {
loop { let mut head = vec![keyword];
let body = loop {
match self.next() { match self.next() {
Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => { Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
let block = self.block(block.stream(), block.span())?; break self.block(block.stream(), block.span())?;
builder.push(block);
break;
}, },
Some(token) => builder.push(token), Some(token) => head.push(token),
None => return self.error("unexpected end of @while expression"), None => return self.error("unexpected end of @while expression"),
} }
} };
Ok(()) Ok(ast::Markup::Special(ast::Special { head: head.into_iter().collect(), body }))
} }
/// Parses and renders a `@for` expression. /// Parses a `@for` expression.
/// ///
/// The leading `@for` should already be consumed. /// The leading `@for` should already be consumed.
fn for_expr(&mut self, builder: &mut Builder) -> ParseResult<()> { fn for_expr(&mut self, keyword: TokenTree) -> ParseResult<ast::Markup> {
let mut head = vec![keyword];
loop { loop {
match self.next() { match self.next() {
Some(TokenTree::Term(in_keyword)) if in_keyword.as_str() == "in" => { Some(TokenTree::Term(in_keyword)) if in_keyword.as_str() == "in" => {
builder.push(TokenTree::Term(in_keyword)); head.push(TokenTree::Term(in_keyword));
break; break;
}, },
Some(token) => builder.push(token), Some(token) => head.push(token),
None => return self.error("unexpected end of @for expression"), None => return self.error("unexpected end of @for expression"),
} }
} }
loop { let body = loop {
match self.next() { match self.next() {
Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => { Some(TokenTree::Group(ref block)) if block.delimiter() == Delimiter::Brace => {
let block = self.block(block.stream(), block.span())?; break self.block(block.stream(), block.span())?;
builder.push(block);
break;
}, },
Some(token) => builder.push(token), Some(token) => head.push(token),
None => return self.error("unexpected end of @for expression"), None => return self.error("unexpected end of @for expression"),
} }
} };
Ok(()) Ok(ast::Markup::Special(ast::Special { head: head.into_iter().collect(), body }))
} }
/// Parses and renders a `@match` expression. /// Parses a `@match` expression.
/// ///
/// The leading `@match` should already be consumed. /// The leading `@match` should already be consumed.
fn match_expr(&mut self, builder: &mut Builder) -> ParseResult<()> { fn match_expr(&mut self, keyword: TokenTree) -> ParseResult<ast::Markup> {
loop { let mut head = vec![keyword];
let (arms, arms_span) = loop {
match self.next() { match self.next() {
Some(TokenTree::Group(ref body)) if body.delimiter() == Delimiter::Brace => { Some(TokenTree::Group(ref body)) if body.delimiter() == Delimiter::Brace => {
let span = body.span(); let span = body.span();
let body = self.with_input(body.stream()).match_arms()?; break (self.with_input(body.stream()).match_arms()?, span);
let mut body = Group::new(Delimiter::Brace, body);
body.set_span(span);
builder.push(TokenTree::Group(body));
break;
}, },
Some(token) => builder.push(token), Some(token) => head.push(token),
None => return self.error("unexpected end of @match expression"), None => return self.error("unexpected end of @match expression"),
} }
} };
Ok(()) Ok(ast::Markup::Match { head: head.into_iter().collect(), arms, arms_span })
} }
fn match_arms(&mut self) -> ParseResult<TokenStream> { fn match_arms(&mut self) -> ParseResult<Vec<ast::Special>> {
let mut arms = Vec::new(); let mut arms = Vec::new();
while let Some(arm) = self.match_arm()? { while let Some(arm) = self.match_arm()? {
arms.push(arm); arms.push(arm);
} }
Ok(arms.into_iter().collect()) Ok(arms)
} }
fn match_arm(&mut self) -> ParseResult<Option<TokenStream>> { fn match_arm(&mut self) -> ParseResult<Option<ast::Special>> {
let mut pat = Vec::new(); let mut head = Vec::new();
loop { loop {
match self.peek2() { match self.peek2() {
Some((TokenTree::Op(eq), Some(TokenTree::Op(gt)))) Some((TokenTree::Op(eq), Some(TokenTree::Op(gt))))
if eq.op() == '=' && gt.op() == '>' && eq.spacing() == Spacing::Joint => { if eq.op() == '=' && gt.op() == '>' && eq.spacing() == Spacing::Joint => {
self.advance2(); self.advance2();
pat.push(TokenTree::Op(eq)); head.push(TokenTree::Op(eq));
pat.push(TokenTree::Op(gt)); head.push(TokenTree::Op(gt));
break; break;
}, },
Some((token, _)) => { Some((token, _)) => {
self.advance(); self.advance();
pat.push(token); head.push(token);
}, },
None => None =>
if pat.is_empty() { if head.is_empty() {
return Ok(None); return Ok(None);
} else { } else {
return self.error("unexpected end of @match pattern"); return self.error("unexpected end of @match pattern");
@ -359,22 +353,23 @@ impl Parser {
}, },
None => return self.error("unexpected end of @match arm"), None => return self.error("unexpected end of @match arm"),
}; };
Ok(Some(pat.into_iter().chain(iter::once(body)).collect())) Ok(Some(ast::Special { head: head.into_iter().collect(), body }))
} }
/// Parses and renders a `@let` expression. /// Parses a `@let` expression.
/// ///
/// The leading `@let` should already be consumed. /// The leading `@let` should already be consumed.
fn let_expr(&mut self, builder: &mut Builder) -> ParseResult<()> { fn let_expr(&mut self, keyword: TokenTree) -> ParseResult<ast::Markup> {
let mut tokens = vec![keyword];
loop { loop {
match self.next() { match self.next() {
Some(token) => { Some(token) => {
match token { match token {
TokenTree::Op(ref op) if op.op() == '=' => { TokenTree::Op(ref op) if op.op() == '=' => {
builder.push(token.clone()); tokens.push(token.clone());
break; break;
}, },
_ => builder.push(token), _ => tokens.push(token),
} }
}, },
None => return self.error("unexpected end of @let expression"), None => return self.error("unexpected end of @let expression"),
@ -385,46 +380,43 @@ impl Parser {
Some(token) => { Some(token) => {
match token { match token {
TokenTree::Op(ref op) if op.op() == ';' => { TokenTree::Op(ref op) if op.op() == ';' => {
builder.push(token.clone()); tokens.push(token.clone());
break; break;
}, },
_ => builder.push(token), _ => tokens.push(token),
} }
}, },
None => return self.error("unexpected end of @let expression"), None => return self.error("unexpected end of @let expression"),
} }
} }
Ok(()) Ok(ast::Markup::Let { tokens: tokens.into_iter().collect() })
} }
/// Parses and renders an element node. /// Parses an element node.
/// ///
/// The element name should already be consumed. /// The element name should already be consumed.
fn element(&mut self, name: &str, builder: &mut Builder) -> ParseResult<()> { fn element(&mut self, name: TokenStream) -> ParseResult<ast::Markup> {
if self.in_attr { if self.in_attr {
return self.error("unexpected element, you silly bumpkin"); return self.error("unexpected element, you silly bumpkin");
} }
builder.element_open_start(name); let attrs = self.attrs()?;
self.attrs(builder)?; let body = match self.peek() {
builder.element_open_end();
match self.peek() {
Some(TokenTree::Op(o)) if o.op() == ';' || o.op() == '/' => { Some(TokenTree::Op(o)) if o.op() == ';' || o.op() == '/' => {
// Void element // Void element
self.advance(); self.advance();
None
}, },
_ => { _ => Some(Box::new(self.markup()?)),
self.markup(builder)?; };
builder.element_close(name); Ok(ast::Markup::Element { name, attrs, body })
},
}
Ok(())
} }
/// Parses and renders the attributes of an element. /// Parses the attributes of an element.
fn attrs(&mut self, builder: &mut Builder) -> ParseResult<()> { fn attrs(&mut self) -> ParseResult<ast::Attrs> {
let mut classes_static = Vec::new(); let mut classes_static = Vec::new();
let mut classes_toggled = Vec::new(); let mut classes_toggled = Vec::new();
let mut ids = Vec::new(); let mut ids = Vec::new();
let mut attrs = Vec::new();
loop { loop {
let mut attempt = self.clone(); let mut attempt = self.clone();
let maybe_name = attempt.namespaced_name(); let maybe_name = attempt.namespaced_name();
@ -433,41 +425,35 @@ impl Parser {
// Non-empty attribute // Non-empty attribute
(Ok(ref name), Some(TokenTree::Op(ref op))) if op.op() == '=' => { (Ok(ref name), Some(TokenTree::Op(ref op))) if op.op() == '=' => {
self.commit(attempt); self.commit(attempt);
builder.attribute_start(&name); let value;
{ {
// Parse a value under an attribute context // Parse a value under an attribute context
let in_attr = mem::replace(&mut self.in_attr, true); let in_attr = mem::replace(&mut self.in_attr, true);
self.markup(builder)?; value = self.markup()?;
self.in_attr = in_attr; self.in_attr = in_attr;
} }
builder.attribute_end(); attrs.push(ast::Attribute {
name: name.clone(),
attr_type: ast::AttrType::Normal { value },
});
}, },
// Empty attribute // Empty attribute
(Ok(ref name), Some(TokenTree::Op(ref op))) if op.op() == '?' => { (Ok(ref name), Some(TokenTree::Op(ref op))) if op.op() == '?' => {
self.commit(attempt); self.commit(attempt);
if let Some((cond, cond_span)) = self.attr_toggler() { let toggler = self.attr_toggler();
// Toggle the attribute based on a boolean expression attrs.push(ast::Attribute {
let body = { name: name.clone(),
let mut builder = self.builder(); attr_type: ast::AttrType::Empty { toggler },
builder.attribute_empty(&name); });
builder.build()
};
builder.emit_if(cond, cond_span, body);
} else {
// Write the attribute unconditionally
builder.attribute_empty(&name);
}
}, },
// Class shorthand // Class shorthand
(Err(_), Some(TokenTree::Op(op))) if op.op() == '.' => { (Err(_), Some(TokenTree::Op(op))) if op.op() == '.' => {
self.commit(attempt); self.commit(attempt);
let class_name = self.name()?; let name = self.name()?;
if let Some((cond, cond_span)) = self.attr_toggler() { if let Some(toggler) = self.attr_toggler() {
// Toggle the class based on a boolean expression classes_toggled.push((name, toggler));
classes_toggled.push((cond, cond_span, class_name));
} else { } else {
// Emit the class unconditionally classes_static.push(name);
classes_static.push(class_name);
} }
}, },
// ID shorthand // ID shorthand
@ -479,91 +465,68 @@ impl Parser {
_ => break, _ => break,
} }
} }
if !classes_static.is_empty() || !classes_toggled.is_empty() { Ok(ast::Attrs { classes_static, classes_toggled, ids, attrs })
builder.attribute_start("class");
builder.string(&classes_static.join(" "));
for (i, (cond, cond_span, mut class_name)) in classes_toggled.into_iter().enumerate() {
// If a class comes first in the list, then it shouldn't be
// prefixed by a space
if i > 0 || !classes_static.is_empty() {
class_name = format!(" {}", class_name);
}
let body = {
let mut builder = self.builder();
builder.string(&class_name);
builder.build()
};
builder.emit_if(cond, cond_span, body);
}
builder.attribute_end();
}
if !ids.is_empty() {
builder.attribute_start("id");
builder.string(&ids.join(" "));
builder.attribute_end();
}
Ok(())
} }
/// Parses the `[cond]` syntax after an empty attribute or class shorthand. /// Parses the `[cond]` syntax after an empty attribute or class shorthand.
fn attr_toggler(&mut self) -> Option<(TokenStream, Span)> { fn attr_toggler(&mut self) -> Option<ast::Toggler> {
match self.peek() { match self.peek() {
Some(TokenTree::Group(ref grp)) if grp.delimiter() == Delimiter::Bracket => { Some(TokenTree::Group(ref group)) if group.delimiter() == Delimiter::Bracket => {
self.advance(); self.advance();
Some((grp.stream(), grp.span())) Some(ast::Toggler {
cond: group.stream(),
cond_span: group.span(),
})
}, },
_ => None, _ => None,
} }
} }
/// Parses an identifier, without dealing with namespaces. /// Parses an identifier, without dealing with namespaces.
fn name(&mut self) -> ParseResult<String> { fn name(&mut self) -> ParseResult<TokenStream> {
let mut s = if let Some(TokenTree::Term(term)) = self.peek() { let mut result = Vec::new();
if let Some(token @ TokenTree::Term(_)) = self.peek() {
self.advance(); self.advance();
String::from(term.as_str()) result.push(token);
} else { } else {
return self.error("expected identifier"); return self.error("expected identifier");
}; }
let mut expect_ident = false; let mut expect_ident = false;
loop { loop {
expect_ident = match self.peek() { expect_ident = match self.peek() {
Some(TokenTree::Op(op)) if op.op() == '-' => { Some(TokenTree::Op(op)) if op.op() == '-' => {
self.advance(); self.advance();
s.push('-'); result.push(TokenTree::Op(op));
true true
}, },
Some(TokenTree::Term(term)) if expect_ident => { Some(TokenTree::Term(term)) if expect_ident => {
self.advance(); self.advance();
s.push_str(term.as_str()); result.push(TokenTree::Term(term));
false false
}, },
_ => break, _ => break,
}; };
} }
Ok(s) Ok(result.into_iter().collect())
} }
/// Parses a HTML element or attribute name, along with a namespace /// Parses a HTML element or attribute name, along with a namespace
/// if necessary. /// if necessary.
fn namespaced_name(&mut self) -> ParseResult<String> { fn namespaced_name(&mut self) -> ParseResult<TokenStream> {
let mut s = self.name()?; let mut result = vec![self.name()?];
if let Some(TokenTree::Op(op)) = self.peek() { if let Some(TokenTree::Op(op)) = self.peek() {
if op.op() == ':' { if op.op() == ':' {
self.advance(); self.advance();
s.push(':'); result.push(TokenStream::from(TokenTree::Op(op)));
s.push_str(&self.name()?); result.push(self.name()?);
} }
} }
Ok(s) Ok(result.into_iter().collect())
} }
/// Parses the given token stream as a Maud expression, returning a block of /// Parses the given token stream as a Maud expression.
/// Rust code. fn block(&mut self, body: TokenStream, span: Span) -> ParseResult<ast::Block> {
fn block(&mut self, body: TokenStream, span: Span) -> ParseResult<TokenTree> { let markups = self.with_input(body).markups()?;
let mut builder = self.builder(); Ok(ast::Block { markups, span })
self.with_input(body).markups(&mut builder)?;
let mut group = Group::new(Delimiter::Brace, builder.build());
group.set_span(span);
Ok(TokenTree::Group(group))
} }
} }