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.
* 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! ☺️

View file

@ -191,5 +191,5 @@ fn ids_shorthand() {
#[test]
fn classes_attrs_ids_mixed_up() {
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::lint::{LateContext, LateLintPass, LintArray, LintContext, LintPass};
use super::util::match_def_path;
use syntax::ast::LitKind;
use util::match_def_path;
declare_lint! {
pub MAUD_DOCTYPE,
Warn,

View file

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

View file

@ -5,7 +5,7 @@
use rustc::hir::def_id::DefId;
use rustc::lint::LateContext;
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.
///

View file

@ -13,6 +13,7 @@ description = "Compile-time HTML templates."
[dependencies]
literalext = { version = "0.1", default-features = false, features = ["proc-macro"] }
matches = "0.1.6"
maud_htmlescape = { version = "0.17.0", path = "../maud_htmlescape" }
[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))]
extern crate literalext;
#[macro_use] extern crate matches;
extern crate maud_htmlescape;
extern crate proc_macro;
mod ast;
mod generate;
mod parse;
mod build;
use proc_macro::{Literal, Span, Term, TokenStream, TokenTree};
use proc_macro::quote;
@ -37,10 +39,11 @@ fn expand(input: TokenStream) -> TokenStream {
// code size of the template itself
let size_hint = input.to_string().len();
let size_hint = TokenTree::Literal(Literal::u64_unsuffixed(size_hint as u64));
let stmts = match parse::parse(input, output_ident.clone()) {
Ok(stmts) => stmts,
let markups = match parse::parse(input) {
Ok(markups) => markups,
Err(e) => panic!(e),
};
let stmts = generate::generate(markups, output_ident.clone());
quote!({
extern crate maud;
let mut $output_ident = String::with_capacity($size_hint);

View file

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