Change macro internals to use an explicit AST (#127)
This commit is contained in:
parent
7c63fc138a
commit
9d90e94c87
11 changed files with 526 additions and 333 deletions
|
@ -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! ☺️
|
||||
|
||||
|
|
|
@ -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>"#);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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
77
maud_macros/src/ast.rs
Normal 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,
|
||||
}
|
|
@ -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
279
maud_macros/src/generate.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue