From 5a71ca797abebdabb5e4ea89cce7165d00e2a0ac Mon Sep 17 00:00:00 2001 From: Vladimir Motylenko Date: Sun, 21 May 2023 13:45:53 +0300 Subject: [PATCH] feat: RSX parser with recovery after errors, and unquoted text (#1054) * Feat: Upgrade to new local version of syn-rsx * chore: Make macro more IDE friendly 1. Add quotation to RawText node. 2. Replace vec! macro with [].to_vec(). Cons: 1. Temporary remove allow(unused_braces) from expressions, to allow completion after dot in rust-analyzer. * chore: Change dependency from syn-rsx to rstml * chore: Fix value_to_string usage, pr comments, and fmt. --- leptos_hot_reload/Cargo.toml | 4 +- leptos_hot_reload/src/lib.rs | 2 +- leptos_hot_reload/src/node.rs | 36 +- leptos_hot_reload/src/parsing.rs | 38 ++- leptos_macro/Cargo.toml | 8 +- leptos_macro/src/component.rs | 37 ++- leptos_macro/src/lib.rs | 34 +- leptos_macro/src/slot.rs | 16 +- leptos_macro/src/template.rs | 137 +++++--- leptos_macro/src/view.rs | 307 ++++++++---------- leptos_macro/tests/ui/component.stderr | 2 +- .../tests/ui/component_absolute.stderr | 2 +- router/src/components/link.rs | 8 +- server_fn/Cargo.toml | 2 +- server_fn/server_fn_macro_default/Cargo.toml | 2 +- server_fn_macro/Cargo.toml | 2 +- 16 files changed, 346 insertions(+), 291 deletions(-) diff --git a/leptos_hot_reload/Cargo.toml b/leptos_hot_reload/Cargo.toml index 6ff80d0b7..3cf6e283a 100644 --- a/leptos_hot_reload/Cargo.toml +++ b/leptos_hot_reload/Cargo.toml @@ -11,7 +11,7 @@ readme = "../README.md" [dependencies] anyhow = "1" serde = { version = "1", features = ["derive"] } -syn = { version = "1", features = [ +syn = { version = "2", features = [ "full", "parsing", "extra-traits", @@ -19,7 +19,7 @@ syn = { version = "1", features = [ "printing", ] } quote = "1" -syn-rsx = "0.9" +rstml = "0.10.6" proc-macro2 = { version = "1", features = ["span-locations", "nightly"] } parking_lot = "0.12" walkdir = "2" diff --git a/leptos_hot_reload/src/lib.rs b/leptos_hot_reload/src/lib.rs index e0329ba09..30dd1fe1e 100644 --- a/leptos_hot_reload/src/lib.rs +++ b/leptos_hot_reload/src/lib.rs @@ -76,7 +76,7 @@ impl ViewMacros { tokens.next(); // , // TODO handle class = ... let rsx = - syn_rsx::parse2(tokens.collect::())?; + rstml::parse2(tokens.collect::())?; let template = LNode::parse_view(rsx)?; views.push(MacroInvocation { id, template }) } diff --git a/leptos_hot_reload/src/node.rs b/leptos_hot_reload/src/node.rs index 217c5e4f8..f28258c6a 100644 --- a/leptos_hot_reload/src/node.rs +++ b/leptos_hot_reload/src/node.rs @@ -1,8 +1,8 @@ -use crate::parsing::{is_component_node, value_to_string}; +use crate::parsing::is_component_node; use anyhow::Result; -use quote::quote; +use quote::ToTokens; +use rstml::node::{Node, NodeAttribute}; use serde::{Deserialize, Serialize}; -use syn_rsx::Node; // A lightweight virtual DOM structure we can use to hold // the state of a Leptos view macro template. This is because @@ -58,36 +58,30 @@ impl LNode { } } Node::Text(text) => { - if let Some(value) = value_to_string(&text.value) { - views.push(LNode::Text(value)); - } else { - let value = text.value.as_ref(); - let code = quote! { #value }; - let code = code.to_string(); - views.push(LNode::DynChild(code)); - } + views.push(LNode::Text(text.value_string())); } Node::Block(block) => { - let value = block.value.as_ref(); - let code = quote! { #value }; + let code = block.into_token_stream(); let code = code.to_string(); views.push(LNode::DynChild(code)); } Node::Element(el) => { if is_component_node(&el) { + let name = el.name().to_string(); let mut children = Vec::new(); for child in el.children { LNode::parse_node(child, &mut children)?; } views.push(LNode::Component { - name: el.name.to_string(), + name: name, props: el + .open_tag .attributes .into_iter() .filter_map(|attr| match attr { - Node::Attribute(attr) => Some(( + NodeAttribute::Attribute(attr) => Some(( attr.key.to_string(), - format!("{:#?}", attr.value), + format!("{:#?}", attr.value()), )), _ => None, }) @@ -95,15 +89,13 @@ impl LNode { children, }); } else { - let name = el.name.to_string(); + let name = el.name().to_string(); let mut attrs = Vec::new(); - for attr in el.attributes { - if let Node::Attribute(attr) = attr { + for attr in el.open_tag.attributes { + if let NodeAttribute::Attribute(attr) = attr { let name = attr.key.to_string(); - if let Some(value) = - attr.value.as_ref().and_then(value_to_string) - { + if let Some(value) = attr.value_literal_string() { attrs.push(( name, LAttributeValue::Static(value), diff --git a/leptos_hot_reload/src/parsing.rs b/leptos_hot_reload/src/parsing.rs index 07963ce3b..da095cc11 100644 --- a/leptos_hot_reload/src/parsing.rs +++ b/leptos_hot_reload/src/parsing.rs @@ -1,7 +1,37 @@ -use syn_rsx::{NodeElement, NodeValueExpr}; +use rstml::node::NodeElement; -pub fn value_to_string(value: &NodeValueExpr) -> Option { - match &value.as_ref() { +/// +/// Converts `syn::Block` to simple expression +/// +/// For example: +/// ```no_build +/// // "string literal" in +/// {"string literal"} +/// // number literal +/// {0x12} +/// // boolean literal +/// {true} +/// // variable +/// {path::x} +/// ``` +pub fn block_to_primitive_expression(block: &syn::Block) -> Option<&syn::Expr> { + // its empty block, or block with multi lines + if block.stmts.len() != 1 { + return None; + } + match &block.stmts[0] { + syn::Stmt::Expr(e, None) => return Some(&e), + _ => {} + } + None +} + +/// Converts simple literals to its string representation. +/// +/// This function doesn't convert literal wrapped inside block +/// like: `{"string"}`. +pub fn value_to_string(value: &syn::Expr) -> Option { + match &value { syn::Expr::Lit(lit) => match &lit.lit { syn::Lit::Str(s) => Some(s.value()), syn::Lit::Char(c) => Some(c.value().to_string()), @@ -14,7 +44,7 @@ pub fn value_to_string(value: &NodeValueExpr) -> Option { } pub fn is_component_node(node: &NodeElement) -> bool { - node.name + node.name() .to_string() .starts_with(|c: char| c.is_ascii_uppercase()) } diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 3e725f704..35ac31ac9 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -12,16 +12,16 @@ readme = "../README.md" proc-macro = true [dependencies] -attribute-derive = { version = "0.5", features = ["syn-full"] } +attribute-derive = { version = "0.6", features = ["syn-full"] } cfg-if = "1" html-escape = "0.2" itertools = "0.10" -prettyplease = "0.1" +prettyplease = "0.2.4" proc-macro-error = "1" proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full"] } -syn-rsx = "0.9" +syn = { version = "2", features = ["full"] } +rstml = "0.10.6" leptos_hot_reload = { workspace = true } server_fn_macro = { workspace = true } convert_case = "0.6.0" diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs index 6b1da2ac9..1e37f48fe 100644 --- a/leptos_macro/src/component.rs +++ b/leptos_macro/src/component.rs @@ -4,15 +4,15 @@ use convert_case::{ Casing, }; use itertools::Itertools; +use leptos_hot_reload::parsing::value_to_string; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt}; use syn::{ parse::Parse, parse_quote, spanned::Spanned, AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item, - ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat, PatIdent, Path, - PathArguments, ReturnType, Stmt, Type, TypePath, Visibility, + ItemFn, LitStr, Meta, Pat, PatIdent, Path, PathArguments, ReturnType, Stmt, + Type, TypePath, Visibility, }; - pub struct Model { is_transparent: bool, docs: Docs, @@ -56,14 +56,17 @@ impl Parse for Model { // We need to remove the `#[doc = ""]` and `#[builder(_)]` // attrs from the function signature - drain_filter(&mut item.attrs, |attr| { - attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop) + drain_filter(&mut item.attrs, |attr| match &attr.meta { + Meta::NameValue(attr) => attr.path == parse_quote!(doc), + Meta::List(attr) => attr.path == parse_quote!(prop), + _ => false, }); item.sig.inputs.iter_mut().for_each(|arg| { if let FnArg::Typed(ty) = arg { - drain_filter(&mut ty.attrs, |attr| { - attr.path == parse_quote!(doc) - || attr.path == parse_quote!(prop) + drain_filter(&mut ty.attrs, |attr| match &attr.meta { + Meta::NameValue(attr) => attr.path == parse_quote!(doc), + Meta::List(attr) => attr.path == parse_quote!(prop), + _ => false, }); } }); @@ -400,12 +403,20 @@ impl Docs { let mut attrs = attrs .iter() - .filter_map(|attr| attr.path.is_ident("doc").then(|| { - let Ok(Meta::NameValue(MetaNameValue { lit: Lit::Str(doc), .. })) = attr.parse_meta() else { - abort!(attr, "expected doc comment to be string literal"); + .filter_map(|attr| { + let Meta::NameValue(attr ) = &attr.meta else { + return None }; - (doc.value(), doc.span()) - })) + if !attr.path.is_ident("doc") { + return None + } + + let Some(val) = value_to_string(&attr.value) else { + abort!(attr, "expected string literal in value of doc comment"); + }; + + Some((val, attr.path.span())) + }) .flat_map(map) .collect_vec(); diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index ab6bd9f02..fdf5dbd1a 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -7,9 +7,9 @@ extern crate proc_macro_error; use proc_macro::TokenStream; use proc_macro2::{Span, TokenTree}; use quote::ToTokens; +use rstml::{node::KeyedAttribute, parse}; use server_fn_macro::{server_macro_impl, ServerContext}; use syn::parse_macro_input; -use syn_rsx::{parse, NodeAttribute}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum Mode { @@ -351,16 +351,22 @@ pub fn view(tokens: TokenStream) -> TokenStream { .chain(tokens) .collect() }; - - match parse(tokens.into()) { - Ok(nodes) => render_view( - &proc_macro2::Ident::new(&cx.to_string(), cx.span()), - &nodes, - Mode::default(), - global_class.as_ref(), - normalized_call_site(proc_macro::Span::call_site()), - ), - Err(error) => error.to_compile_error(), + let config = rstml::ParserConfig::default().recover_block(true); + let parser = rstml::Parser::new(config); + let (nodes, errors) = parser.parse_recoverable(tokens).split_vec(); + let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens()); + let nodes_output = render_view( + &cx, + &nodes, + Mode::default(), + global_class.as_ref(), + normalized_call_site(proc_macro::Span::call_site()), + ); + quote! { + { + #(#errors;)* + #nodes_output + } } .into() } @@ -874,9 +880,9 @@ pub fn params_derive( } } -pub(crate) fn attribute_value(attr: &NodeAttribute) -> &syn::Expr { - match &attr.value { - Some(value) => value.as_ref(), +pub(crate) fn attribute_value(attr: &KeyedAttribute) -> &syn::Expr { + match &attr.possible_value { + Some(value) => &value.value, None => abort!(attr.key, "attribute should have value"), } } diff --git a/leptos_macro/src/slot.rs b/leptos_macro/src/slot.rs index d7a6771de..d149d9946 100644 --- a/leptos_macro/src/slot.rs +++ b/leptos_macro/src/slot.rs @@ -5,7 +5,8 @@ use attribute_derive::Attribute as AttributeDerive; use proc_macro2::{Ident, TokenStream}; use quote::{ToTokens, TokenStreamExt}; use syn::{ - parse::Parse, parse_quote, Field, ItemStruct, LitStr, Type, Visibility, + parse::Parse, parse_quote, Field, ItemStruct, LitStr, Meta, Type, + Visibility, }; pub struct Model { @@ -31,13 +32,16 @@ impl Parse for Model { // We need to remove the `#[doc = ""]` and `#[builder(_)]` // attrs from the function signature - drain_filter(&mut item.attrs, |attr| { - attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop) + drain_filter(&mut item.attrs, |attr| match &attr.meta { + Meta::NameValue(attr) => attr.path == parse_quote!(doc), + Meta::List(attr) => attr.path == parse_quote!(prop), + _ => false, }); item.fields.iter_mut().for_each(|arg| { - drain_filter(&mut arg.attrs, |attr| { - attr.path == parse_quote!(doc) - || attr.path == parse_quote!(prop) + drain_filter(&mut arg.attrs, |attr| match &attr.meta { + Meta::NameValue(attr) => attr.path == parse_quote!(doc), + Meta::List(attr) => attr.path == parse_quote!(prop), + _ => false, }); }); diff --git a/leptos_macro/src/template.rs b/leptos_macro/src/template.rs index 77dc88ad4..6b35a2382 100644 --- a/leptos_macro/src/template.rs +++ b/leptos_macro/src/template.rs @@ -1,9 +1,14 @@ use crate::attribute_value; -use leptos_hot_reload::parsing::is_component_node; +use itertools::Either; +use leptos_hot_reload::parsing::{ + block_to_primitive_expression, is_component_node, value_to_string, +}; use proc_macro2::{Ident, Span, TokenStream}; -use quote::{quote, quote_spanned}; +use quote::{quote, quote_spanned, ToTokens}; +use rstml::node::{ + KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, +}; use syn::spanned::Spanned; -use syn_rsx::{Node, NodeAttribute, NodeElement, NodeValueExpr}; use uuid::Uuid; pub(crate) fn render_template(cx: &Ident, nodes: &[Node]) -> TokenStream { @@ -53,7 +58,7 @@ fn root_element_to_tokens( .unwrap(); }; - let span = node.name.span(); + let span = node.name().span(); let navigations = if navigations.is_empty() { quote! {} @@ -67,7 +72,7 @@ fn root_element_to_tokens( quote! { #(#expressions;);* } }; - let tag_name = node.name.to_string(); + let tag_name = node.name().to_string(); quote_spanned! { span => { @@ -104,9 +109,9 @@ enum PrevSibChange { Skip, } -fn attributes(node: &NodeElement) -> impl Iterator { - node.attributes.iter().filter_map(|node| { - if let Node::Attribute(attribute) = node { +fn attributes(node: &NodeElement) -> impl Iterator { + node.attributes().iter().filter_map(|node| { + if let NodeAttribute::Attribute(attribute) = node { Some(attribute) } else { None @@ -129,11 +134,11 @@ fn element_to_tokens( ) -> Ident { // create this element *next_el_id += 1; - let this_el_ident = child_ident(*next_el_id, node.name.span()); + let this_el_ident = child_ident(*next_el_id, node.name().span()); // Open tag - let name_str = node.name.to_string(); - let span = node.name.span(); + let name_str = node.name().to_string(); + let span = node.name().span(); // CSR/hydrate, push to template template.push('<'); @@ -145,7 +150,7 @@ fn element_to_tokens( } // navigation for this el - let debug_name = node.name.to_string(); + let debug_name = node.name().to_string(); let this_nav = if is_root_el { quote_spanned! { span => let #this_el_ident = #debug_name; @@ -247,14 +252,17 @@ fn next_sibling_node( if is_component_node(sibling) { next_sibling_node(children, idx + 1, next_el_id) } else { - Ok(Some(child_ident(*next_el_id + 1, sibling.name.span()))) + Ok(Some(child_ident( + *next_el_id + 1, + sibling.name().span(), + ))) } } Node::Block(sibling) => { - Ok(Some(child_ident(*next_el_id + 1, sibling.value.span()))) + Ok(Some(child_ident(*next_el_id + 1, sibling.span()))) } Node::Text(sibling) => { - Ok(Some(child_ident(*next_el_id + 1, sibling.value.span()))) + Ok(Some(child_ident(*next_el_id + 1, sibling.span()))) } _ => Err("expected either an element or a block".to_string()), } @@ -263,7 +271,7 @@ fn next_sibling_node( fn attr_to_tokens( cx: &Ident, - node: &NodeAttribute, + node: &KeyedAttribute, el_id: &Ident, template: &mut String, expressions: &mut Vec, @@ -272,8 +280,8 @@ fn attr_to_tokens( let name = name.strip_prefix('_').unwrap_or(&name); let name = name.strip_prefix("attr:").unwrap_or(name); - let value = match &node.value { - Some(expr) => match expr.as_ref() { + let value = match &node.value() { + Some(expr) => match expr { syn::Expr::Lit(expr_lit) => { if let syn::Lit::Str(s) = &expr_lit.lit { AttributeValue::Static(s.value()) @@ -367,7 +375,7 @@ fn child_to_tokens( Node::Element(node) => { if is_component_node(node) { proc_macro_error::emit_error!( - node.name.span(), + node.name().span(), "component children not allowed in template!, use view! \ instead" ); @@ -389,7 +397,7 @@ fn child_to_tokens( } Node::Text(node) => block_to_tokens( cx, - &node.value, + Either::Left(node.value_string()), node.value.span(), parent, prev_sib, @@ -399,10 +407,42 @@ fn child_to_tokens( expressions, navigations, ), - Node::Block(node) => block_to_tokens( + Node::RawText(node) => block_to_tokens( cx, - &node.value, - node.value.span(), + Either::Left(node.to_string_best()), + node.span(), + parent, + prev_sib, + next_sib, + next_el_id, + template, + expressions, + navigations, + ), + Node::Block(NodeBlock::ValidBlock(b)) => { + let value = match block_to_primitive_expression(b) + .and_then(value_to_string) + { + Some(v) => Either::Left(v), + None => Either::Right(b.into_token_stream()), + }; + block_to_tokens( + cx, + value, + b.span(), + parent, + prev_sib, + next_sib, + next_el_id, + template, + expressions, + navigations, + ) + } + Node::Block(b @ NodeBlock::Invalid { .. }) => block_to_tokens( + cx, + Either::Right(b.into_token_stream()), + b.span(), parent, prev_sib, next_sib, @@ -418,7 +458,7 @@ fn child_to_tokens( #[allow(clippy::too_many_arguments)] fn block_to_tokens( _cx: &Ident, - value: &NodeValueExpr, + value: Either, span: Span, parent: &Ident, prev_sib: Option, @@ -428,18 +468,6 @@ fn block_to_tokens( expressions: &mut Vec, navigations: &mut Vec, ) -> PrevSibChange { - let value = value.as_ref(); - let str_value = match value { - syn::Expr::Lit(lit) => match &lit.lit { - syn::Lit::Str(s) => Some(s.value()), - syn::Lit::Char(c) => Some(c.value().to_string()), - syn::Lit::Int(i) => Some(i.base10_digits().to_string()), - syn::Lit::Float(f) => Some(f.base10_digits().to_string()), - _ => None, - }, - _ => None, - }; - // code to navigate to this text node let (name, location) = /* if is_first_child && mode == Mode::Client { @@ -473,27 +501,30 @@ fn block_to_tokens( } }; - if let Some(v) = str_value { - navigations.push(location); - template.push_str(&v); + match value { + Either::Left(v) => { + navigations.push(location); + template.push_str(&v); - if let Some(name) = name { - PrevSibChange::Sib(name) - } else { - PrevSibChange::Parent + if let Some(name) = name { + PrevSibChange::Sib(name) + } else { + PrevSibChange::Parent + } } - } else { - template.push_str(""); - navigations.push(location); + Either::Right(value) => { + template.push_str(""); + navigations.push(location); - expressions.push(quote! { - leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx)); - }); + expressions.push(quote! { + leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view(cx)); + }); - if let Some(name) = name { - PrevSibChange::Sib(name) - } else { - PrevSibChange::Parent + if let Some(name) = name { + PrevSibChange::Sib(name) + } else { + PrevSibChange::Parent + } } } } diff --git a/leptos_macro/src/view.rs b/leptos_macro/src/view.rs index 882cdac96..3084328e6 100644 --- a/leptos_macro/src/view.rs +++ b/leptos_macro/src/view.rs @@ -1,11 +1,15 @@ use crate::{attribute_value, Mode}; use convert_case::{Case::Snake, Casing}; -use leptos_hot_reload::parsing::{is_component_node, value_to_string}; +use leptos_hot_reload::parsing::{ + block_to_primitive_expression, is_component_node, value_to_string, +}; use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use quote::{format_ident, quote, quote_spanned}; +use rstml::node::{ + KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName, +}; use std::collections::HashMap; use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit}; -use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr}; #[derive(Clone, Copy)] enum TagType { @@ -213,18 +217,22 @@ fn root_node_to_tokens_ssr( global_class, view_marker, ), - Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {}, + Node::Comment(_) | Node::Doctype(_) => quote! {}, Node::Text(node) => { - let value = node.value.as_ref(); quote! { - leptos::leptos_dom::html::text(#value) + leptos::leptos_dom::html::text(#node) + } + } + Node::RawText(r) => { + let text = r.to_string_best(); + let text = syn::LitStr::new(&text, r.span()); + quote! { + leptos::leptos_dom::html::text(#text) } } Node::Block(node) => { - let value = node.value.as_ref(); quote! { - #[allow(unused_braces)] - #value + #node } } Node::Element(node) => { @@ -254,9 +262,9 @@ fn fragment_to_tokens_ssr( }); quote! { { - leptos::Fragment::lazy(|| vec![ + leptos::Fragment::lazy(|| [ #(#nodes),* - ]) + ].to_vec()) #view_marker } } @@ -329,15 +337,15 @@ fn root_element_to_tokens_ssr( }, }); - let tag_name = node.name.to_string(); + let tag_name = node.name().to_string(); let is_custom_element = is_custom_element(&tag_name); let typed_element_name = if is_custom_element { - Ident::new("Custom", node.name.span()) + Ident::new("Custom", node.name().span()) } else { let camel_cased = camel_case_tag_name( &tag_name.replace("svg::", "").replace("math::", ""), ); - Ident::new(&camel_cased, node.name.span()) + Ident::new(&camel_cased, node.name().span()) }; let typed_element_name = if is_svg_element(&tag_name) { quote! { svg::#typed_element_name } @@ -409,7 +417,7 @@ fn element_to_tokens_ssr( })); } else { let tag_name = node - .name + .name() .to_string() .replace("svg::", "") .replace("math::", ""); @@ -419,8 +427,8 @@ fn element_to_tokens_ssr( let mut inner_html = None; - for attr in &node.attributes { - if let Node::Attribute(attr) = attr { + for attr in node.attributes() { + if let NodeAttribute::Attribute(attr) = attr { inner_html = attribute_to_tokens_ssr( cx, attr, @@ -439,9 +447,9 @@ fn element_to_tokens_ssr( quote! { leptos::leptos_dom::HydrationCtx::id() } }; match node - .attributes + .attributes() .iter() - .find(|node| matches!(node, Node::Attribute(attr) if attr.key.to_string() == "id")) + .find(|node| matches!(node, NodeAttribute::Attribute(attr) if attr.key.to_string() == "id")) { Some(_) => { template.push_str(" leptos-hk=\"_{}\""); @@ -462,7 +470,7 @@ fn element_to_tokens_ssr( if let Some(inner_html) = inner_html { template.push_str("{}"); - let value = inner_html.as_ref(); + let value = inner_html; holes.push(quote! { (#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default() @@ -484,32 +492,23 @@ fn element_to_tokens_ssr( ); } Node::Text(text) => { - if let Some(value) = value_to_string(&text.value) { - let value = if is_script_or_style { - value.into() - } else { - html_escape::encode_safe(&value) - }; - template.push_str( - &value - .replace('{', "\\{") - .replace('}', "\\}"), - ); + let value = text.value_string(); + let value = if is_script_or_style { + value.into() } else { - template.push_str("{}"); - let value = text.value.as_ref(); - - holes.push(quote! { - #value.into_view(#cx).render_to_string(#cx) - }) - } + html_escape::encode_safe(&value) + }; + template.push_str( + &value.replace('{', "\\{").replace('}', "\\}"), + ); } - Node::Block(block) => { - if let Some(value) = value_to_string(&block.value) { + Node::Block(NodeBlock::ValidBlock(block)) => { + if let Some(value) = + block_to_primitive_expression(block) + .and_then(value_to_string) + { template.push_str(&value); } else { - let value = block.value.as_ref(); - if !template.is_empty() { chunks.push(SsrElementChunks::String { template: std::mem::take(template), @@ -517,10 +516,16 @@ fn element_to_tokens_ssr( }) } chunks.push(SsrElementChunks::View(quote! { - {#value}.into_view(#cx) + {#block}.into_view(#cx) })); } } + // Keep invalid blocks for faster IDE diff (on user type) + Node::Block(block @ NodeBlock::Invalid { .. }) => { + chunks.push(SsrElementChunks::View(quote! { + {#block}.into_view(#cx) + })); + } Node::Fragment(_) => abort!( Span::call_site(), "You can't nest a fragment inside an element." @@ -531,7 +536,7 @@ fn element_to_tokens_ssr( } template.push_str("'); } } @@ -540,17 +545,17 @@ fn element_to_tokens_ssr( // returns `inner_html` fn attribute_to_tokens_ssr<'a>( cx: &Ident, - node: &'a NodeAttribute, + attr: &'a KeyedAttribute, template: &mut String, holes: &mut Vec, exprs_for_compiler: &mut Vec, global_class: Option<&TokenTree>, -) -> Option<&'a NodeValueExpr> { - let name = node.key.to_string(); +) -> Option<&'a syn::Expr> { + let name = attr.key.to_string(); if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" { // ignore refs on SSR } else if let Some(name) = name.strip_prefix("on:") { - let handler = attribute_value(node); + let handler = attribute_value(attr); let (event_type, _, _) = parse_event_name(name); exprs_for_compiler.push(quote! { @@ -563,16 +568,16 @@ fn attribute_to_tokens_ssr<'a>( // ignore props for SSR // ignore classes and sdtyles: we'll handle these separately } else if name == "inner_html" { - return node.value.as_ref(); + return attr.value(); } else { let name = name.replacen("attr:", "", 1); // special case of global_class and class attribute if name == "class" && global_class.is_some() - && node.value.as_ref().and_then(value_to_string).is_none() + && attr.value().and_then(value_to_string).is_none() { - let span = node.key.span(); + let span = attr.key.span(); proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \ and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \ toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \ @@ -582,7 +587,7 @@ fn attribute_to_tokens_ssr<'a>( if name != "class" && name != "style" { template.push(' '); - if let Some(value) = node.value.as_ref() { + if let Some(value) = attr.value() { if let Some(value) = value_to_string(value) { template.push_str(&name); template.push_str("=\""); @@ -590,7 +595,6 @@ fn attribute_to_tokens_ssr<'a>( template.push('"'); } else { template.push_str("{}"); - let value = value.as_ref(); holes.push(quote! { &{#value}.into_attribute(#cx) .as_nameless_value_string() @@ -630,11 +634,13 @@ fn set_class_attribute_ssr( Some(val) => (String::new(), Some(val)), }; let static_class_attr = node - .attributes + .attributes() .iter() .filter_map(|a| match a { - Node::Attribute(attr) if attr.key.to_string() == "class" => { - attr.value.as_ref().and_then(value_to_string) + NodeAttribute::Attribute(attr) + if attr.key.to_string() == "class" => + { + attr.value().and_then(value_to_string) } _ => None, }) @@ -644,17 +650,17 @@ fn set_class_attribute_ssr( .join(" "); let dyn_class_attr = node - .attributes + .attributes() .iter() .filter_map(|a| { - if let Node::Attribute(a) = a { + if let NodeAttribute::Attribute(a) = a { if a.key.to_string() == "class" { - if a.value.as_ref().and_then(value_to_string).is_some() + if a.value().and_then(value_to_string).is_some() || fancy_class_name(&a.key.to_string(), cx, a).is_some() { None } else { - Some((a.key.span(), &a.value)) + Some((a.key.span(), a.value())) } } else { None @@ -666,10 +672,10 @@ fn set_class_attribute_ssr( .collect::>(); let class_attrs = node - .attributes + .attributes() .iter() .filter_map(|node| { - if let Node::Attribute(node) = node { + if let NodeAttribute::Attribute(node) = node { let name = node.key.to_string(); if name == "class" { return if let Some((_, name, value)) = @@ -713,7 +719,6 @@ fn set_class_attribute_ssr( for (_span, value) in dyn_class_attr { if let Some(value) = value { template.push_str(" {}"); - let value = value.as_ref(); holes.push(quote! { &(#cx, #value).into_attribute(#cx).as_nameless_value_string() .map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string()) @@ -745,11 +750,13 @@ fn set_style_attribute_ssr( holes: &mut Vec, ) { let static_style_attr = node - .attributes + .attributes() .iter() .filter_map(|a| match a { - Node::Attribute(attr) if attr.key.to_string() == "style" => { - attr.value.as_ref().and_then(value_to_string) + NodeAttribute::Attribute(attr) + if attr.key.to_string() == "style" => + { + attr.value().and_then(value_to_string) } _ => None, }) @@ -757,17 +764,17 @@ fn set_style_attribute_ssr( .map(|style| format!("{style};")); let dyn_style_attr = node - .attributes + .attributes() .iter() .filter_map(|a| { - if let Node::Attribute(a) = a { + if let NodeAttribute::Attribute(a) = a { if a.key.to_string() == "style" { - if a.value.as_ref().and_then(value_to_string).is_some() + if a.value().and_then(value_to_string).is_some() || fancy_style_name(&a.key.to_string(), cx, a).is_some() { None } else { - Some((a.key.span(), &a.value)) + Some((a.key.span(), a.value())) } } else { None @@ -779,10 +786,10 @@ fn set_style_attribute_ssr( .collect::>(); let style_attrs = node - .attributes + .attributes() .iter() .filter_map(|node| { - if let Node::Attribute(node) = node { + if let NodeAttribute::Attribute(node) = node { let name = node.key.to_string(); if name == "style" { return if let Some((_, name, value)) = @@ -825,7 +832,6 @@ fn set_style_attribute_ssr( for (_span, value) in dyn_style_attr { if let Some(value) = value { template.push_str(" {};"); - let value = value.as_ref(); holes.push(quote! { &(#cx, #value).into_attribute(#cx).as_nameless_value_string() .map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string()) @@ -899,18 +905,18 @@ fn fragment_to_tokens( let tokens = if lazy { quote! { { - leptos::Fragment::lazy(|| vec![ + leptos::Fragment::lazy(|| [ #(#nodes),* - ]) + ].to_vec()) #view_marker } } } else { quote! { { - leptos::Fragment::new(vec![ + leptos::Fragment::new([ #(#nodes),* - ]) + ].to_vec()) #view_marker } } @@ -948,18 +954,14 @@ fn node_to_tokens( view_marker, ), Node::Comment(_) | Node::Doctype(_) => Some(quote! {}), - Node::Text(node) => { - let value = node.value.as_ref(); - Some(quote! { - leptos::leptos_dom::html::text(#value) - }) - } - Node::Block(node) => { - let value = node.value.as_ref(); - Some(quote! { #value }) - } - Node::Attribute(node) => { - Some(attribute_to_tokens(cx, node, global_class)) + Node::Text(node) => Some(quote! { + leptos::leptos_dom::html::text(#node) + }), + Node::Block(node) => Some(quote! { #node }), + Node::RawText(r) => { + let text = r.to_string_best(); + let text = syn::LitStr::new(&text, r.span()); + Some(quote! { #text }) } Node::Element(node) => element_to_tokens( cx, @@ -980,6 +982,7 @@ fn element_to_tokens( global_class: Option<&TokenTree>, view_marker: Option, ) -> Option { + let name = node.name(); if is_component_node(node) { if let Some(slot) = get_slot(node) { slot_to_tokens(cx, node, slot, parent_slots, global_class); @@ -988,20 +991,17 @@ fn element_to_tokens( Some(component_to_tokens(cx, node, global_class)) } } else { - let tag = node.name.to_string(); + let tag = name.to_string(); let name = if is_custom_element(&tag) { - let name = node.name.to_string(); + let name = node.name().to_string(); quote! { leptos::leptos_dom::html::custom(#cx, leptos::leptos_dom::html::Custom::new(#name)) } } else if is_svg_element(&tag) { - let name = &node.name; parent_type = TagType::Svg; quote! { leptos::leptos_dom::svg::#name(#cx) } } else if is_math_ml_element(&tag) { - let name = &node.name; parent_type = TagType::Math; quote! { leptos::leptos_dom::math::#name(#cx) } } else if is_ambiguous_element(&tag) { - let name = &node.name; match parent_type { TagType::Unknown => { // We decided this warning was too aggressive, but I'll leave it here in case we want it later @@ -1020,12 +1020,11 @@ fn element_to_tokens( } } } else { - let name = &node.name; parent_type = TagType::Html; quote! { leptos::leptos_dom::html::#name(#cx) } }; - let attrs = node.attributes.iter().filter_map(|node| { - if let Node::Attribute(node) = node { + let attrs = node.attributes().iter().filter_map(|node| { + if let NodeAttribute::Attribute(node) = node { let name = node.key.to_string(); let name = name.trim(); if name.starts_with("class:") @@ -1041,8 +1040,8 @@ fn element_to_tokens( None } }); - let class_attrs = node.attributes.iter().filter_map(|node| { - if let Node::Attribute(node) = node { + let class_attrs = node.attributes().iter().filter_map(|node| { + if let NodeAttribute::Attribute(node) = node { let name = node.key.to_string(); if let Some((fancy, _, _)) = fancy_class_name(&name, cx, node) { Some(fancy) @@ -1055,8 +1054,8 @@ fn element_to_tokens( None } }); - let style_attrs = node.attributes.iter().filter_map(|node| { - if let Node::Attribute(node) = node { + let style_attrs = node.attributes().iter().filter_map(|node| { + if let NodeAttribute::Attribute(node) = node { let name = node.key.to_string(); if let Some((fancy, _, _)) = fancy_style_name(&name, cx, node) { Some(fancy) @@ -1101,32 +1100,18 @@ fn element_to_tokens( }), false, ), - Node::Text(node) => { - if let Some(primitive) = value_to_string(&node.value) { - (quote! { #primitive }, true) - } else { - let value = node.value.as_ref(); - ( - quote! { - #[allow(unused_braces)] #value - }, - false, - ) - } - } - Node::Block(node) => { - if let Some(primitive) = value_to_string(&node.value) { - (quote! { #primitive }, true) - } else { - let value = node.value.as_ref(); - ( - quote! { - #[allow(unused_braces)] #value - }, - false, - ) - } + Node::Text(node) => (quote! { #node }, true), + Node::RawText(node) => { + let text = node.to_string_best(); + let text = syn::LitStr::new(&text, node.span()); + (quote! { #text }, true) } + Node::Block(node) => ( + quote! { + #node + }, + false, + ), Node::Element(node) => ( element_to_tokens( cx, @@ -1139,9 +1124,7 @@ fn element_to_tokens( .unwrap_or_default(), false, ), - Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => { - (quote! {}, false) - } + Node::Comment(_) | Node::Doctype(_) => (quote! {}, false), }; if is_static { quote! { @@ -1172,7 +1155,7 @@ fn element_to_tokens( fn attribute_to_tokens( cx: &Ident, - node: &NodeAttribute, + node: &KeyedAttribute, global_class: Option<&TokenTree>, ) -> TokenStream { let span = node.key.span(); @@ -1303,7 +1286,7 @@ fn attribute_to_tokens( // special case of global_class and class attribute if name == "class" && global_class.is_some() - && node.value.as_ref().and_then(value_to_string).is_none() + && node.value().and_then(value_to_string).is_none() { let span = node.key.span(); proc_macro_error::emit_error!(span, "Combining a global class (view! { cx, class = ... }) \ @@ -1313,10 +1296,8 @@ fn attribute_to_tokens( }; // all other attributes - let value = match node.value.as_ref() { + let value = match node.value() { Some(value) => { - let value = value.as_ref(); - quote! { #value } } None => quote_spanned! { span => "" }, @@ -1367,7 +1348,7 @@ pub(crate) fn parse_event_name(name: &str) -> (TokenStream, bool, bool) { pub(crate) fn slot_to_tokens( cx: &Ident, node: &NodeElement, - slot: &NodeAttribute, + slot: &KeyedAttribute, parent_slots: Option<&mut HashMap>>, global_class: Option<&TokenTree>, ) { @@ -1376,19 +1357,19 @@ pub(crate) fn slot_to_tokens( let name = convert_to_snake_case(if name.starts_with("slot:") { name.replacen("slot:", "", 1) } else { - node.name.to_string() + node.name().to_string() }); - let component_name = ident_from_tag_name(&node.name); - let span = node.name.span(); + let component_name = ident_from_tag_name(node.name()); + let span = node.name().span(); let Some(parent_slots) = parent_slots else { proc_macro_error::emit_error!(span, "slots cannot be used inside HTML elements"); return; }; - let attrs = node.attributes.iter().filter_map(|node| { - if let Node::Attribute(node) = node { + let attrs = node.attributes().iter().filter_map(|node| { + if let NodeAttribute::Attribute(node) = node { if is_slot(node) { None } else { @@ -1406,10 +1387,8 @@ pub(crate) fn slot_to_tokens( let name = &attr.key; let value = attr - .value - .as_ref() + .value() .map(|v| { - let v = v.as_ref(); quote! { #v } }) .unwrap_or_else(|| quote! { #name }); @@ -1474,9 +1453,9 @@ pub(crate) fn slot_to_tokens( let slot = Ident::new(&slot, span); if values.len() > 1 { quote! { - .#slot(vec![ + .#slot([ #(#values)* - ]) + ].to_vec()) } } else { let value = &values[0]; @@ -1504,12 +1483,12 @@ pub(crate) fn component_to_tokens( node: &NodeElement, global_class: Option<&TokenTree>, ) -> TokenStream { - let name = &node.name; - let component_name = ident_from_tag_name(&node.name); - let span = node.name.span(); + let name = node.name(); + let component_name = ident_from_tag_name(node.name()); + let span = node.name().span(); - let attrs = node.attributes.iter().filter_map(|node| { - if let Node::Attribute(node) = node { + let attrs = node.attributes().iter().filter_map(|node| { + if let NodeAttribute::Attribute(node) = node { Some(node) } else { None @@ -1526,10 +1505,8 @@ pub(crate) fn component_to_tokens( let name = &attr.key; let value = attr - .value - .as_ref() + .value() .map(|v| { - let v = v.as_ref(); quote! { #v } }) .unwrap_or_else(|| quote! { #name }); @@ -1637,7 +1614,7 @@ pub(crate) fn component_to_tokens( } pub(crate) fn event_from_attribute_node( - attr: &NodeAttribute, + attr: &KeyedAttribute, force_undelegated: bool, ) -> (TokenStream, &Expr) { let event_name = attr @@ -1697,7 +1674,7 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident { fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> { match expr { syn::Expr::Block(block) => block.block.stmts.last().and_then(|stmt| { - if let syn::Stmt::Expr(expr) = stmt { + if let syn::Stmt::Expr(expr, ..) = stmt { expr_to_ident(expr) } else { None @@ -1708,15 +1685,15 @@ fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> { } } -fn is_slot(node: &NodeAttribute) -> bool { +fn is_slot(node: &KeyedAttribute) -> bool { let key = node.key.to_string(); let key = key.trim(); key == "slot" || key.starts_with("slot:") } -fn get_slot(node: &NodeElement) -> Option<&NodeAttribute> { - node.attributes.iter().find_map(|node| { - if let Node::Attribute(node) = node { +fn get_slot(node: &NodeElement) -> Option<&KeyedAttribute> { + node.attributes().iter().find_map(|node| { + if let NodeAttribute::Attribute(node) = node { if is_slot(node) { Some(node) } else { @@ -1744,7 +1721,7 @@ fn is_self_closing(node: &NodeElement) -> bool { // self-closing tags // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element matches!( - node.name.to_string().as_str(), + node.name().to_string().as_str(), "area" | "base" | "br" @@ -1899,13 +1876,13 @@ fn parse_event(event_name: &str) -> (&str, bool) { fn fancy_class_name<'a>( name: &str, cx: &Ident, - node: &'a NodeAttribute, + node: &'a KeyedAttribute, ) -> Option<(TokenStream, String, &'a Expr)> { // special case for complex class names: // e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)` if name == "class" { - if let Some(expr) = node.value.as_ref() { - if let syn::Expr::Tuple(tuple) = expr.as_ref() { + if let Some(expr) = node.value() { + if let syn::Expr::Tuple(tuple) = expr { if tuple.elems.len() == 2 { let span = node.key.span(); let class = quote_spanned! { @@ -1948,12 +1925,12 @@ fn fancy_class_name<'a>( fn fancy_style_name<'a>( name: &str, cx: &Ident, - node: &'a NodeAttribute, + node: &'a KeyedAttribute, ) -> Option<(TokenStream, String, &'a Expr)> { // special case for complex dynamic style names: if name == "style" { - if let Some(expr) = node.value.as_ref() { - if let syn::Expr::Tuple(tuple) = expr.as_ref() { + if let Some(expr) = node.value() { + if let syn::Expr::Tuple(tuple) = expr { if tuple.elems.len() == 2 { let span = node.key.span(); let style = quote_spanned! { diff --git a/leptos_macro/tests/ui/component.stderr b/leptos_macro/tests/ui/component.stderr index 7ec216b38..0d737db89 100644 --- a/leptos_macro/tests/ui/component.stderr +++ b/leptos_macro/tests/ui/component.stderr @@ -44,7 +44,7 @@ error: unexpected end of input, expected assignment `=` 47 | #[prop(default)] default: bool, | ^ -error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` +error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` = help: try `#[prop(default=5 * 10)]` --> tests/ui/component.rs:56:22 diff --git a/leptos_macro/tests/ui/component_absolute.stderr b/leptos_macro/tests/ui/component_absolute.stderr index f44737765..e7b652c60 100644 --- a/leptos_macro/tests/ui/component_absolute.stderr +++ b/leptos_macro/tests/ui/component_absolute.stderr @@ -44,7 +44,7 @@ error: unexpected end of input, expected assignment `=` 45 | #[prop(default)] default: bool, | ^ -error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` +error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` = help: try `#[prop(default=5 * 10)]` --> tests/ui/component_absolute.rs:54:22 diff --git a/router/src/components/link.rs b/router/src/components/link.rs index 7b1bcb5ab..df4e0dc7e 100644 --- a/router/src/components/link.rs +++ b/router/src/components/link.rs @@ -90,10 +90,14 @@ where children: Children, ) -> HtmlElement { #[cfg(not(any(feature = "hydrate", feature = "csr")))] - _ = state; + { + _ = state; + } #[cfg(not(any(feature = "hydrate", feature = "csr")))] - _ = replace; + { + _ = replace; + } let location = use_location(cx); let is_active = create_memo(cx, move |_| match href.get() { diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 37aa8f242..1ef191233 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -15,7 +15,7 @@ serde_qs = "0.12" thiserror = "1" serde_json = "1" quote = "1" -syn = { version = "1", features = ["full", "parsing", "extra-traits"] } +syn = { version = "2", features = ["full", "parsing", "extra-traits"] } proc-macro2 = "1" ciborium = "0.2" xxhash-rust = { version = "0.8", features = ["const_xxh64"] } diff --git a/server_fn/server_fn_macro_default/Cargo.toml b/server_fn/server_fn_macro_default/Cargo.toml index 3c2478f29..a4e7d4359 100644 --- a/server_fn/server_fn_macro_default/Cargo.toml +++ b/server_fn/server_fn_macro_default/Cargo.toml @@ -11,7 +11,7 @@ description = "The default implementation of the server_fn macro without a conte proc-macro = true [dependencies] -syn = { version = "1", features = ["full"] } +syn = { version = "2", features = ["full"] } server_fn_macro = { workspace = true } [dev-dependencies] diff --git a/server_fn_macro/Cargo.toml b/server_fn_macro/Cargo.toml index 61f112b14..ea732394e 100644 --- a/server_fn_macro/Cargo.toml +++ b/server_fn_macro/Cargo.toml @@ -11,7 +11,7 @@ readme = "../README.md" [dependencies] serde = { version = "1", features = ["derive"] } quote = "1" -syn = { version = "1", features = ["full", "parsing", "extra-traits"] } +syn = { version = "2", features = ["full", "parsing", "extra-traits"] } proc-macro2 = "1" proc-macro-error = "1" xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] }