From 9d56ba0bcb2fba569fb5c21b69d2763317d3d67c Mon Sep 17 00:00:00 2001 From: Chris Wong Date: Sat, 9 Jun 2018 19:27:25 +1200 Subject: [PATCH 1/6] Keep spans for semicolons; change element body to a block --- maud_macros/src/ast.rs | 8 +++++++- maud_macros/src/generate.rs | 6 +++--- maud_macros/src/parse.rs | 14 ++++++++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs index d89e1bdf..5e5f7225 100644 --- a/maud_macros/src/ast.rs +++ b/maud_macros/src/ast.rs @@ -16,7 +16,7 @@ pub enum Markup { Element { name: TokenStream, attrs: Attrs, - body: Option>, + body: ElementBody, }, Let { tokens: TokenStream, @@ -42,6 +42,12 @@ pub struct Attrs { pub type ClassOrId = TokenStream; +#[derive(Debug)] +pub enum ElementBody { + Void { semi_span: Span }, + Block { block: Block }, +} + #[derive(Debug)] pub struct Block { pub markups: Vec, diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs index 8528ce37..08fa58ae 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -96,15 +96,15 @@ impl Generator { &self, name: TokenStream, attrs: Attrs, - body: Option>, + body: ElementBody, 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); + if let ElementBody::Block { block } = body { + self.markups(block.markups, build); build.push_str(""); diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index a5cb24ad..80d34fb9 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -417,9 +417,19 @@ impl Parser { if punct.as_char() == ';' || punct.as_char() == '/' => { // Void element self.advance(); - None + ast::ElementBody::Void { semi_span: punct.span() } + }, + _ => { + match self.markup()? { + ast::Markup::Block(block) => ast::ElementBody::Block { block }, + markup => ast::ElementBody::Block { + block: ast::Block { + markups: vec![markup], + span: Span::call_site(), + }, + }, + } }, - _ => Some(Box::new(self.markup()?)), }; Ok(ast::Markup::Element { name, attrs, body }) } From bfa15a0081562f99bc09eb358ec8026ee455c9d4 Mon Sep 17 00:00:00 2001 From: Chris Wong Date: Sat, 16 Jun 2018 20:49:46 +1200 Subject: [PATCH 2/6] Rename Block.span to Block.outer_span --- maud_macros/src/ast.rs | 2 +- maud_macros/src/generate.rs | 12 ++++++------ maud_macros/src/parse.rs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs index 5e5f7225..6d5209bf 100644 --- a/maud_macros/src/ast.rs +++ b/maud_macros/src/ast.rs @@ -51,7 +51,7 @@ pub enum ElementBody { #[derive(Debug)] pub struct Block { pub markups: Vec, - pub span: Span, + pub outer_span: Span, } #[derive(Debug)] diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs index 08fa58ae..c3d53084 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -39,9 +39,9 @@ impl Generator { fn markup(&self, markup: Markup, build: &mut Builder) { match markup { - Markup::Block(Block { markups, span }) => { + Markup::Block(Block { markups, outer_span }) => { if markups.iter().any(|markup| matches!(*markup, Markup::Let { .. })) { - build.push_tokens(self.block(Block { markups, span })); + build.push_tokens(self.block(Block { markups, outer_span })); } else { self.markups(markups, build); } @@ -70,11 +70,11 @@ impl Generator { } } - fn block(&self, Block { markups, span }: Block) -> TokenStream { + fn block(&self, Block { markups, outer_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); + block.set_span(outer_span); TokenStream::from(block) } @@ -179,7 +179,7 @@ fn desugar_classes_or_ids( for (symbol, toggler) in values_toggled { let body = Block { markups: prepend_leading_space(symbol, &mut leading_space), - span: toggler.cond_span, + outer_span: toggler.cond_span, }; let head = desugar_toggler(toggler); markups.push(Markup::Special { @@ -191,7 +191,7 @@ fn desugar_classes_or_ids( attr_type: AttrType::Normal { value: Markup::Block(Block { markups, - span: Span::call_site(), + outer_span: Span::call_site(), }), }, }) diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index 80d34fb9..b1b70c71 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -425,7 +425,7 @@ impl Parser { markup => ast::ElementBody::Block { block: ast::Block { markups: vec![markup], - span: Span::call_site(), + outer_span: Span::call_site(), }, }, } @@ -548,8 +548,8 @@ impl Parser { } /// Parses the given token stream as a Maud expression. - fn block(&mut self, body: TokenStream, span: Span) -> ParseResult { + fn block(&mut self, body: TokenStream, outer_span: Span) -> ParseResult { let markups = self.with_input(body).markups()?; - Ok(ast::Block { markups, span }) + Ok(ast::Block { markups, outer_span }) } } From 0ceb271a50bcfddfef7f4ea8020f755c21120cc3 Mon Sep 17 00:00:00 2001 From: Chris Wong Date: Sat, 16 Jun 2018 20:55:42 +1200 Subject: [PATCH 3/6] Add outer_span property to Splice --- maud_macros/src/ast.rs | 1 + maud_macros/src/generate.rs | 2 +- maud_macros/src/parse.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs index 6d5209bf..d8962b39 100644 --- a/maud_macros/src/ast.rs +++ b/maud_macros/src/ast.rs @@ -12,6 +12,7 @@ pub enum Markup { }, Splice { expr: TokenStream, + outer_span: Span, }, Element { name: TokenStream, diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs index c3d53084..6774cfbf 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -48,7 +48,7 @@ impl Generator { }, Markup::Literal { content, .. } => build.push_escaped(&content), Markup::Symbol { symbol } => self.name(symbol, build), - Markup::Splice { expr } => build.push_tokens(self.splice(expr)), + 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::Special { segments } => { diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index b1b70c71..dd10f5b8 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -143,7 +143,7 @@ impl Parser { // Splice TokenTree::Group(ref group) if group.delimiter() == Delimiter::Parenthesis => { self.advance(); - ast::Markup::Splice { expr: group.stream() } + ast::Markup::Splice { expr: group.stream(), outer_span: group.span() } } // Block TokenTree::Group(ref group) if group.delimiter() == Delimiter::Brace => { From 4b25728efeeaa224cda607bda90cf1cd538e2f9c Mon Sep 17 00:00:00 2001 From: Chris Wong Date: Sat, 16 Jun 2018 20:59:41 +1200 Subject: [PATCH 4/6] Add at_span to Let --- maud_macros/src/ast.rs | 1 + maud_macros/src/generate.rs | 2 +- maud_macros/src/parse.rs | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs index d8962b39..f67a6cc7 100644 --- a/maud_macros/src/ast.rs +++ b/maud_macros/src/ast.rs @@ -20,6 +20,7 @@ pub enum Markup { body: ElementBody, }, Let { + at_span: Span, tokens: TokenStream, }, Special { diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs index 6774cfbf..3d737072 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -50,7 +50,7 @@ impl Generator { 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::Let { tokens, .. } => build.push_tokens(tokens), Markup::Special { segments } => { for segment in segments { build.push_tokens(self.special(segment)); diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index dd10f5b8..ef3c2605 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -92,7 +92,7 @@ impl Parser { )) if punct.as_char() == '@' && ident.to_string() == "let" => { self.advance2(); let keyword = TokenTree::Ident(ident.clone()); - result.push(self.let_expr(keyword)?); + result.push(self.let_expr(punct.span(), keyword)?); }, _ => result.push(self.markup()?), } @@ -371,7 +371,7 @@ impl Parser { /// Parses a `@let` expression. /// /// The leading `@let` should already be consumed. - fn let_expr(&mut self, keyword: TokenTree) -> ParseResult { + fn let_expr(&mut self, at_span: Span, keyword: TokenTree) -> ParseResult { let mut tokens = vec![keyword]; loop { match self.next() { @@ -401,7 +401,7 @@ impl Parser { None => return self.error("unexpected end of @let expression"), } } - Ok(ast::Markup::Let { tokens: tokens.into_iter().collect() }) + Ok(ast::Markup::Let { at_span, tokens: tokens.into_iter().collect() }) } /// Parses an element node. From b9279f7f3f01365415c756eadc8290c3d0d35d6c Mon Sep 17 00:00:00 2001 From: Chris Wong Date: Sat, 16 Jun 2018 21:15:34 +1200 Subject: [PATCH 5/6] Add .span() method to derive the span of an AST node --- maud_macros/src/ast.rs | 66 ++++++++++++++++++++++++++++++++++++- maud_macros/src/generate.rs | 10 ------ 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/maud_macros/src/ast.rs b/maud_macros/src/ast.rs index f67a6cc7..7415935a 100644 --- a/maud_macros/src/ast.rs +++ b/maud_macros/src/ast.rs @@ -1,4 +1,4 @@ -use proc_macro::{Span, TokenStream}; +use proc_macro::{Span, TokenStream, TokenTree}; #[derive(Debug)] pub enum Markup { @@ -34,6 +34,30 @@ pub enum Markup { }, } +impl Markup { + pub fn span(&self) -> Span { + match *self { + Markup::Block(ref block) => block.span(), + Markup::Literal { span, .. } => span, + Markup::Symbol { ref symbol } => span_tokens(symbol.clone()), + Markup::Splice { outer_span, .. } => outer_span, + Markup::Element { ref name, ref body, .. } => { + let name_span = span_tokens(name.clone()); + name_span.join(body.span()).unwrap_or(name_span) + }, + Markup::Let { at_span, ref tokens } => { + at_span.join(span_tokens(tokens.clone())).unwrap_or(at_span) + }, + Markup::Special { ref segments } => { + join_spans(segments.iter().map(|segment| segment.span())) + }, + Markup::Match { at_span, arms_span, .. } => { + at_span.join(arms_span).unwrap_or(at_span) + }, + } + } +} + #[derive(Debug)] pub struct Attrs { pub classes_static: Vec, @@ -50,12 +74,27 @@ pub enum ElementBody { Block { block: Block }, } +impl ElementBody { + pub fn span(&self) -> Span { + match *self { + ElementBody::Void { semi_span } => semi_span, + ElementBody::Block { ref block } => block.span(), + } + } +} + #[derive(Debug)] pub struct Block { pub markups: Vec, pub outer_span: Span, } +impl Block { + pub fn span(&self) -> Span { + self.outer_span + } +} + #[derive(Debug)] pub struct Special { pub at_span: Span, @@ -63,6 +102,13 @@ pub struct Special { pub body: Block, } +impl Special { + pub fn span(&self) -> Span { + let body_span = self.body.span(); + self.at_span.join(body_span).unwrap_or(self.at_span) + } +} + #[derive(Debug)] pub struct Attribute { pub name: TokenStream, @@ -90,3 +136,21 @@ pub struct MatchArm { pub head: TokenStream, pub body: Block, } + +pub fn span_tokens>(tokens: I) -> Span { + join_spans(tokens.into_iter().map(|token| token.span())) +} + +pub fn join_spans>(spans: I) -> Span { + let mut iter = spans.into_iter(); + let mut span = match iter.next() { + Some(span) => span, + None => return Span::call_site(), + }; + for new_span in iter { + if let Some(joined) = span.join(new_span) { + span = joined; + } + } + span +} diff --git a/maud_macros/src/generate.rs b/maud_macros/src/generate.rs index 3d737072..1b48b275 100644 --- a/maud_macros/src/generate.rs +++ b/maud_macros/src/generate.rs @@ -224,16 +224,6 @@ fn desugar_toggler(Toggler { mut cond, cond_span }: Toggler) -> TokenStream { quote!(if $cond) } -fn span_tokens>(tokens: I) -> Span { - tokens - .into_iter() - .fold(None, |span: Option, token| Some(match span { - None => token.span(), - Some(span) => span.join(token.span()).unwrap_or(span), - })) - .unwrap_or_else(Span::def_site) -} - //////////////////////////////////////////////////////// struct Builder { From 41aea5f92c9f3244319efd64ce1ff88220f2e507 Mon Sep 17 00:00:00 2001 From: Chris Wong Date: Sat, 16 Jun 2018 21:33:23 +1200 Subject: [PATCH 6/6] Require braces around element bodies --- maud/tests/basic_syntax.rs | 20 +++++++------------- maud/tests/control_structures.rs | 22 ++++++++++++++-------- maud_extras/lib.rs | 2 +- maud_macros/src/parse.rs | 17 ++++++++++++----- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/maud/tests/basic_syntax.rs b/maud/tests/basic_syntax.rs index 49b5bc6a..8c33dc73 100644 --- a/maud/tests/basic_syntax.rs +++ b/maud/tests/basic_syntax.rs @@ -50,12 +50,6 @@ fn simple_elements() { assert_eq!(s, "

picklebarrelkumquat

"); } -#[test] -fn nesting_elements() { - let s = html!(html body div p sup "butts").into_string(); - assert_eq!(s, "

butts

"); -} - #[test] fn empty_elements() { let s = html!("pinkie" br; "pie").into_string(); @@ -73,7 +67,7 @@ fn simple_attributes() { let s = html! { link rel="stylesheet" href="styles.css"; section id="midriff" { - p class="hotpink" "Hello!" + p class="hotpink" { "Hello!" } } }.into_string(); assert_eq!(s, concat!( @@ -83,7 +77,7 @@ fn simple_attributes() { #[test] fn empty_attributes() { - let s = html!(div readonly? input type="checkbox" checked?;).into_string(); + let s = html!(div readonly? { input type="checkbox" checked?; }).into_string(); assert_eq!(s, r#"
"#); } @@ -112,7 +106,7 @@ fn toggle_empty_attributes_braces() { #[test] fn colons_in_names() { - let s = html!(pon-pon:controls-alpha a on:click="yay()" "Yay!").into_string(); + let s = html!(pon-pon:controls-alpha { a on:click="yay()" { "Yay!" } }).into_string(); assert_eq!(s, concat!( r#""#, r#"Yay!"#, @@ -151,14 +145,14 @@ fn classes_shorthand() { #[test] fn hyphens_in_class_names() { - let s = html!(p.rocks-these.are--my--rocks "yes").into_string(); + let s = html!(p.rocks-these.are--my--rocks { "yes" }).into_string(); assert_eq!(s, r#"

yes

"#); } #[test] fn toggle_classes() { fn test(is_cupcake: bool, is_muffin: bool) -> Markup { - html!(p.cupcake[is_cupcake].muffin[is_muffin] "Testing!") + html!(p.cupcake[is_cupcake].muffin[is_muffin] { "Testing!" }) } assert_eq!(test(true, true).into_string(), r#"

Testing!

"#); assert_eq!(test(false, true).into_string(), r#"

Testing!

"#); @@ -169,14 +163,14 @@ fn toggle_classes() { #[test] fn toggle_classes_braces() { struct Maud { rocks: bool } - let s = html!(p.rocks[Maud { rocks: true }.rocks] "Awesome!").into_string(); + let s = html!(p.rocks[Maud { rocks: true }.rocks] { "Awesome!" }).into_string(); assert_eq!(s, r#"

Awesome!

"#); } #[test] fn mixed_classes() { fn test(is_muffin: bool) -> Markup { - html!(p.cupcake.muffin[is_muffin].lamington "Testing!") + html!(p.cupcake.muffin[is_muffin].lamington { "Testing!" }) } assert_eq!(test(true).into_string(), r#"

Testing!

"#); assert_eq!(test(false).into_string(), r#"

Testing!

"#); diff --git a/maud/tests/control_structures.rs b/maud/tests/control_structures.rs index 028737d3..e27164e7 100644 --- a/maud/tests/control_structures.rs +++ b/maud/tests/control_structures.rs @@ -44,8 +44,10 @@ fn if_let() { fn while_expr() { let mut numbers = (0..3).into_iter().peekable(); let s = html! { - ul @while numbers.peek().is_some() { - li (numbers.next().unwrap()) + ul { + @while numbers.peek().is_some() { + li { (numbers.next().unwrap()) } + } } }.into_string(); assert_eq!(s, "
  • 0
  • 1
  • 2
"); @@ -56,8 +58,10 @@ fn while_let_expr() { let mut numbers = (0..3).into_iter(); #[cfg_attr(feature = "cargo-clippy", allow(while_let_on_iterator))] let s = html! { - ul @while let Some(n) = numbers.next() { - li (n) + ul { + @while let Some(n) = numbers.next() { + li { (n) } + } } }.into_string(); assert_eq!(s, "
  • 0
  • 1
  • 2
"); @@ -67,8 +71,10 @@ fn while_let_expr() { fn for_expr() { let ponies = ["Apple Bloom", "Scootaloo", "Sweetie Belle"]; let s = html! { - ul @for pony in &ponies { - li (pony) + ul { + @for pony in &ponies { + li { (pony) } + } } }.into_string(); assert_eq!(s, concat!( @@ -85,7 +91,7 @@ fn match_expr() { let s = html! { @match input { Some(value) => { - div (value) + div { (value) } }, None => { "oh noes" @@ -102,7 +108,7 @@ fn match_expr_without_delims() { let s = html! { @match input { Some(value) => (value), - None => span "oh noes", + None => span { "oh noes" }, } }.into_string(); assert_eq!(s, output); diff --git a/maud_extras/lib.rs b/maud_extras/lib.rs index 00cf8922..777a852d 100644 --- a/maud_extras/lib.rs +++ b/maud_extras/lib.rs @@ -112,7 +112,7 @@ pub struct Title>(pub T); impl> Render for Title { fn render(&self) -> Markup { html! { - title (self.0.as_ref()) + title { (self.0.as_ref()) } } } } diff --git a/maud_macros/src/parse.rs b/maud_macros/src/parse.rs index ef3c2605..9b76c2d1 100644 --- a/maud_macros/src/parse.rs +++ b/maud_macros/src/parse.rs @@ -422,11 +422,18 @@ impl Parser { _ => { match self.markup()? { ast::Markup::Block(block) => ast::ElementBody::Block { block }, - markup => ast::ElementBody::Block { - block: ast::Block { - markups: vec![markup], - outer_span: Span::call_site(), - }, + markup => { + let markup_span = markup.span(); + markup_span + .error("element body must be wrapped in braces") + .help("see https://github.com/lfairy/maud/pull/137 for details") + .emit(); + ast::ElementBody::Block { + block: ast::Block { + markups: vec![markup], + outer_span: markup_span, + }, + } }, } },