123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- //! An arbitrary block parser.
- //!
- //! Is meant to parse the contents of a block that is either a component or an element.
- //! We put these together to cut down on code duplication and make the parsers a bit more resilient.
- //!
- //! This involves custom structs for name, attributes, and children, as well as a custom parser for the block itself.
- //! It also bubbles out diagnostics if it can to give better errors.
- use crate::innerlude::*;
- use proc_macro2::Span;
- use proc_macro2_diagnostics::SpanDiagnosticExt;
- use syn::{
- ext::IdentExt,
- parse::{Parse, ParseBuffer, ParseStream},
- spanned::Spanned,
- token::{self, Brace},
- Expr, Ident, LitStr, Token,
- };
- /// An item in the form of
- ///
- /// {
- /// attributes,
- /// ..spreads,
- /// children
- /// }
- ///
- /// Does not make any guarnatees about the contents of the block - this is meant to be verified by the
- /// element/component impls themselves.
- ///
- /// The name of the block is expected to be parsed by the parent parser. It will accept items out of
- /// order if possible and then bubble up diagnostics to the parent. This lets us give better errors
- /// and autocomplete
- #[derive(PartialEq, Eq, Clone, Debug, Default)]
- pub struct RsxBlock {
- pub brace: token::Brace,
- pub attributes: Vec<Attribute>,
- pub spreads: Vec<Spread>,
- pub children: Vec<BodyNode>,
- pub diagnostics: Diagnostics,
- }
- impl Parse for RsxBlock {
- fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
- let content: ParseBuffer;
- let brace = syn::braced!(content in input);
- RsxBlock::parse_inner(&content, brace)
- }
- }
- impl RsxBlock {
- /// Only parse the children of the block - all others will be rejected
- pub fn parse_children(content: &ParseBuffer) -> syn::Result<Self> {
- let mut nodes = vec![];
- let mut diagnostics = Diagnostics::new();
- while !content.is_empty() {
- nodes.push(Self::parse_body_node_with_comma_diagnostics(
- content,
- &mut diagnostics,
- )?);
- }
- Ok(Self {
- children: nodes,
- diagnostics,
- ..Default::default()
- })
- }
- pub fn parse_inner(content: &ParseBuffer, brace: token::Brace) -> syn::Result<Self> {
- let mut items = vec![];
- let mut diagnostics = Diagnostics::new();
- // If we are after attributes, we can try to provide better completions and diagnostics
- // by parsing the following nodes as body nodes if they are ambiguous, we can parse them as body nodes
- let mut after_attributes = false;
- // Lots of manual parsing but it's important to do it all here to give the best diagnostics possible
- // We can do things like lookaheads, peeking, etc. to give better errors and autocomplete
- // We allow parsing in any order but complain if its done out of order.
- // Autofmt will fortunately fix this for us in most cases
- //
- // We do this by parsing the unambiguous cases first and then do some clever lookahead to parse the rest
- while !content.is_empty() {
- // Parse spread attributes
- if content.peek(Token![..]) {
- let dots = content.parse::<Token![..]>()?;
- // in case someone tries to do ...spread which is not valid
- if let Ok(extra) = content.parse::<Token![.]>() {
- diagnostics.push(
- extra
- .span()
- .error("Spread expressions only take two dots - not 3! (..spread)"),
- );
- }
- let expr = content.parse::<Expr>()?;
- let attr = Spread {
- expr,
- dots,
- dyn_idx: DynIdx::default(),
- comma: content.parse().ok(),
- };
- if !content.is_empty() && attr.comma.is_none() {
- diagnostics.push(
- attr.span()
- .error("Attributes must be separated by commas")
- .help("Did you forget a comma?"),
- );
- }
- items.push(RsxItem::Spread(attr));
- after_attributes = true;
- continue;
- }
- // Parse unambiguous attributes - these can't be confused with anything
- if (content.peek(LitStr) || content.peek(Ident) || content.peek(Ident::peek_any))
- && content.peek2(Token![:])
- && !content.peek3(Token![:])
- {
- let attr = content.parse::<Attribute>()?;
- if !content.is_empty() && attr.comma.is_none() {
- diagnostics.push(
- attr.span()
- .error("Attributes must be separated by commas")
- .help("Did you forget a comma?"),
- );
- }
- items.push(RsxItem::Attribute(attr));
- continue;
- }
- // Eagerly match on completed children, generally
- if content.peek(LitStr)
- | content.peek(Token![for])
- | content.peek(Token![if])
- | content.peek(Token![match])
- | content.peek(token::Brace)
- // web components
- | (content.peek(Ident::peek_any) && content.peek2(Token![-]))
- // elements
- | (content.peek(Ident::peek_any) && (after_attributes || content.peek2(token::Brace)))
- // components
- | (content.peek(Ident::peek_any) && (after_attributes || content.peek2(token::Brace) || content.peek2(Token![::])))
- {
- items.push(RsxItem::Child(
- Self::parse_body_node_with_comma_diagnostics(content, &mut diagnostics)?,
- ));
- if !content.is_empty() && content.peek(Token![,]) {
- let comma = content.parse::<Token![,]>()?;
- diagnostics.push(
- comma.span().warning(
- "Elements and text nodes do not need to be separated by commas.",
- ),
- );
- }
- after_attributes = true;
- continue;
- }
- // Parse shorthand attributes
- // todo: this might cause complications with partial expansion... think more about the cases
- // where we can imagine expansion and what better diagnostics we can provide
- if Self::peek_lowercase_ident(&content)
- && !content.peek2(Brace)
- && !content.peek2(Token![:]) // regular attributes / components with generics
- && !content.peek2(Token![-]) // web components
- && !content.peek2(Token![<]) // generics on components
- // generics on components
- && !content.peek2(Token![::])
- {
- let attribute = content.parse::<Attribute>()?;
- if !content.is_empty() && attribute.comma.is_none() {
- diagnostics.push(
- attribute
- .span()
- .error("Attributes must be separated by commas")
- .help("Did you forget a comma?"),
- );
- }
- items.push(RsxItem::Attribute(attribute));
- continue;
- }
- // Finally just attempt a bodynode parse
- items.push(RsxItem::Child(
- Self::parse_body_node_with_comma_diagnostics(content, &mut diagnostics)?,
- ))
- }
- // Validate the order of the items
- RsxBlock::validate(&items, &mut diagnostics);
- // todo: maybe make this a method such that the rsxblock is lossless
- // Decompose into attributes, spreads, and children
- let mut attributes = vec![];
- let mut spreads = vec![];
- let mut children = vec![];
- for item in items {
- match item {
- RsxItem::Attribute(attr) => attributes.push(attr),
- RsxItem::Spread(spread) => spreads.push(spread),
- RsxItem::Child(child) => children.push(child),
- }
- }
- Ok(Self {
- attributes,
- children,
- spreads,
- brace,
- diagnostics,
- })
- }
- // Parse a body node with diagnostics for unnecessary trailing commas
- fn parse_body_node_with_comma_diagnostics(
- content: &ParseBuffer,
- diagnostics: &mut Diagnostics,
- ) -> syn::Result<BodyNode> {
- let body_node = content.parse::<BodyNode>()?;
- if !content.is_empty() && content.peek(Token![,]) {
- let comma = content.parse::<Token![,]>()?;
- diagnostics.push(
- comma
- .span()
- .warning("Elements and text nodes do not need to be separated by commas."),
- );
- }
- Ok(body_node)
- }
- fn peek_lowercase_ident(stream: &ParseStream) -> bool {
- let Ok(ident) = stream.fork().parse::<Ident>() else {
- return false;
- };
- ident
- .to_string()
- .chars()
- .next()
- .unwrap()
- .is_ascii_lowercase()
- }
- /// Ensure the ordering of the items is correct
- /// - Attributes must come before children
- /// - Spreads must come before children
- /// - Spreads must come after attributes
- ///
- /// div {
- /// key: "value",
- /// ..props,
- /// "Hello, world!"
- /// }
- fn validate(items: &[RsxItem], diagnostics: &mut Diagnostics) {
- #[derive(Debug, PartialEq, Eq)]
- enum ValidationState {
- Attributes,
- Spreads,
- Children,
- }
- use ValidationState::*;
- let mut state = ValidationState::Attributes;
- for item in items.iter() {
- match item {
- RsxItem::Attribute(_) => {
- if state == Children || state == Spreads {
- diagnostics.push(
- item.span()
- .error("Attributes must come before children in an element"),
- );
- }
- state = Attributes;
- }
- RsxItem::Spread(_) => {
- if state == Children {
- diagnostics.push(
- item.span()
- .error("Spreads must come before children in an element"),
- );
- }
- state = Spreads;
- }
- RsxItem::Child(_) => {
- state = Children;
- }
- }
- }
- }
- }
- pub enum RsxItem {
- Attribute(Attribute),
- Spread(Spread),
- Child(BodyNode),
- }
- impl RsxItem {
- pub fn span(&self) -> Span {
- match self {
- RsxItem::Attribute(attr) => attr.span(),
- RsxItem::Spread(spread) => spread.dots.span(),
- RsxItem::Child(child) => child.span(),
- }
- }
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- use quote::quote;
- #[test]
- fn basic_cases() {
- let input = quote! {
- { "Hello, world!" }
- };
- let block: RsxBlock = syn::parse2(input).unwrap();
- assert_eq!(block.attributes.len(), 0);
- assert_eq!(block.children.len(), 1);
- let input = quote! {
- {
- key: "value",
- onclick: move |_| {
- "Hello, world!"
- },
- ..spread,
- "Hello, world!"
- }
- };
- let block: RsxBlock = syn::parse2(input).unwrap();
- dbg!(block);
- let complex_element = quote! {
- {
- key: "value",
- onclick2: move |_| {
- "Hello, world!"
- },
- thing: if true { "value" },
- otherthing: if true { "value" } else { "value" },
- onclick: move |_| {
- "Hello, world!"
- },
- ..spread,
- ..spread1
- ..spread2,
- "Hello, world!"
- }
- };
- let _block: RsxBlock = syn::parse2(complex_element).unwrap();
- let complex_component = quote! {
- {
- key: "value",
- onclick2: move |_| {
- "Hello, world!"
- },
- ..spread,
- "Hello, world!"
- }
- };
- let _block: RsxBlock = syn::parse2(complex_component).unwrap();
- }
- /// Some tests of partial expansion to give better autocomplete
- #[test]
- fn partial_cases() {
- let with_hander = quote! {
- {
- onclick: move |_| {
- some.
- }
- }
- };
- let _block: RsxBlock = syn::parse2(with_hander).unwrap();
- }
- /// Ensure the hotreload scoring algorithm works as expected
- #[test]
- fn hr_score() {
- let _block = quote! {
- {
- a: "value {cool}",
- b: "{cool} value",
- b: "{cool} {thing} value",
- b: "{thing} value",
- }
- };
- // loop { accumulate perfect matches }
- // stop when all matches are equally valid
- //
- // Remove new attr one by one as we find its perfect match. If it doesn't have a perfect match, we
- // score it instead.
- quote! {
- // start with
- div {
- div { class: "other {abc} {def} {hij}" } // 1, 1, 1
- div { class: "thing {abc} {def}" } // 1, 1, 1
- // div { class: "thing {abc}" } // 1, 0, 1
- }
- // end with
- div {
- h1 {
- class: "thing {abc}" // 1, 1, MAX
- }
- h1 {
- class: "thing {hij}" // 1, 1, MAX
- }
- // h2 {
- // class: "thing {def}" // 1, 1, 0
- // }
- // h3 {
- // class: "thing {def}" // 1, 1, 0
- // }
- }
- // how about shuffling components, for, if, etc
- Component {
- class: "thing {abc}",
- other: "other {abc} {def}",
- }
- Component {
- class: "thing",
- other: "other",
- }
- Component {
- class: "thing {abc}",
- other: "other",
- }
- Component {
- class: "thing {abc}",
- other: "other {abc} {def}",
- }
- };
- }
- #[test]
- fn kitchen_sink_parse() {
- let input = quote! {
- // Elements
- {
- class: "hello",
- id: "node-{node_id}",
- ..props,
- // Text Nodes
- "Hello, world!"
- // Exprs
- {rsx! { "hi again!" }}
- for item in 0..10 {
- // "Second"
- div { "cool-{item}" }
- }
- Link {
- to: "/home",
- class: "link {is_ready}",
- "Home"
- }
- if false {
- div { "hi again!?" }
- } else if true {
- div { "its cool?" }
- } else {
- div { "not nice !" }
- }
- }
- };
- let _parsed: RsxBlock = syn::parse2(input).unwrap();
- }
- #[test]
- fn simple_comp_syntax() {
- let input = quote! {
- { class: "inline-block mr-4", icons::icon_14 {} }
- };
- let _parsed: RsxBlock = syn::parse2(input).unwrap();
- }
- #[test]
- fn with_sutter() {
- let input = quote! {
- {
- div {}
- d
- div {}
- }
- };
- let _parsed: RsxBlock = syn::parse2(input).unwrap();
- }
- #[test]
- fn looks_like_prop_but_is_expr() {
- let input = quote! {
- {
- a: "asd".to_string(),
- // b can be omitted, and it will be filled with its default value
- c: "asd".to_string(),
- d: Some("asd".to_string()),
- e: Some("asd".to_string()),
- }
- };
- let _parsed: RsxBlock = syn::parse2(input).unwrap();
- }
- #[test]
- fn no_comma_diagnostics() {
- let input = quote! {
- { a, ..ComponentProps { a: 1, b: 2, c: 3, children: VNode::empty(), onclick: Default::default() } }
- };
- let parsed: RsxBlock = syn::parse2(input).unwrap();
- assert!(parsed.diagnostics.is_empty());
- }
- #[test]
- fn proper_attributes() {
- let input = quote! {
- {
- onclick: action,
- href,
- onmounted: onmounted,
- prevent_default,
- class,
- rel,
- target: tag_target,
- aria_current,
- ..attributes,
- {children}
- }
- };
- let parsed: RsxBlock = syn::parse2(input).unwrap();
- dbg!(parsed.attributes);
- }
- #[test]
- fn reserved_attributes() {
- let input = quote! {
- {
- label {
- for: "blah",
- }
- }
- };
- let parsed: RsxBlock = syn::parse2(input).unwrap();
- dbg!(parsed.attributes);
- }
- #[test]
- fn diagnostics_check() {
- let input = quote::quote! {
- {
- class: "foo bar"
- "Hello world"
- }
- };
- let _parsed: RsxBlock = syn::parse2(input).unwrap();
- }
- #[test]
- fn incomplete_components() {
- let input = quote::quote! {
- {
- some::cool::Component
- }
- };
- let _parsed: RsxBlock = syn::parse2(input).unwrap();
- }
- #[test]
- fn incomplete_root_elements() {
- use syn::parse::Parser;
- let input = quote::quote! {
- di
- };
- let parsed = RsxBlock::parse_children.parse2(input).unwrap();
- let children = parsed.children;
- assert_eq!(children.len(), 1);
- if let BodyNode::Element(parsed) = &children[0] {
- assert_eq!(
- parsed.name,
- ElementName::Ident(Ident::new("di", Span::call_site()))
- );
- } else {
- panic!("expected element, got {:?}", children);
- }
- assert!(parsed.diagnostics.is_empty());
- }
- }
|