123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620 |
- use crate::innerlude::*;
- use proc_macro2::{Span, TokenStream as TokenStream2};
- use proc_macro2_diagnostics::SpanDiagnosticExt;
- use quote::{quote, ToTokens, TokenStreamExt};
- use std::fmt::{Display, Formatter};
- use syn::{
- parse::{Parse, ParseStream},
- punctuated::Punctuated,
- spanned::Spanned,
- token::Brace,
- Ident, LitStr, Result, Token,
- };
- /// Parse the VNode::Element type
- #[derive(PartialEq, Eq, Clone, Debug)]
- pub struct Element {
- /// div { } -> div
- pub name: ElementName,
- /// The actual attributes that were parsed
- pub raw_attributes: Vec<Attribute>,
- /// The attributes after merging - basically the formatted version of the combined attributes
- /// where possible.
- ///
- /// These are the actual attributes that get rendered out
- pub merged_attributes: Vec<Attribute>,
- /// The `...` spread attributes.
- pub spreads: Vec<Spread>,
- // /// Elements can have multiple, unlike components which can only have one
- // pub spreads: Vec<Spread>,
- /// The children of the element
- pub children: Vec<BodyNode>,
- /// the brace of the `div { }`
- pub brace: Option<Brace>,
- /// A list of diagnostics that were generated during parsing. This element might be a valid rsx_block
- /// but not technically a valid element - these diagnostics tell us what's wrong and then are used
- /// when rendering
- pub diagnostics: Diagnostics,
- }
- impl Parse for Element {
- fn parse(stream: ParseStream) -> Result<Self> {
- let name = stream.parse::<ElementName>()?;
- // We very liberally parse elements - they might not even have a brace!
- // This is designed such that we can throw a compile error but still give autocomplete
- // ... partial completions mean we do some weird parsing to get the right completions
- let mut brace = None;
- let mut block = RsxBlock::default();
- match stream.peek(Brace) {
- // If the element is followed by a brace, it is complete. Parse the body
- true => {
- block = stream.parse::<RsxBlock>()?;
- brace = Some(block.brace);
- }
- // Otherwise, it is incomplete. Add a diagnostic
- false => block.diagnostics.push(
- name.span()
- .error("Elements must be followed by braces")
- .help("Did you forget a brace?"),
- ),
- }
- // Make sure these attributes have an el_name set for completions and Template generation
- for attr in block.attributes.iter_mut() {
- attr.el_name = Some(name.clone());
- }
- // Assemble the new element from the contents of the block
- let mut element = Element {
- brace,
- name: name.clone(),
- raw_attributes: block.attributes,
- children: block.children,
- diagnostics: block.diagnostics,
- spreads: block.spreads.clone(),
- merged_attributes: Vec::new(),
- };
- // And then merge the various attributes together
- // The original raw_attributes are kept for lossless parsing used by hotreload/autofmt
- element.merge_attributes();
- // And then merge the spreads *after* the attributes are merged. This ensures walking the
- // merged attributes in path order stops before we hit the spreads, but spreads are still
- // counted as dynamic attributes
- for spread in block.spreads.iter() {
- element.merged_attributes.push(Attribute {
- name: AttributeName::Spread(spread.dots),
- colon: None,
- value: AttributeValue::AttrExpr(PartialExpr::from_expr(&spread.expr)),
- comma: spread.comma,
- dyn_idx: spread.dyn_idx.clone(),
- el_name: Some(name.clone()),
- });
- }
- Ok(element)
- }
- }
- impl ToTokens for Element {
- fn to_tokens(&self, tokens: &mut TokenStream2) {
- let el = self;
- let el_name = &el.name;
- let ns = |name| match el_name {
- ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
- ElementName::Custom(_) => quote! { None },
- };
- let static_attrs = el
- .merged_attributes
- .iter()
- .map(|attr| {
- // Rendering static attributes requires a bit more work than just a dynamic attrs
- // Early return for dynamic attributes
- let Some((name, value)) = attr.as_static_str_literal() else {
- let id = attr.dyn_idx.get();
- return quote! { dioxus_core::TemplateAttribute::Dynamic { id: #id } };
- };
- let ns = match name {
- AttributeName::BuiltIn(name) => ns(quote!(#name.1)),
- AttributeName::Custom(_) => quote!(None),
- AttributeName::Spread(_) => {
- unreachable!("spread attributes should not be static")
- }
- };
- let name = match (el_name, name) {
- (ElementName::Ident(_), AttributeName::BuiltIn(_)) => {
- quote! { dioxus_elements::#el_name::#name.0 }
- }
- //hmmmm I think we could just totokens this, but the to_string might be inserting quotes
- _ => {
- let as_string = name.to_string();
- quote! { #as_string }
- }
- };
- let value = value.to_static().unwrap();
- quote! {
- dioxus_core::TemplateAttribute::Static {
- name: #name,
- namespace: #ns,
- value: #value,
- }
- }
- })
- .collect::<Vec<_>>();
- // Render either the child
- let children = el.children.iter().map(|c| match c {
- BodyNode::Element(el) => quote! { #el },
- BodyNode::Text(text) if text.is_static() => {
- let text = text.input.to_static().unwrap();
- quote! { dioxus_core::TemplateNode::Text { text: #text } }
- }
- BodyNode::Text(text) => {
- let id = text.dyn_idx.get();
- quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
- }
- BodyNode::ForLoop(floop) => {
- let id = floop.dyn_idx.get();
- quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
- }
- BodyNode::RawExpr(exp) => {
- let id = exp.dyn_idx.get();
- quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
- }
- BodyNode::Component(exp) => {
- let id = exp.dyn_idx.get();
- quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
- }
- BodyNode::IfChain(exp) => {
- let id = exp.dyn_idx.get();
- quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
- }
- });
- let ns = ns(quote!(NAME_SPACE));
- let el_name = el_name.tag_name();
- let diagnostics = &el.diagnostics;
- let completion_hints = &el.completion_hints();
- // todo: generate less code if there's no diagnostics by not including the curlies
- tokens.append_all(quote! {
- {
- #completion_hints
- #diagnostics
- dioxus_core::TemplateNode::Element {
- tag: #el_name,
- namespace: #ns,
- attrs: &[ #(#static_attrs),* ],
- children: &[ #(#children),* ],
- }
- }
- })
- }
- }
- impl Element {
- pub(crate) fn add_merging_non_string_diagnostic(diagnostics: &mut Diagnostics, span: Span) {
- diagnostics.push(span.error("Cannot merge non-fmt literals").help(
- "Only formatted strings can be merged together. If you want to merge literals, you can use a format string.",
- ));
- }
- /// Collapses ifmt attributes into a single dynamic attribute using a space or `;` as a delimiter
- ///
- /// ```ignore,
- /// div {
- /// class: "abc-def",
- /// class: if some_expr { "abc" },
- /// }
- /// ```
- fn merge_attributes(&mut self) {
- let mut attrs: Vec<&Attribute> = vec![];
- for attr in &self.raw_attributes {
- if attrs.iter().any(|old_attr| old_attr.name == attr.name) {
- continue;
- }
- attrs.push(attr);
- }
- for attr in attrs {
- if attr.name.is_likely_key() {
- continue;
- }
- // Collect all the attributes with the same name
- let matching_attrs = self
- .raw_attributes
- .iter()
- .filter(|a| a.name == attr.name)
- .collect::<Vec<_>>();
- // if there's only one attribute with this name, then we don't need to merge anything
- if matching_attrs.len() == 1 {
- self.merged_attributes.push(attr.clone());
- continue;
- }
- // If there are multiple attributes with the same name, then we need to merge them
- // This will be done by creating an ifmt attribute that combines all the segments
- // We might want to throw a diagnostic of trying to merge things together that might not
- // make a whole lot of sense - like merging two exprs together
- let mut out = IfmtInput::new(attr.span());
- for (idx, matching_attr) in matching_attrs.iter().enumerate() {
- // If this is the first attribute, then we don't need to add a delimiter
- if idx != 0 {
- // FIXME: I don't want to special case anything - but our delimiter is special cased to a space
- // We really don't want to special case anything in the macro, but the hope here is that
- // multiline strings can be merged with a space
- out.push_raw_str(" ".to_string());
- }
- // Merge raw literals into the output
- if let AttributeValue::AttrLiteral(HotLiteral::Fmted(lit)) = &matching_attr.value {
- out.push_ifmt(lit.formatted_input.clone());
- continue;
- }
- // Merge `if cond { "abc" } else if ...` into the output
- if let AttributeValue::IfExpr(value) = &matching_attr.value {
- out.push_expr(value.quote_as_string(&mut self.diagnostics));
- continue;
- }
- Self::add_merging_non_string_diagnostic(
- &mut self.diagnostics,
- matching_attr.span(),
- );
- }
- let out_lit = HotLiteral::Fmted(out.into());
- self.merged_attributes.push(Attribute {
- name: attr.name.clone(),
- value: AttributeValue::AttrLiteral(out_lit),
- colon: attr.colon,
- dyn_idx: attr.dyn_idx.clone(),
- comma: matching_attrs.last().unwrap().comma,
- el_name: attr.el_name.clone(),
- });
- }
- }
- pub(crate) fn key(&self) -> Option<&AttributeValue> {
- self.raw_attributes
- .iter()
- .find(|attr| attr.name.is_likely_key())
- .map(|attr| &attr.value)
- }
- fn completion_hints(&self) -> TokenStream2 {
- // If there is already a brace, we don't need any completion hints
- if self.brace.is_some() {
- return quote! {};
- }
- let ElementName::Ident(name) = &self.name else {
- return quote! {};
- };
- quote! {
- {
- #[allow(dead_code)]
- #[doc(hidden)]
- mod __completions {
- fn ignore() {
- super::dioxus_elements::elements::completions::CompleteWithBraces::#name
- }
- }
- }
- }
- }
- }
- #[derive(PartialEq, Eq, Clone, Debug, Hash)]
- pub enum ElementName {
- Ident(Ident),
- Custom(LitStr),
- }
- impl ToTokens for ElementName {
- fn to_tokens(&self, tokens: &mut TokenStream2) {
- match self {
- ElementName::Ident(i) => tokens.append_all(quote! { #i }),
- ElementName::Custom(s) => s.to_tokens(tokens),
- }
- }
- }
- impl Parse for ElementName {
- fn parse(stream: ParseStream) -> Result<Self> {
- let raw =
- Punctuated::<Ident, Token![-]>::parse_separated_nonempty_with(stream, parse_raw_ident)?;
- if raw.len() == 1 {
- Ok(ElementName::Ident(raw.into_iter().next().unwrap()))
- } else {
- let span = raw.span();
- let tag = raw
- .into_iter()
- .map(|ident| ident.to_string())
- .collect::<Vec<_>>()
- .join("-");
- let tag = LitStr::new(&tag, span);
- Ok(ElementName::Custom(tag))
- }
- }
- }
- impl ElementName {
- pub(crate) fn tag_name(&self) -> TokenStream2 {
- match self {
- ElementName::Ident(i) => quote! { dioxus_elements::elements::#i::TAG_NAME },
- ElementName::Custom(s) => quote! { #s },
- }
- }
- pub fn span(&self) -> Span {
- match self {
- ElementName::Ident(i) => i.span(),
- ElementName::Custom(s) => s.span(),
- }
- }
- }
- impl PartialEq<&str> for ElementName {
- fn eq(&self, other: &&str) -> bool {
- match self {
- ElementName::Ident(i) => i == *other,
- ElementName::Custom(s) => s.value() == *other,
- }
- }
- }
- impl Display for ElementName {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- match self {
- ElementName::Ident(i) => write!(f, "{}", i),
- ElementName::Custom(s) => write!(f, "{}", s.value()),
- }
- }
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- use prettier_please::PrettyUnparse;
- #[test]
- fn parses_name() {
- let _parsed: ElementName = syn::parse2(quote::quote! { div }).unwrap();
- let _parsed: ElementName = syn::parse2(quote::quote! { some-cool-element }).unwrap();
- let _parsed: Element = syn::parse2(quote::quote! { div {} }).unwrap();
- let _parsed: Element = syn::parse2(quote::quote! { some-cool-element {} }).unwrap();
- let parsed: Element = syn::parse2(quote::quote! {
- some-cool-div {
- id: "hi",
- id: "hi {abc}",
- id: "hi {def}",
- class: 123,
- something: bool,
- data_attr: "data",
- data_attr: "data2",
- data_attr: "data3",
- exp: { some_expr },
- something: {cool},
- something: bool,
- something: 123,
- onclick: move |_| {
- println!("hello world");
- },
- "some-attr": "hello world",
- onclick: move |_| {},
- class: "hello world",
- id: "my-id",
- data_attr: "data",
- data_attr: "data2",
- data_attr: "data3",
- "somte_attr3": "hello world",
- something: {cool},
- something: bool,
- something: 123,
- onclick: move |_| {
- println!("hello world");
- },
- ..attrs1,
- ..attrs2,
- ..attrs3
- }
- })
- .unwrap();
- dbg!(parsed);
- }
- #[test]
- fn parses_variety() {
- let input = quote::quote! {
- div {
- class: "hello world",
- id: "my-id",
- data_attr: "data",
- data_attr: "data2",
- data_attr: "data3",
- "somte_attr3": "hello world",
- something: {cool},
- something: bool,
- something: 123,
- onclick: move |_| {
- println!("hello world");
- },
- ..attrs,
- ..attrs2,
- ..attrs3
- }
- };
- let parsed: Element = syn::parse2(input).unwrap();
- dbg!(parsed);
- }
- #[test]
- fn to_tokens_properly() {
- let input = quote::quote! {
- div {
- class: "hello world",
- class2: "hello {world}",
- class3: "goodbye {world}",
- class4: "goodbye world",
- "something": "cool {blah}",
- "something2": "cooler",
- div {
- div {
- h1 { class: "h1 col" }
- h2 { class: "h2 col" }
- h3 { class: "h3 col" }
- div {}
- }
- }
- }
- };
- let parsed: Element = syn::parse2(input).unwrap();
- println!("{}", parsed.to_token_stream().pretty_unparse());
- }
- #[test]
- fn to_tokens_with_diagnostic() {
- let input = quote::quote! {
- div {
- class: "hello world",
- id: "my-id",
- ..attrs,
- div {
- ..attrs,
- class: "hello world",
- id: "my-id",
- }
- }
- };
- let parsed: Element = syn::parse2(input).unwrap();
- println!("{}", parsed.to_token_stream().pretty_unparse());
- }
- #[test]
- fn merges_attributes() {
- let input = quote::quote! {
- div {
- class: "hello world",
- class: if count > 3 { "abc {def}" },
- class: if count < 50 { "small" } else { "big" }
- }
- };
- let parsed: Element = syn::parse2(input).unwrap();
- assert_eq!(parsed.diagnostics.len(), 0);
- assert_eq!(parsed.merged_attributes.len(), 1);
- assert_eq!(
- parsed.merged_attributes[0].name.to_string(),
- "class".to_string()
- );
- let attr = &parsed.merged_attributes[0].value;
- println!("{}", attr.to_token_stream().pretty_unparse());
- let _attr = match attr {
- AttributeValue::AttrLiteral(lit) => lit,
- _ => panic!("expected literal"),
- };
- }
- /// There are a number of cases where merging attributes doesn't make sense
- /// - merging two expressions together
- /// - merging two literals together
- /// - merging a literal and an expression together
- ///
- /// etc
- ///
- /// We really only want to merge formatted things together
- ///
- /// IE
- /// class: "hello world ",
- /// class: if some_expr { "abc" }
- ///
- /// Some open questions - should the delimiter be explicit?
- #[test]
- fn merging_weird_fails() {
- let input = quote::quote! {
- div {
- class: "hello world",
- class: if some_expr { 123 },
- style: "color: red;",
- style: "color: blue;",
- width: "1px",
- width: 1,
- width: false,
- contenteditable: true,
- }
- };
- let parsed: Element = syn::parse2(input).unwrap();
- assert_eq!(parsed.merged_attributes.len(), 4);
- assert_eq!(parsed.diagnostics.len(), 3);
- // style should not generate a diagnostic
- assert!(!parsed
- .diagnostics
- .diagnostics
- .into_iter()
- .any(|f| f.emit_as_item_tokens().to_string().contains("style")));
- }
- #[test]
- fn diagnostics() {
- let input = quote::quote! {
- p {
- class: "foo bar"
- "Hello world"
- }
- };
- let _parsed: Element = syn::parse2(input).unwrap();
- }
- #[test]
- fn parses_raw_elements() {
- let input = quote::quote! {
- use {
- "hello"
- }
- };
- let _parsed: Element = syn::parse2(input).unwrap();
- }
- }
|