|
@@ -1,25 +1,55 @@
|
|
use std::fmt::{Display, Formatter};
|
|
use std::fmt::{Display, Formatter};
|
|
|
|
|
|
|
|
+use crate::errors::missing_trailing_comma;
|
|
|
|
+
|
|
|
|
+use self::util::try_parse_braces;
|
|
|
|
+
|
|
use super::*;
|
|
use super::*;
|
|
|
|
|
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
|
use quote::quote;
|
|
use quote::quote;
|
|
use syn::{
|
|
use syn::{
|
|
- parse::ParseBuffer, punctuated::Punctuated, spanned::Spanned, token::Brace, Expr, Ident,
|
|
|
|
- LitStr, Token,
|
|
|
|
|
|
+ ext::IdentExt, punctuated::Punctuated, spanned::Spanned, token::Brace, Expr, Ident, LitStr,
|
|
|
|
+ Token,
|
|
};
|
|
};
|
|
|
|
|
|
// =======================================
|
|
// =======================================
|
|
// Parse the VNode::Element type
|
|
// Parse the VNode::Element type
|
|
// =======================================
|
|
// =======================================
|
|
-#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
|
|
|
|
|
+#[derive(Clone, Debug)]
|
|
pub struct Element {
|
|
pub struct Element {
|
|
pub name: ElementName,
|
|
pub name: ElementName,
|
|
pub key: Option<IfmtInput>,
|
|
pub key: Option<IfmtInput>,
|
|
pub attributes: Vec<AttributeType>,
|
|
pub attributes: Vec<AttributeType>,
|
|
pub merged_attributes: Vec<AttributeType>,
|
|
pub merged_attributes: Vec<AttributeType>,
|
|
pub children: Vec<BodyNode>,
|
|
pub children: Vec<BodyNode>,
|
|
- pub brace: syn::token::Brace,
|
|
|
|
|
|
+ pub brace: Option<syn::token::Brace>,
|
|
|
|
+ // Non-fatal errors that occurred during parsing
|
|
|
|
+ errors: Vec<syn::Error>,
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl PartialEq for Element {
|
|
|
|
+ fn eq(&self, other: &Self) -> bool {
|
|
|
|
+ self.name == other.name
|
|
|
|
+ && self.key == other.key
|
|
|
|
+ && self.attributes == other.attributes
|
|
|
|
+ && self.merged_attributes == other.merged_attributes
|
|
|
|
+ && self.children == other.children
|
|
|
|
+ && self.brace == other.brace
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl Eq for Element {}
|
|
|
|
+
|
|
|
|
+impl Hash for Element {
|
|
|
|
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
|
|
+ self.name.hash(state);
|
|
|
|
+ self.key.hash(state);
|
|
|
|
+ self.attributes.hash(state);
|
|
|
|
+ self.merged_attributes.hash(state);
|
|
|
|
+ self.children.hash(state);
|
|
|
|
+ self.brace.hash(state);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
impl Element {
|
|
impl Element {
|
|
@@ -58,22 +88,61 @@ impl Element {
|
|
attributes,
|
|
attributes,
|
|
merged_attributes,
|
|
merged_attributes,
|
|
children,
|
|
children,
|
|
- brace,
|
|
|
|
|
|
+ brace: Some(brace),
|
|
|
|
+ errors: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
-}
|
|
|
|
|
|
|
|
-impl Parse for Element {
|
|
|
|
- fn parse(stream: ParseStream) -> Result<Self> {
|
|
|
|
|
|
+ /// Create a new incomplete element that has not been fully typed yet
|
|
|
|
+ fn incomplete(name: ElementName) -> Self {
|
|
|
|
+ Self {
|
|
|
|
+ errors: vec![syn::Error::new(
|
|
|
|
+ name.span(),
|
|
|
|
+ format!("Missing braces after element name `{}`", name),
|
|
|
|
+ )],
|
|
|
|
+ name,
|
|
|
|
+ key: None,
|
|
|
|
+ attributes: Vec::new(),
|
|
|
|
+ merged_attributes: Vec::new(),
|
|
|
|
+ children: Vec::new(),
|
|
|
|
+ brace: None,
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pub(crate) fn parse_with_options(
|
|
|
|
+ stream: ParseStream,
|
|
|
|
+ partial_completions: bool,
|
|
|
|
+ ) -> Result<Self> {
|
|
|
|
+ fn peek_any_ident(input: ParseStream) -> bool {
|
|
|
|
+ input.peek(Ident::peek_any)
|
|
|
|
+ && !input.peek(Token![for])
|
|
|
|
+ && !input.peek(Token![if])
|
|
|
|
+ && !input.peek(Token![match])
|
|
|
|
+ }
|
|
|
|
+
|
|
let el_name = ElementName::parse(stream)?;
|
|
let el_name = ElementName::parse(stream)?;
|
|
|
|
|
|
// parse the guts
|
|
// parse the guts
|
|
- let content: ParseBuffer;
|
|
|
|
- let brace = syn::braced!(content in stream);
|
|
|
|
|
|
+ let Ok((brace, content)) = try_parse_braces(stream) else {
|
|
|
|
+ // If there are no braces, this is an incomplete element. We still parse it so that we can autocomplete it, but we don't need to parse the children
|
|
|
|
+ return Ok(Self::incomplete(el_name));
|
|
|
|
+ };
|
|
|
|
|
|
let mut attributes: Vec<AttributeType> = vec![];
|
|
let mut attributes: Vec<AttributeType> = vec![];
|
|
let mut children: Vec<BodyNode> = vec![];
|
|
let mut children: Vec<BodyNode> = vec![];
|
|
let mut key = None;
|
|
let mut key = None;
|
|
|
|
+ let mut errors = Vec::new();
|
|
|
|
+
|
|
|
|
+ macro_rules! accumulate_or_return_error {
|
|
|
|
+ ($error:expr) => {
|
|
|
|
+ let error = $error;
|
|
|
|
+ if partial_completions {
|
|
|
|
+ errors.push(error);
|
|
|
|
+ } else {
|
|
|
|
+ return Err(error);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
|
|
// parse fields with commas
|
|
// parse fields with commas
|
|
// break when we don't get this pattern anymore
|
|
// break when we don't get this pattern anymore
|
|
@@ -92,7 +161,7 @@ impl Parse for Element {
|
|
}
|
|
}
|
|
|
|
|
|
if content.parse::<Token![,]>().is_err() {
|
|
if content.parse::<Token![,]>().is_err() {
|
|
- missing_trailing_comma!(span);
|
|
|
|
|
|
+ accumulate_or_return_error!(missing_trailing_comma(span));
|
|
}
|
|
}
|
|
continue;
|
|
continue;
|
|
}
|
|
}
|
|
@@ -106,28 +175,30 @@ impl Parse for Element {
|
|
content.parse::<Token![:]>()?;
|
|
content.parse::<Token![:]>()?;
|
|
|
|
|
|
let value = content.parse::<ElementAttrValue>()?;
|
|
let value = content.parse::<ElementAttrValue>()?;
|
|
|
|
+ let followed_by_comma = content.parse::<Token![,]>().is_ok();
|
|
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
|
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
|
el_name: el_name.clone(),
|
|
el_name: el_name.clone(),
|
|
attr: ElementAttr {
|
|
attr: ElementAttr {
|
|
name: ElementAttrName::Custom(name),
|
|
name: ElementAttrName::Custom(name),
|
|
value,
|
|
value,
|
|
},
|
|
},
|
|
|
|
+ followed_by_comma,
|
|
}));
|
|
}));
|
|
|
|
|
|
if content.is_empty() {
|
|
if content.is_empty() {
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
- if content.parse::<Token![,]>().is_err() {
|
|
|
|
- missing_trailing_comma!(ident.span());
|
|
|
|
|
|
+ if !followed_by_comma {
|
|
|
|
+ accumulate_or_return_error!(missing_trailing_comma(ident.span()));
|
|
}
|
|
}
|
|
continue;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
// Parse
|
|
// Parse
|
|
// abc: 123,
|
|
// abc: 123,
|
|
- if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
|
|
|
|
- let name = content.parse::<Ident>()?;
|
|
|
|
|
|
+ if peek_any_ident(&content) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
|
|
|
|
+ let name = Ident::parse_any(&content)?;
|
|
|
|
|
|
let name_str = name.to_string();
|
|
let name_str = name.to_string();
|
|
content.parse::<Token![:]>()?;
|
|
content.parse::<Token![:]>()?;
|
|
@@ -136,36 +207,7 @@ impl Parse for Element {
|
|
// for example the `hi` part of `class: "hi"`.
|
|
// for example the `hi` part of `class: "hi"`.
|
|
let span = content.span();
|
|
let span = content.span();
|
|
|
|
|
|
- if name_str.starts_with("on") {
|
|
|
|
- // check for any duplicate event listeners
|
|
|
|
- if attributes.iter().any(|f| {
|
|
|
|
- if let AttributeType::Named(ElementAttrNamed {
|
|
|
|
- attr:
|
|
|
|
- ElementAttr {
|
|
|
|
- name: ElementAttrName::BuiltIn(n),
|
|
|
|
- value: ElementAttrValue::EventTokens(_),
|
|
|
|
- },
|
|
|
|
- ..
|
|
|
|
- }) = f
|
|
|
|
- {
|
|
|
|
- n == &name_str
|
|
|
|
- } else {
|
|
|
|
- false
|
|
|
|
- }
|
|
|
|
- }) {
|
|
|
|
- return Err(syn::Error::new(
|
|
|
|
- name.span(),
|
|
|
|
- format!("Duplicate event listener `{}`", name),
|
|
|
|
- ));
|
|
|
|
- }
|
|
|
|
- attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
|
|
|
- el_name: el_name.clone(),
|
|
|
|
- attr: ElementAttr {
|
|
|
|
- name: ElementAttrName::BuiltIn(name),
|
|
|
|
- value: ElementAttrValue::EventTokens(content.parse()?),
|
|
|
|
- },
|
|
|
|
- }));
|
|
|
|
- } else if name_str == "key" {
|
|
|
|
|
|
+ if name_str == "key" {
|
|
let _key: IfmtInput = content.parse()?;
|
|
let _key: IfmtInput = content.parse()?;
|
|
|
|
|
|
if _key.is_static() {
|
|
if _key.is_static() {
|
|
@@ -174,13 +216,39 @@ impl Parse for Element {
|
|
|
|
|
|
key = Some(_key);
|
|
key = Some(_key);
|
|
} else {
|
|
} else {
|
|
- let value = content.parse::<ElementAttrValue>()?;
|
|
|
|
|
|
+ let value = if name_str.starts_with("on") {
|
|
|
|
+ // check for any duplicate event listeners
|
|
|
|
+ if attributes.iter().any(|f| {
|
|
|
|
+ if let AttributeType::Named(ElementAttrNamed {
|
|
|
|
+ attr:
|
|
|
|
+ ElementAttr {
|
|
|
|
+ name: ElementAttrName::BuiltIn(n),
|
|
|
|
+ value: ElementAttrValue::EventTokens(_),
|
|
|
|
+ },
|
|
|
|
+ ..
|
|
|
|
+ }) = f
|
|
|
|
+ {
|
|
|
|
+ n == &name_str
|
|
|
|
+ } else {
|
|
|
|
+ false
|
|
|
|
+ }
|
|
|
|
+ }) {
|
|
|
|
+ return Err(syn::Error::new(
|
|
|
|
+ name.span(),
|
|
|
|
+ format!("Duplicate event listener `{}`", name),
|
|
|
|
+ ));
|
|
|
|
+ }
|
|
|
|
+ ElementAttrValue::EventTokens(content.parse()?)
|
|
|
|
+ } else {
|
|
|
|
+ content.parse::<ElementAttrValue>()?
|
|
|
|
+ };
|
|
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
|
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
|
el_name: el_name.clone(),
|
|
el_name: el_name.clone(),
|
|
attr: ElementAttr {
|
|
attr: ElementAttr {
|
|
- name: ElementAttrName::BuiltIn(name),
|
|
|
|
|
|
+ name: ElementAttrName::built_in(&name),
|
|
value,
|
|
value,
|
|
},
|
|
},
|
|
|
|
+ followed_by_comma: content.peek(Token![,]),
|
|
}));
|
|
}));
|
|
}
|
|
}
|
|
|
|
|
|
@@ -189,18 +257,18 @@ impl Parse for Element {
|
|
}
|
|
}
|
|
|
|
|
|
if content.parse::<Token![,]>().is_err() {
|
|
if content.parse::<Token![,]>().is_err() {
|
|
- missing_trailing_comma!(span);
|
|
|
|
|
|
+ accumulate_or_return_error!(missing_trailing_comma(span));
|
|
}
|
|
}
|
|
continue;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
// Parse shorthand fields
|
|
// Parse shorthand fields
|
|
- if content.peek(Ident)
|
|
|
|
|
|
+ if peek_any_ident(&content)
|
|
&& !content.peek2(Brace)
|
|
&& !content.peek2(Brace)
|
|
&& !content.peek2(Token![:])
|
|
&& !content.peek2(Token![:])
|
|
&& !content.peek2(Token![-])
|
|
&& !content.peek2(Token![-])
|
|
{
|
|
{
|
|
- let name = content.parse::<Ident>()?;
|
|
|
|
|
|
+ let name = Ident::parse_any(&content)?;
|
|
let name_ = name.clone();
|
|
let name_ = name.clone();
|
|
|
|
|
|
// If the shorthand field is children, these are actually children!
|
|
// If the shorthand field is children, these are actually children!
|
|
@@ -216,21 +284,37 @@ Like so:
|
|
));
|
|
));
|
|
};
|
|
};
|
|
|
|
|
|
- let value = ElementAttrValue::Shorthand(name.clone());
|
|
|
|
|
|
+ let followed_by_comma = content.parse::<Token![,]>().is_ok();
|
|
|
|
+
|
|
|
|
+ // If the shorthand field starts with a capital letter and it isn't followed by a comma, it's actually the start of typing a component
|
|
|
|
+ let starts_with_capital = match name.to_string().chars().next() {
|
|
|
|
+ Some(c) => c.is_uppercase(),
|
|
|
|
+ None => false,
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ if starts_with_capital && !followed_by_comma {
|
|
|
|
+ children.push(BodyNode::Component(Component::incomplete(name.into())));
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Otherwise, it is really a shorthand field
|
|
|
|
+ let value = ElementAttrValue::shorthand(&name);
|
|
|
|
+
|
|
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
|
attributes.push(attribute::AttributeType::Named(ElementAttrNamed {
|
|
el_name: el_name.clone(),
|
|
el_name: el_name.clone(),
|
|
attr: ElementAttr {
|
|
attr: ElementAttr {
|
|
- name: ElementAttrName::BuiltIn(name),
|
|
|
|
|
|
+ name: ElementAttrName::built_in(&name),
|
|
value,
|
|
value,
|
|
},
|
|
},
|
|
|
|
+ followed_by_comma,
|
|
}));
|
|
}));
|
|
|
|
|
|
if content.is_empty() {
|
|
if content.is_empty() {
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
- if content.parse::<Token![,]>().is_err() {
|
|
|
|
- missing_trailing_comma!(name_.span());
|
|
|
|
|
|
+ if !followed_by_comma {
|
|
|
|
+ accumulate_or_return_error!(missing_trailing_comma(name_.span()));
|
|
}
|
|
}
|
|
continue;
|
|
continue;
|
|
}
|
|
}
|
|
@@ -239,15 +323,13 @@ Like so:
|
|
}
|
|
}
|
|
|
|
|
|
while !content.is_empty() {
|
|
while !content.is_empty() {
|
|
- if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
|
|
|
- attr_after_element!(content.span());
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
|
|
|
|
|
+ if ((content.peek(Ident) || content.peek(LitStr)) && content.peek2(Token![:]))
|
|
|
|
+ && !content.peek3(Token![:])
|
|
|
|
+ {
|
|
attr_after_element!(content.span());
|
|
attr_after_element!(content.span());
|
|
}
|
|
}
|
|
|
|
|
|
- children.push(content.parse::<BodyNode>()?);
|
|
|
|
|
|
+ children.push(BodyNode::parse_with_options(&content, partial_completions)?);
|
|
// consume comma if it exists
|
|
// consume comma if it exists
|
|
// we don't actually care if there *are* commas after elements/text
|
|
// we don't actually care if there *are* commas after elements/text
|
|
if content.peek(Token![,]) {
|
|
if content.peek(Token![,]) {
|
|
@@ -255,7 +337,53 @@ Like so:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- Ok(Self::new(key, el_name, attributes, children, brace))
|
|
|
|
|
|
+ let mut myself = Self::new(key, el_name, attributes, children, brace);
|
|
|
|
+
|
|
|
|
+ myself.errors = errors;
|
|
|
|
+
|
|
|
|
+ Ok(myself)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// If this element doesn't include braces, the user is probably still typing the element name.
|
|
|
|
+ /// We can add hints for rust analyzer to complete the element name better.
|
|
|
|
+ pub(crate) fn completion_hints(&self) -> TokenStream2 {
|
|
|
|
+ let Element { name, brace, .. } = self;
|
|
|
|
+
|
|
|
|
+ // If there are braces, this is a complete element and we don't need to add any hints
|
|
|
|
+ if brace.is_some() {
|
|
|
|
+ return quote! {};
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Only complete the element name if it's a built in element
|
|
|
|
+ let ElementName::Ident(name) = name else {
|
|
|
|
+ return quote! {};
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ quote! {
|
|
|
|
+ #[allow(dead_code)]
|
|
|
|
+ {
|
|
|
|
+ // Autocomplete as an element
|
|
|
|
+ dioxus_elements::elements::completions::CompleteWithBraces::#name;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// If this element is only partially complete, return the errors that occurred during parsing
|
|
|
|
+ pub(crate) fn errors(&self) -> TokenStream2 {
|
|
|
|
+ let Element { errors, .. } = self;
|
|
|
|
+
|
|
|
|
+ let mut tokens = quote! {};
|
|
|
|
+ for error in errors {
|
|
|
|
+ tokens.append_all(error.to_compile_error());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ tokens
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl Parse for Element {
|
|
|
|
+ fn parse(stream: ParseStream) -> Result<Self> {
|
|
|
|
+ Self::parse_with_options(stream, true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -268,10 +396,17 @@ pub enum ElementName {
|
|
impl ElementName {
|
|
impl ElementName {
|
|
pub(crate) fn tag_name(&self) -> TokenStream2 {
|
|
pub(crate) fn tag_name(&self) -> TokenStream2 {
|
|
match self {
|
|
match self {
|
|
- ElementName::Ident(i) => quote! { dioxus_elements::#i::TAG_NAME },
|
|
|
|
|
|
+ ElementName::Ident(i) => quote! { dioxus_elements::elements::#i::TAG_NAME },
|
|
ElementName::Custom(s) => quote! { #s },
|
|
ElementName::Custom(s) => quote! { #s },
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ pub(crate) fn namespace(&self) -> TokenStream2 {
|
|
|
|
+ match self {
|
|
|
|
+ ElementName::Ident(i) => quote! { dioxus_elements::elements::#i::NAME_SPACE },
|
|
|
|
+ ElementName::Custom(_) => quote! { None },
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
impl ElementName {
|
|
impl ElementName {
|
|
@@ -322,7 +457,7 @@ impl Parse for ElementName {
|
|
impl ToTokens for ElementName {
|
|
impl ToTokens for ElementName {
|
|
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
|
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
|
match self {
|
|
match self {
|
|
- ElementName::Ident(i) => tokens.append_all(quote! { dioxus_elements::#i }),
|
|
|
|
|
|
+ ElementName::Ident(i) => tokens.append_all(quote! { dioxus_elements::elements::#i }),
|
|
ElementName::Custom(s) => tokens.append_all(quote! { #s }),
|
|
ElementName::Custom(s) => tokens.append_all(quote! { #s }),
|
|
}
|
|
}
|
|
}
|
|
}
|