This commit is contained in:
Greg Johnston 2024-02-10 14:19:09 -05:00
parent 17732a6e6a
commit 0fddfb4823
41 changed files with 1230 additions and 11699 deletions

View file

@ -1,4 +1,4 @@
use leptos::*; use leptos::{component, create_signal, prelude::*, view, IntoView};
/// A simple counter component. /// A simple counter component.
/// ///

View file

@ -9,32 +9,8 @@ description = "DOM operations for the Leptos web framework."
rust-version.workspace = true rust-version.workspace = true
[dependencies] [dependencies]
async-recursion = "1" tachys = { workspace = true }
base64 = { version = "0.22", optional = true } reactive_graph = { workspace = true }
cfg-if = "1"
drain_filter_polyfill = "0.1"
futures = "0.3"
getrandom = { version = "0.2", optional = true }
html-escape = "0.2"
indexmap = "2"
itertools = "0.12"
js-sys = "0.3"
leptos_reactive = { workspace = true }
server_fn = { workspace = true }
once_cell = "1"
pad-adapter = "0.1"
paste = "1"
rand = { version = "0.8", optional = true }
rustc-hash = "1.1.0"
serde_json = "1"
smallvec = "1"
tracing = "0.1"
wasm-bindgen = { version = "0.2", features = ["enable-interning"] }
wasm-bindgen-futures = "0.4.31"
serde = "1"
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies] [dev-dependencies]
leptos = { path = "../leptos" } leptos = { path = "../leptos" }

View file

@ -1,6 +1,18 @@
#![deny(missing_docs)] #![deny(missing_docs)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
pub use tachys::*;
use web_sys::HtmlElement;
pub fn mount_to<F, N>(parent: HtmlElement, f: F)
where
F: FnOnce() -> N + 'static,
N: IntoView,
{
mount_to_with_stop_hydrating(parent, true, f)
}
/*#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))] #![cfg_attr(feature = "nightly", feature(unboxed_closures))]
// to prevent warnings from popping up when a nightly feature is stabilized // to prevent warnings from popping up when a nightly feature is stabilized
#![allow(stable_features)] #![allow(stable_features)]
@ -1311,4 +1323,4 @@ cfg_if! {
std::backtrace::Backtrace std::backtrace::Backtrace
} }
} }
} }*/

View file

@ -6,6 +6,7 @@ use convert_case::{
use itertools::Itertools; use itertools::Itertools;
use leptos_hot_reload::parsing::value_to_string; use leptos_hot_reload::parsing::value_to_string;
use proc_macro2::{Ident, Span, TokenStream}; use proc_macro2::{Ident, Span, TokenStream};
use proc_macro_error::abort;
use quote::{format_ident, quote, quote_spanned, ToTokens, TokenStreamExt}; use quote::{format_ident, quote, quote_spanned, ToTokens, TokenStreamExt};
use syn::{ use syn::{
parse::Parse, parse_quote, spanned::Spanned, token::Colon, parse::Parse, parse_quote, spanned::Spanned, token::Colon,
@ -16,7 +17,6 @@ use syn::{
}; };
pub struct Model { pub struct Model {
is_transparent: bool,
is_island: bool, is_island: bool,
docs: Docs, docs: Docs,
vis: Visibility, vis: Visibility,
@ -58,17 +58,7 @@ impl Parse for Model {
} }
}); });
// Make sure return type is correct
if !is_valid_into_view_return_type(&item.sig.output) {
abort!(
item.sig,
"return type is incorrect";
help = "return signature must be `-> impl IntoView`"
);
}
Ok(Self { Ok(Self {
is_transparent: false,
is_island: false, is_island: false,
docs, docs,
vis: item.vis.clone(), vis: item.vis.clone(),
@ -108,7 +98,6 @@ pub fn convert_from_snake_case(name: &Ident) -> Ident {
impl ToTokens for Model { impl ToTokens for Model {
fn to_tokens(&self, tokens: &mut TokenStream) { fn to_tokens(&self, tokens: &mut TokenStream) {
let Self { let Self {
is_transparent,
is_island, is_island,
docs, docs,
vis, vis,
@ -121,7 +110,6 @@ impl ToTokens for Model {
let no_props = props.is_empty(); let no_props = props.is_empty();
// check for components that end ; // check for components that end ;
if !is_transparent {
let ends_semi = let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt { body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(), Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
@ -131,14 +119,14 @@ impl ToTokens for Model {
proc_macro_error::emit_error!( proc_macro_error::emit_error!(
semi.span(), semi.span(),
"A component that ends with a `view!` macro followed by a \ "A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually \ semicolon will return (), an empty view. This is usually an \
an accident, not intentional, so we prevent it. If youd \ accident, not intentional, so we prevent it. If youd like \
like to return (), you can do it it explicitly by \ to return (), you can do it it explicitly by returning () as \
returning () as the last item from the component." the last item from the component."
); );
} }
}
//body.sig.ident = format_ident!("__{}", body.sig.ident);
#[allow(clippy::redundant_clone)] // false positive #[allow(clippy::redundant_clone)] // false positive
let body_name = body.sig.ident.clone(); let body_name = body.sig.ident.clone();
@ -204,21 +192,21 @@ impl ToTokens for Model {
#[allow(clippy::let_with_type_underscore)] #[allow(clippy::let_with_type_underscore)]
#[cfg_attr( #[cfg_attr(
any(debug_assertions, feature="ssr"), any(debug_assertions, feature="ssr"),
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all) ::leptos::tracing::instrument(level = "info", name = #trace_name, skip_all)
)] )]
}, },
quote! { quote! {
let span = ::leptos::leptos_dom::tracing::Span::current(); let span = ::leptos::tracing::Span::current();
}, },
quote! { quote! {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let _guard = span.entered(); let _guard = span.entered();
}, },
if no_props || !cfg!(feature = "trace-component-props") { if no_props {
quote! {} quote! {}
} else { } else {
quote! { quote! {
::leptos::leptos_dom::tracing_props![#prop_names]; ::leptos::tracing_props![#prop_names];
} }
}, },
) )
@ -248,7 +236,7 @@ impl ToTokens for Model {
let body_name = unmodified_fn_name_from_fn_name(&body_name); let body_name = unmodified_fn_name_from_fn_name(&body_name);
let body_expr = if *is_island { let body_expr = if *is_island {
quote! { quote! {
::leptos::SharedContext::with_hydration(move || { ::leptos::reactive_graph::Owner::with_hydration(move || {
#body_name(#prop_names) #body_name(#prop_names)
}) })
} }
@ -258,32 +246,25 @@ impl ToTokens for Model {
} }
}; };
let component = if *is_transparent { let component = quote! {
body_expr ::leptos::reactive_graph::untrack(
} else {
quote! {
::leptos::leptos_dom::Component::new(
::std::stringify!(#name),
move || { move || {
#tracing_guard_expr #tracing_guard_expr
#tracing_props_expr #tracing_props_expr
#body_expr #body_expr
} }
) )
}
}; };
// add island wrapper if island // add island wrapper if island
let component = if *is_island { let component = if *is_island {
quote! { quote! {
{ {
::leptos::leptos_dom::html::custom( ::leptos::tachys::html::islands::Island::new(
::leptos::leptos_dom::html::Custom::new("leptos-island"), #component_id,
#component
) )
.attr("data-component", #component_id) // #island_serialized_props
.attr("data-hkc", ::leptos::leptos_dom::HydrationCtx::peek_always().to_string())
#island_serialized_props
.child(#component)
} }
} }
} else { } else {
@ -301,20 +282,10 @@ impl ToTokens for Model {
let destructure_props = if no_props { let destructure_props = if no_props {
quote! {} quote! {}
} else { } else {
let wrapped_children = if is_island_with_children let wrapped_children = if is_island_with_children {
&& cfg!(feature = "ssr")
{
quote! { quote! {
let children = ::std::boxed::Box::new(|| ::leptos::Fragment::lazy(|| ::std::vec![ use leptos::tachys::view::any_view::IntoAny;
::leptos::SharedContext::with_hydration(move || { let children = Box::new(|| ::leptos::tachys::html::islands::IslandChildren::new(children()).into_any());
::leptos::IntoView::into_view(
::leptos::leptos_dom::html::custom(
::leptos::leptos_dom::html::Custom::new("leptos-children"),
)
.child(::leptos::SharedContext::no_hydration(children))
)
})
]));
} }
} else { } else {
quote! {} quote! {}
@ -328,24 +299,6 @@ impl ToTokens for Model {
} }
}; };
let into_view = if no_props {
quote! {
impl #impl_generics ::leptos::IntoView for #props_name #generics #where_clause {
fn into_view(self) -> ::leptos::View {
#name().into_view()
}
}
}
} else {
quote! {
impl #impl_generics ::leptos::IntoView for #props_name #generics #where_clause {
fn into_view(self) -> ::leptos::View {
#name(self).into_view()
}
}
}
};
let count = props let count = props
.iter() .iter()
.filter( .filter(
@ -412,7 +365,7 @@ impl ToTokens for Model {
#component #component
}; };
let binding = if *is_island && cfg!(feature = "hydrate") { let binding = if *is_island {
let island_props = if is_island_with_children let island_props = if is_island_with_children
|| is_island_with_other_props || is_island_with_other_props
{ {
@ -453,15 +406,20 @@ impl ToTokens for Model {
}; };
let children = if is_island_with_children { let children = if is_island_with_children {
quote! { quote! {
.children(::std::boxed::Box::new(move || ::leptos::Fragment::lazy(|| ::std::vec![ .children({Box::new(|| {
::leptos::SharedContext::with_hydration(move || { use leptos::tachys::view::any_view::IntoAny;
::leptos::IntoView::into_view( ::leptos::tachys::html::islands::IslandChildren::new(
::leptos::leptos_dom::html::custom( // TODO owner restoration for context
::leptos::leptos_dom::html::Custom::new("leptos-children"), ()
) ).into_any()})})
.prop("$$owner", ::leptos::Owner::current().map(|n| n.as_ffi())) //.children(children)
) /*.children(Box::new(|| {
})]))) use leptos::tachys::view::any_view::IntoAny;
::leptos::tachys::html::islands::IslandChildren::new(
// TODO owner restoration for context
()
).into_any()
}))*/
} }
} else { } else {
quote! {} quote! {}
@ -477,30 +435,26 @@ impl ToTokens for Model {
} else { } else {
quote! {} quote! {}
}; };
let deserialize_island_props = if is_island_with_other_props { let deserialize_island_props = quote! {}; /*if is_island_with_other_props {
quote! { quote! {
let props = el.dataset().get(::leptos::wasm_bindgen::intern("props")) let props = el.dataset().get("props") // TODO ::leptos::wasm_bindgen::intern("props"))
.and_then(|data| ::leptos::serde_json::from_str::<#props_serialized_name>(&data).ok()) .and_then(|data| ::leptos::serde_json::from_str::<#props_serialized_name>(&data).ok())
.expect("could not deserialize props"); .expect("could not deserialize props");
} }
} else { } else {
quote! {} quote! {}
}; };*/
// TODO
quote! { quote! {
#[::leptos::wasm_bindgen::prelude::wasm_bindgen(wasm_bindgen = ::leptos::wasm_bindgen)] #[::leptos::tachys::wasm_bindgen::prelude::wasm_bindgen(wasm_bindgen = ::leptos::wasm_bindgen)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn #hydrate_fn_name(el: ::leptos::web_sys::HtmlElement) { pub fn #hydrate_fn_name(el: ::leptos::tachys::web_sys::HtmlElement) {
if let Some(Ok(key)) = el.dataset().get(::leptos::wasm_bindgen::intern("hkc")).map(|key| std::str::FromStr::from_str(&key)) {
::leptos::leptos_dom::HydrationCtx::continue_from(key);
}
#deserialize_island_props #deserialize_island_props
_ = ::leptos::run_as_child(move || { let island = #name(#island_props);
::leptos::SharedContext::register_island(&el); let state = island.hydrate_from_position::<true>(&el, ::leptos::tachys::view::Position::Current);
::leptos::leptos_dom::mount_to_with_stop_hydrating(el, false, move || { // TODO better cleanup
#name(#island_props) std::mem::forget(state);
})
});
} }
} }
} else { } else {
@ -520,6 +474,7 @@ impl ToTokens for Model {
#[derive(::leptos::typed_builder_macro::TypedBuilder #props_derive_serialize)] #[derive(::leptos::typed_builder_macro::TypedBuilder #props_derive_serialize)]
//#[builder(doc)] //#[builder(doc)]
#[builder(crate_module_path=::leptos::typed_builder)] #[builder(crate_module_path=::leptos::typed_builder)]
#[allow(non_snake_case)]
#vis struct #props_name #impl_generics #where_clause { #vis struct #props_name #impl_generics #where_clause {
#prop_builder_fields #prop_builder_fields
} }
@ -529,36 +484,21 @@ impl ToTokens for Model {
#[allow(missing_docs)] #[allow(missing_docs)]
#binding #binding
impl #impl_generics ::leptos::Props for #props_name #generics #where_clause { impl #impl_generics ::leptos::component::Props for #props_name #generics #where_clause {
type Builder = #props_builder_name #generics; type Builder = #props_builder_name #generics;
fn builder() -> Self::Builder { fn builder() -> Self::Builder {
#props_name::builder() #props_name::builder()
} }
} }
impl #impl_generics ::leptos::DynAttrs for #props_name #generics #where_clause { // TODO restore dyn attrs
/*impl #impl_generics ::leptos::DynAttrs for #props_name #generics #where_clause {
fn dyn_attrs(mut self, v: Vec<(&'static str, ::leptos::Attribute)>) -> Self { fn dyn_attrs(mut self, v: Vec<(&'static str, ::leptos::Attribute)>) -> Self {
#dyn_attrs_props #dyn_attrs_props
self self
} }
} } */
impl #impl_generics ::leptos::DynBindings for #props_name #generics #where_clause {
fn dyn_bindings<B: Into<::leptos::leptos_dom::html::Binding>>(mut self, bindings: impl std::iter::IntoIterator<Item = B>) -> Self {
for binding in bindings.into_iter() {
let binding: ::leptos::leptos_dom::html::Binding = binding.into();
match binding {
#dyn_binding_props
_ => {}
}
}
self
}
}
#into_view
#docs_and_prop_docs #docs_and_prop_docs
#[allow(non_snake_case, clippy::too_many_arguments)] #[allow(non_snake_case, clippy::too_many_arguments)]
@ -579,15 +519,8 @@ impl ToTokens for Model {
impl Model { impl Model {
#[allow(clippy::wrong_self_convention)] #[allow(clippy::wrong_self_convention)]
pub fn is_transparent(mut self, is_transparent: bool) -> Self { pub fn is_island(mut self, is_island: bool) -> Self {
self.is_transparent = is_transparent; self.is_island = is_island;
self
}
#[allow(clippy::wrong_self_convention)]
pub fn is_island(mut self) -> Self {
self.is_island = true;
self self
} }
@ -1218,16 +1151,6 @@ fn prop_to_doc(
} }
} }
fn is_valid_into_view_return_type(ty: &ReturnType) -> bool {
[
parse_quote!(-> impl IntoView),
parse_quote!(-> impl leptos::IntoView),
parse_quote!(-> impl ::leptos::IntoView),
]
.iter()
.any(|test| ty == test)
}
pub fn unmodified_fn_name_from_fn_name(ident: &Ident) -> Ident { pub fn unmodified_fn_name_from_fn_name(ident: &Ident) -> Ident {
Ident::new(&format!("__{ident}"), ident.span()) Ident::new(&format!("__{ident}"), ident.span())
} }

View file

@ -13,7 +13,7 @@ use component::DummyModel;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::{Span, TokenTree}; use proc_macro2::{Span, TokenTree};
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use rstml::{node::KeyedAttribute, parse}; use rstml::node::KeyedAttribute;
use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility}; use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility};
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -35,7 +35,6 @@ impl Default for Mode {
mod params; mod params;
mod view; mod view;
use crate::component::unmodified_fn_name_from_fn_name; use crate::component::unmodified_fn_name_from_fn_name;
use view::{client_template::render_template, render_view};
mod component; mod component;
mod slice; mod slice;
mod slot; mod slot;
@ -358,12 +357,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
let parser = rstml::Parser::new(config); let parser = rstml::Parser::new(config);
let (nodes, errors) = parser.parse_recoverable(tokens).split_vec(); let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens()); let errors = errors.into_iter().map(|e| e.emit_as_expr_tokens());
let nodes_output = render_view( let nodes_output = view::render_view(&nodes, global_class.as_ref(), None);
&nodes,
Mode::default(),
global_class.as_ref(),
normalized_call_site(proc_macro::Span::call_site()),
);
quote! { quote! {
{ {
#(#errors;)* #(#errors;)*
@ -373,38 +367,6 @@ pub fn view(tokens: TokenStream) -> TokenStream {
.into() .into()
} }
fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
cfg_if::cfg_if! {
if #[cfg(all(debug_assertions, feature = "nightly"))] {
Some(leptos_hot_reload::span_to_stable_id(
site.source_file().path(),
site.start().line()
))
} else {
_ = site;
None
}
}
}
/// An optimized, cached template for client-side rendering. Follows the same
/// syntax as the [view!] macro. In hydration or server-side rendering mode,
/// behaves exactly as the `view` macro. In client-side rendering mode, uses a `<template>`
/// node to efficiently render the element. Should only be used with a single root element.
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn template(tokens: TokenStream) -> TokenStream {
if cfg!(feature = "csr") {
match parse(tokens) {
Ok(nodes) => render_template(&nodes),
Err(error) => error.to_compile_error(),
}
.into()
} else {
view(tokens)
}
}
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`. /// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
/// ///
/// The `#[component]` macro allows you to annotate plain Rust functions as components /// The `#[component]` macro allows you to annotate plain Rust functions as components
@ -585,47 +547,11 @@ pub fn template(tokens: TokenStream) -> TokenStream {
/// ``` /// ```
#[proc_macro_error::proc_macro_error] #[proc_macro_error::proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { pub fn component(
let is_transparent = if !args.is_empty() { _args: proc_macro::TokenStream,
let transparent = parse_macro_input!(args as syn::Ident); s: TokenStream,
) -> TokenStream {
if transparent != "transparent" { component_macro(s, false)
abort!(
transparent,
"only `transparent` is supported";
help = "try `#[component(transparent)]` or `#[component]`"
);
}
true
} else {
false
};
let Ok(mut dummy) = syn::parse::<DummyModel>(s.clone()) else {
return s;
};
let parse_result = syn::parse::<component::Model>(s);
if let (ref mut unexpanded, Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.is_transparent(is_transparent).into_token_stream();
unexpanded.sig.ident =
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
quote! {
#expanded
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#unexpanded
}
} else {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! {
#[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#dummy
}
}
.into()
} }
/// Defines a component as an interactive island when you are using the /// Defines a component as an interactive island when you are using the
@ -702,13 +628,15 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
#[proc_macro_error::proc_macro_error] #[proc_macro_error::proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let Ok(mut dummy) = syn::parse::<DummyModel>(s.clone()) else { component_macro(s, true)
return s; }
};
fn component_macro(s: TokenStream, island: bool) -> TokenStream {
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(s); let parse_result = syn::parse::<component::Model>(s);
if let (ref mut unexpanded, Ok(model)) = (&mut dummy, parse_result) { if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) {
let expanded = model.is_island().into_token_stream(); let expanded = model.is_island(island).into_token_stream();
if !matches!(unexpanded.vis, Visibility::Public(_)) { if !matches!(unexpanded.vis, Visibility::Public(_)) {
unexpanded.vis = Visibility::Public(Pub { unexpanded.vis = Visibility::Public(Pub {
span: unexpanded.vis.span(), span: unexpanded.vis.span(),
@ -718,17 +646,20 @@ pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
unmodified_fn_name_from_fn_name(&unexpanded.sig.ident); unmodified_fn_name_from_fn_name(&unexpanded.sig.ident);
quote! { quote! {
#expanded #expanded
#[doc(hidden)] #[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)] #[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#unexpanded #unexpanded
} }
} else { } else if let Ok(mut dummy) = dummy {
dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident); dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident);
quote! { quote! {
#[doc(hidden)] #[doc(hidden)]
#[allow(non_snake_case, dead_code, clippy::too_many_arguments)] #[allow(non_snake_case, dead_code, clippy::too_many_arguments)]
#dummy #dummy
} }
} else {
quote! {}
} }
.into() .into()
} }
@ -989,13 +920,6 @@ pub fn params_derive(
} }
} }
pub(crate) fn attribute_value(attr: &KeyedAttribute) -> &syn::Expr {
match attr.value() {
Some(value) => value,
None => abort!(attr.key, "attribute should have value"),
}
}
/// Generates a `slice` into a struct with a default getter and setter. /// Generates a `slice` into a struct with a default getter and setter.
/// ///
/// Can be used to access deeply nested fields within a global state object. /// Can be used to access deeply nested fields within a global state object.

View file

@ -1,512 +0,0 @@
use super::{
component_builder::component_to_tokens,
expr_to_ident, fancy_class_name, fancy_style_name,
ide_helper::IdeTagHelper,
is_ambiguous_element, is_custom_element, is_math_ml_element,
is_self_closing, is_svg_element, parse_event_name,
slot_helper::{get_slot, slot_to_tokens},
};
use crate::{attribute_value, view::directive_call_from_attribute_node};
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{quote, quote_spanned};
use rstml::node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName};
use std::collections::HashMap;
use syn::spanned::Spanned;
#[derive(Clone, Copy)]
pub(crate) enum TagType {
Unknown,
Html,
Svg,
Math,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fragment_to_tokens(
nodes: &[Node],
lazy: bool,
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> Option<TokenStream> {
let mut slots = HashMap::new();
let has_slots = parent_slots.is_some();
let original_span = nodes
.first()
.zip(nodes.last())
.and_then(|(first, last)| first.span().join(last.span()))
.unwrap_or_else(Span::call_site);
let mut nodes = nodes
.iter()
.filter_map(|node| {
let span = node.span();
let node = node_to_tokens(
node,
parent_type,
has_slots.then_some(&mut slots),
global_class,
None,
)?;
let node = quote_spanned!(span => { #node });
Some(quote! {
::leptos::IntoView::into_view(#[allow(unused_braces)] #node)
})
})
.peekable();
if nodes.peek().is_none() {
_ = nodes.collect::<Vec<_>>();
if let Some(parent_slots) = parent_slots {
for (slot, mut values) in slots.drain() {
parent_slots
.entry(slot)
.and_modify(|entry| entry.append(&mut values))
.or_insert(values);
}
}
return None;
}
let view_marker = if let Some(marker) = view_marker {
quote! { .with_view_marker(#marker) }
} else {
quote! {}
};
let tokens = if lazy {
quote_spanned! {original_span=>
{
::leptos::Fragment::lazy(|| ::std::vec![
#(#nodes),*
])
#view_marker
}
}
} else {
quote_spanned! {original_span=>
{
::leptos::Fragment::new(::std::vec![
#(#nodes),*
])
#view_marker
}
}
};
if let Some(parent_slots) = parent_slots {
for (slot, mut values) in slots.drain() {
parent_slots
.entry(slot)
.and_modify(|entry| entry.append(&mut values))
.or_insert(values);
}
}
Some(tokens)
}
pub(crate) fn node_to_tokens(
node: &Node,
parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> Option<TokenStream> {
match node {
Node::Fragment(fragment) => fragment_to_tokens(
&fragment.children,
true,
parent_type,
None,
global_class,
view_marker,
),
Node::Comment(_) | Node::Doctype(_) => Some(quote! {}),
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();
if text == "cx," {
proc_macro_error::abort!(
r.span(),
"`cx,` is not used with the `view!` macro in 0.5."
)
}
let text = syn::LitStr::new(&text, r.span());
Some(quote! { #text })
}
Node::Element(node) => element_to_tokens(
node,
parent_type,
parent_slots,
global_class,
view_marker,
),
}
}
pub(crate) fn element_to_tokens(
node: &NodeElement,
mut parent_type: TagType,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> Option<TokenStream> {
let name = node.name();
if is_component_node(node) {
if let Some(slot) = get_slot(node) {
slot_to_tokens(node, slot, parent_slots, global_class);
None
} else {
Some(component_to_tokens(node, global_class))
}
} else {
let tag = name.to_string();
// collect close_tag name to emit semantic information for IDE.
let mut ide_helper_close_tag = IdeTagHelper::new();
let close_tag = node.close_tag.as_ref().map(|c| &c.name);
let name = if is_custom_element(&tag) {
let name = node.name().to_string();
// link custom ident to name span for IDE docs
let custom = Ident::new("custom", node.name().span());
quote! { ::leptos::leptos_dom::html::#custom(::leptos::leptos_dom::html::Custom::new(#name)) }
} else if is_svg_element(&tag) {
parent_type = TagType::Svg;
quote! { ::leptos::leptos_dom::svg::#name() }
} else if is_math_ml_element(&tag) {
parent_type = TagType::Math;
quote! { ::leptos::leptos_dom::math::#name() }
} else if is_ambiguous_element(&tag) {
match parent_type {
TagType::Unknown => {
// We decided this warning was too aggressive, but I'll leave it here in case we want it later
/* proc_macro_error::emit_warning!(name.span(), "The view macro is assuming this is an HTML element, \
but it is ambiguous; if it is an SVG or MathML element, prefix with svg:: or math::"); */
quote! {
::leptos::leptos_dom::html::#name()
}
}
TagType::Html => {
quote! { ::leptos::leptos_dom::html::#name() }
}
TagType::Svg => {
quote! { ::leptos::leptos_dom::svg::#name() }
}
TagType::Math => {
quote! { ::leptos::leptos_dom::math::#name() }
}
}
} else {
parent_type = TagType::Html;
quote! { ::leptos::leptos_dom::html::#name() }
};
if let Some(close_tag) = close_tag {
ide_helper_close_tag.save_tag_completion(close_tag)
}
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:")
|| fancy_class_name(name, node).is_some()
|| name.starts_with("style:")
|| fancy_style_name(name, node).is_some()
{
None
} else {
Some(attribute_to_tokens(node, global_class))
}
} else {
None
}
});
let bindings = node.attributes().iter().filter_map(|node| {
use rstml::node::NodeBlock;
use syn::{Expr, ExprRange, RangeLimits, Stmt};
if let NodeAttribute::Block(NodeBlock::ValidBlock(block)) = node {
match block.stmts.first()? {
Stmt::Expr(
Expr::Range(ExprRange {
start: None,
limits: RangeLimits::HalfOpen(_),
end: Some(end),
..
}),
_,
) => Some(quote! { .bindings(#end) }),
_ => None,
}
} else {
None
}
});
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, node) {
Some(fancy)
} else if name.trim().starts_with("class:") {
Some(attribute_to_tokens(node, global_class))
} else {
None
}
} else {
None
}
});
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, node) {
Some(fancy)
} else if name.trim().starts_with("style:") {
Some(attribute_to_tokens(node, global_class))
} else {
None
}
} else {
None
}
});
let global_class_expr = match global_class {
None => quote! {},
Some(class) => quote! { .classes(#class) },
};
if is_self_closing(node) && !node.children.is_empty() {
proc_macro_error::abort!(
node.name().span(),
format!(
"<{tag}> is a self-closing tag and cannot have children."
)
);
}
let children = node
.children
.iter()
.filter_map(|node| match node {
Node::Fragment(fragment) => Some(
fragment_to_tokens(
&fragment.children,
true,
parent_type,
None,
global_class,
None,
)
.unwrap_or(quote_spanned! {
Span::call_site()=> ::leptos::leptos_dom::Unit
}),
),
Node::Text(node) => Some(quote! { #node }),
Node::RawText(node) => {
let text = node.to_string_best();
let text = syn::LitStr::new(&text, node.span());
Some(quote! { #text })
}
Node::Block(node) => Some(quote! { #node }),
Node::Element(node) => Some(
element_to_tokens(
node,
parent_type,
None,
global_class,
None,
)
.unwrap_or_default(),
),
Node::Comment(_) | Node::Doctype(_) => None,
})
.map(|node| quote!(.child(#node)));
let view_marker = if let Some(marker) = view_marker {
quote! { .with_view_marker(#marker) }
} else {
quote! {}
};
let ide_helper_close_tag = ide_helper_close_tag.into_iter();
let result = quote_spanned! {node.span()=> {
#(#ide_helper_close_tag)*
#name
#(#attrs)*
#(#bindings)*
#(#class_attrs)*
#(#style_attrs)*
#global_class_expr
#(#children)*
#view_marker
}
};
// We need to move "allow" out of "quote_spanned" because it breaks hovering in rust-analyzer
Some(quote!(#[allow(unused_braces)] #result))
}
}
pub(crate) fn attribute_to_tokens(
node: &KeyedAttribute,
global_class: Option<&TokenTree>,
) -> TokenStream {
let span = node.key.span();
let name = node.key.to_string();
if name == "ref" || name == "_ref" || name == "ref_" || name == "node_ref" {
let value = expr_to_ident(attribute_value(node));
let node_ref = quote_spanned! { span=> node_ref };
quote! {
.#node_ref(#value)
}
} else if let Some(name) = name.strip_prefix("use:") {
directive_call_from_attribute_node(node, name)
} else if let Some(name) = name.strip_prefix("on:") {
let handler = attribute_value(node);
let (event_type, is_custom, is_force_undelegated) =
parse_event_name(name);
let event_name_ident = match &node.key {
NodeName::Punctuated(parts) => {
if parts.len() >= 2 {
Some(&parts[1])
} else {
None
}
}
_ => unreachable!(),
};
let undelegated_ident = match &node.key {
NodeName::Punctuated(parts) => parts.last().and_then(|last| {
if last.to_string() == "undelegated" {
Some(last)
} else {
None
}
}),
_ => unreachable!(),
};
let on = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let on = quote_spanned! {
on.span()=> .on
};
let event_type = if is_custom {
event_type
} else if let Some(ev_name) = event_name_ident {
quote_spanned! {
ev_name.span()=> #ev_name
}
} else {
event_type
};
let event_type = if is_force_undelegated {
let undelegated = if let Some(undelegated) = undelegated_ident {
quote_spanned! {
undelegated.span()=> #undelegated
}
} else {
quote! { undelegated }
};
quote! { ::leptos::ev::#undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
};
quote! {
#on(#event_type, #handler)
}
} else if let Some(name) = name.strip_prefix("prop:") {
let value = attribute_value(node);
let prop = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let prop = quote_spanned! {
prop.span()=> .prop
};
quote! {
#prop(#name, #value)
}
} else if let Some(name) = name.strip_prefix("class:") {
let value = attribute_value(node);
let class = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let class = quote_spanned! {
class.span()=> .class
};
quote! {
#class(#name, #value)
}
} else if let Some(name) = name.strip_prefix("style:") {
let value = attribute_value(node);
let style = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let style = quote_spanned! {
style.span()=> .style
};
quote! {
#style(#name, #value)
}
} else {
let name = name.replacen("attr:", "", 1);
if let Some((fancy, _, _)) = fancy_class_name(&name, node) {
return fancy;
}
// special case of global_class and class attribute
if name == "class"
&& global_class.is_some()
&& 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! { 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 \
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
};
// all other attributes
let value = match node.value() {
Some(value) => {
quote! { #value }
}
None => quote_spanned! { span=> "" },
};
let attr = match &node.key {
NodeName::Punctuated(parts) => Some(&parts[0]),
_ => None,
};
let attr = if let Some(attr) = attr {
quote_spanned! {
attr.span()=> .attr
}
} else {
quote! {
.attr
}
};
quote! {
#attr(#name, #value)
}
}
}

View file

@ -1,540 +0,0 @@
use super::{component_builder::component_to_tokens, IdeTagHelper};
use crate::attribute_value;
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, ToTokens};
use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
};
use syn::spanned::Spanned;
pub(crate) fn render_template(nodes: &[Node]) -> TokenStream {
// No reason to make template unique, because its "static" is in inner scope.
let template_uid = Ident::new("__TEMPLATE", Span::call_site());
match nodes.first() {
Some(Node::Element(node)) => {
root_element_to_tokens(&template_uid, node)
}
_ => {
abort!(Span::call_site(), "template! takes a single root element.")
}
}
}
fn root_element_to_tokens(
template_uid: &Ident,
node: &NodeElement,
) -> TokenStream {
let mut template = String::new();
let mut navigations = Vec::new();
let mut stmts_for_ide = IdeTagHelper::new();
let mut expressions = Vec::new();
if is_component_node(node) {
component_to_tokens(node, None)
} else {
element_to_tokens(
node,
&Ident::new("root", Span::call_site()),
None,
&mut 0,
&mut 0,
&mut template,
&mut navigations,
&mut stmts_for_ide,
&mut expressions,
true,
);
// create the root element from which navigations and expressions will begin
let generate_root = quote! {
let root = #template_uid.with(|tpl| tpl.content().clone_node_with_deep(true))
.unwrap()
.first_child()
.unwrap();
};
let tag_name = node.name().to_string();
let stmts_for_ide = stmts_for_ide.into_iter();
quote! {
{
thread_local! {
static #template_uid: ::leptos::web_sys::HtmlTemplateElement = {
let document = ::leptos::document();
let el = document.create_element("template").unwrap();
el.set_inner_html(#template);
::leptos::wasm_bindgen::JsCast::unchecked_into(el)
}
}
#(#stmts_for_ide)*
#generate_root
#(#navigations)*
#(#expressions;)*
::leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
#[cfg(debug_assertions)]
name: #tag_name.into(),
element: ::leptos::wasm_bindgen::JsCast::unchecked_into(root),
#[cfg(debug_assertions)]
view_marker: None
})
}
}
}
}
#[derive(Clone, Debug)]
enum PrevSibChange {
Sib(Ident),
Parent,
Skip,
}
fn attributes(node: &NodeElement) -> impl Iterator<Item = &KeyedAttribute> {
node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(attribute) = node {
Some(attribute)
} else {
None
}
})
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens(
node: &NodeElement,
parent: &Ident,
prev_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
stmts_for_ide: &mut IdeTagHelper,
expressions: &mut Vec<TokenStream>,
is_root_el: bool,
) -> Ident {
// create this element
*next_el_id += 1;
// Use any other span instead of node.name.span(), to avoid missundestanding in IDE helpers.
// same as view::root_element_to_tokens_ssr::typed_element_name
let this_el_ident = child_ident(*next_el_id, Span::call_site());
// Open tag
let name_str = node.name().to_string();
// Span for diagnostic message in case of error in quote_spanned! macro
let span = node.open_tag.span();
// CSR/hydrate, push to template
template.push('<');
template.push_str(&name_str);
// attributes
for attr in attributes(node) {
attr_to_tokens(attr, &this_el_ident, template, expressions);
}
// navigation for this el
let debug_name = node.name().to_string();
let this_nav = if is_root_el {
quote_spanned! {
span=> let #this_el_ident = #debug_name;
let #this_el_ident =
::leptos::wasm_bindgen::JsCast::unchecked_into::<::leptos::web_sys::Node>(
#parent.clone()
);
//debug!("=> got {}", #this_el_ident.node_name());
}
} else if let Some(prev_sib) = &prev_sib {
quote_spanned! {
span=> let #this_el_ident = #debug_name;
//log::debug!("next_sibling ({})", #debug_name);
let #this_el_ident = #prev_sib.next_sibling().unwrap_or_else(|| ::std::panic!("error : {} => {} ", #debug_name, "nextSibling"));
//log::debug!("=> got {}", #this_el_ident.node_name());
}
} else {
quote_spanned! {
span=> let #this_el_ident = #debug_name;
//log::debug!("first_child ({})", #debug_name);
let #this_el_ident = #parent.first_child().unwrap_or_else(|| ::std::panic!("error: {} => {}", #debug_name, "firstChild"));
//log::debug!("=> got {}", #this_el_ident.node_name());
}
};
navigations.push(this_nav);
// emit ide helper info
stmts_for_ide.save_element_completion(node);
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
if matches!(
name_str.as_str(),
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
) {
template.push_str("/>");
return this_el_ident;
} else {
template.push('>');
}
// iterate over children
let mut prev_sib = prev_sib;
for (idx, child) in node.children.iter().enumerate() {
// set next sib (for any insertions)
let next_sib =
match next_sibling_node(&node.children, idx + 1, next_el_id) {
Ok(next_sib) => next_sib,
Err(err) => abort!(span, "{}", err),
};
let curr_id = child_to_tokens(
child,
&this_el_ident,
if idx == 0 { None } else { prev_sib.clone() },
next_sib,
next_el_id,
next_co_id,
template,
navigations,
stmts_for_ide,
expressions,
);
prev_sib = match curr_id {
PrevSibChange::Sib(id) => Some(id),
PrevSibChange::Parent => None,
PrevSibChange::Skip => prev_sib,
};
}
// close tag
template.push_str("</");
template.push_str(&name_str);
template.push('>');
this_el_ident
}
fn next_sibling_node(
children: &[Node],
idx: usize,
next_el_id: &mut usize,
) -> Result<Option<Ident>, String> {
if children.len() <= idx {
Ok(None)
} else {
let sibling = &children[idx];
match sibling {
Node::Element(sibling) => {
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(),
)))
}
}
Node::Block(sibling) => {
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
}
Node::Text(sibling) => {
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
}
_ => Err("expected either an element or a block".to_string()),
}
}
}
fn attr_to_tokens(
node: &KeyedAttribute,
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
) {
let name = node.key.to_string();
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 {
syn::Expr::Lit(expr_lit) => {
if let syn::Lit::Str(s) = &expr_lit.lit {
AttributeValue::Static(s.value())
} else {
AttributeValue::Dynamic(expr)
}
}
_ => AttributeValue::Dynamic(expr),
},
None => AttributeValue::Empty,
};
let span = node.key.span();
// refs
if name == "ref" {
abort!(span, "node_ref not yet supported in template! macro")
}
// Event Handlers
else if name.starts_with("on:") {
let (event_type, handler) =
crate::view::event_from_attribute_node(node, false);
expressions.push(quote! {
::leptos::leptos_dom::add_event_helper(
::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id),
#event_type,
#handler,
);
})
}
// Properties
else if let Some(name) = name.strip_prefix("prop:") {
let value = attribute_value(node);
expressions.push(quote_spanned! {
span=> ::leptos::leptos_dom::property(
::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id),
#name,
::leptos::IntoProperty::into_property(#value),
)
});
}
// Classes
else if let Some(name) = name.strip_prefix("class:") {
let value = attribute_value(node);
expressions.push(quote_spanned! {
span=> ::leptos::leptos_dom::class_helper(
::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id),
#name.into(),
::leptos::IntoClass::into_class(#value),
)
});
}
// Attributes
else {
match value {
AttributeValue::Empty => {
template.push(' ');
template.push_str(name);
}
// Static attributes (i.e., just a literal given as value, not an expression)
// are just set in the template — again, nothing programmatic
AttributeValue::Static(value) => {
template.push(' ');
template.push_str(name);
template.push_str("=\"");
template.push_str(&value);
template.push('"');
}
AttributeValue::Dynamic(value) => {
// For client-side rendering, dynamic attributes don't need to be rendered in the template
// They'll immediately be set synchronously before the cloned template is mounted
expressions.push(quote_spanned! {
span=> ::leptos::leptos_dom::attribute_helper(
::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id),
#name.into(),
::leptos::IntoAttribute::into_attribute(#[allow(unused_braces)] {#value}),
)
});
}
}
}
}
enum AttributeValue<'a> {
Static(String),
Dynamic(&'a syn::Expr),
Empty,
}
#[allow(clippy::too_many_arguments)]
fn child_to_tokens(
node: &Node,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
stmts_for_ide: &mut IdeTagHelper,
expressions: &mut Vec<TokenStream>,
) -> PrevSibChange {
match node {
Node::Element(node) => {
if is_component_node(node) {
proc_macro_error::emit_error!(
node.name().span(),
"component children not allowed in template!, use view! \
instead"
);
PrevSibChange::Skip
} else {
PrevSibChange::Sib(element_to_tokens(
node,
parent,
prev_sib,
next_el_id,
next_co_id,
template,
navigations,
stmts_for_ide,
expressions,
false,
))
}
}
Node::Text(node) => block_to_tokens(
Either::Left(node.value_string()),
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
Node::RawText(node) => block_to_tokens(
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(
value,
b.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
)
}
Node::Block(b @ NodeBlock::Invalid { .. }) => block_to_tokens(
Either::Right(b.into_token_stream()),
b.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
_ => abort!(Span::call_site(), "unexpected child node type"),
}
}
#[allow(clippy::too_many_arguments)]
fn block_to_tokens(
value: Either<String, TokenStream>,
span: Span,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
) -> PrevSibChange {
// code to navigate to this text node
let (name, location) = /* if is_first_child && mode == Mode::Client {
(None, quote! { })
}
else */ {
*next_el_id += 1;
let name = child_ident(*next_el_id, span);
let location = if let Some(sibling) = &prev_sib {
quote_spanned! {
span=> //log::debug!("-> next sibling");
let #name = #sibling.next_sibling().unwrap_or_else(|| ::std::panic!("error : {} => {} ", "{block}", "nextSibling"));
//log::debug!("\tnext sibling = {}", #name.node_name());
}
} else {
quote_spanned! {
span=> //log::debug!("\\|/ first child on {}", #parent.node_name());
let #name = #parent.first_child().unwrap_or_else(|| ::std::panic!("error : {} => {} ", "{block}", "firstChild"));
//log::debug!("\tfirst child = {}", #name.node_name());
}
};
(Some(name), location)
};
let mount_kind = match &next_sib {
Some(child) => {
quote! { ::leptos::leptos_dom::MountKind::Before(&#child.clone()) }
}
None => {
quote! { ::leptos::leptos_dom::MountKind::Append(&#parent) }
}
};
match value {
Either::Left(v) => {
navigations.push(location);
template.push_str(&v);
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
Either::Right(value) => {
template.push_str("<!>");
navigations.push(location);
expressions.push(quote! {
::leptos::leptos_dom::mount_child(#mount_kind, &::leptos::IntoView::into_view(#[allow(unused_braces)] {#value}));
});
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
}
}
fn child_ident(el_id: usize, span: Span) -> Ident {
let id = format!("_el{el_id}");
Ident::new(&id, span)
}

View file

@ -1,13 +1,7 @@
#[cfg(debug_assertions)] use super::{fragment_to_tokens, TagType};
use super::ident_from_tag_name;
use super::{
client_builder::{fragment_to_tokens, TagType},
event_from_attribute_node,
};
use crate::view::directive_call_from_attribute_node;
use proc_macro2::{Ident, TokenStream, TokenTree}; use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned}; use quote::{format_ident, quote, quote_spanned};
use rstml::node::{NodeAttribute, NodeElement}; use rstml::node::{NodeAttribute, NodeElement, NodeName};
use std::collections::HashMap; use std::collections::HashMap;
use syn::spanned::Spanned; use syn::spanned::Spanned;
@ -97,7 +91,8 @@ pub(crate) fn component_to_tokens(
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let events = attrs // TODO events and directives
/* let events = attrs
.clone() .clone()
.filter(|attr| attr.key.to_string().starts_with("on:")) .filter(|attr| attr.key.to_string().starts_with("on:"))
.map(|attr| { .map(|attr| {
@ -120,7 +115,7 @@ pub(crate) fn component_to_tokens(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let events_and_directives = let events_and_directives =
events.into_iter().chain(directives).collect::<Vec<_>>(); events.into_iter().chain(directives).collect::<Vec<_>>(); */
let dyn_attrs = attrs let dyn_attrs = attrs
.filter(|attr| attr.key.to_string().starts_with("attr:")) .filter(|attr| attr.key.to_string().starts_with("attr:"))
@ -130,7 +125,7 @@ pub(crate) fn component_to_tokens(
let value = attr.value().map(|v| { let value = attr.value().map(|v| {
quote! { #v } quote! { #v }
})?; })?;
Some(quote! { (#name, ::leptos::IntoAttribute::into_attribute(#value)) }) Some(quote! { (#name, #value.into_attribute()) })
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -146,7 +141,6 @@ pub(crate) fn component_to_tokens(
} else { } else {
let children = fragment_to_tokens( let children = fragment_to_tokens(
&node.children, &node.children,
true,
TagType::Unknown, TagType::Unknown,
Some(&mut slots), Some(&mut slots),
global_class, global_class,
@ -178,7 +172,7 @@ pub(crate) fn component_to_tokens(
.children({ .children({
#(#clonables)* #(#clonables)*
move |#(#bindables)*| #children #view_marker move |#(#bindables)*| #children
}) })
} }
} else { } else {
@ -186,7 +180,7 @@ pub(crate) fn component_to_tokens(
.children({ .children({
#(#clonables)* #(#clonables)*
::leptos::ToChildren::to_children(move || #children #view_marker) ::leptos::children::ToChildren::to_children(move || #children)
}) })
} }
} }
@ -230,7 +224,7 @@ pub(crate) fn component_to_tokens(
}; };
let component_props_builder = quote_spanned! {name.span()=> let component_props_builder = quote_spanned! {name.span()=>
::leptos::component_props_builder(#name_ref #generics) ::leptos::component::component_props_builder(#name_ref #generics)
}; };
#[allow(unused_mut)] // used in debug #[allow(unused_mut)] // used in debug
@ -243,9 +237,15 @@ pub(crate) fn component_to_tokens(
#(#slots)* #(#slots)*
#children #children
#build #build
#dyn_attrs #dyn_attrs;
#(#spread_bindings)*
#[allow(unreachable_code)]
::leptos::component::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
#name_ref,
props
) )
}
}; };
// (Temporarily?) removed // (Temporarily?) removed
@ -253,12 +253,39 @@ pub(crate) fn component_to_tokens(
/* #[cfg(debug_assertions)] /* #[cfg(debug_assertions)]
IdeTagHelper::add_component_completion(&mut component, node); */ IdeTagHelper::add_component_completion(&mut component, node); */
if events_and_directives.is_empty() { // TODO events and directives
/* if events_and_directives.is_empty() {
component component
} else { } else {
quote_spanned! {node.span()=> quote_spanned! {node.span()=>
::leptos::IntoView::into_view(#[allow(unused_braces)] {#component}) #component.into_view()
#(#events_and_directives)* #(#events_and_directives)*
} }
} */
component
}
#[cfg(debug_assertions)]
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
match tag_name {
NodeName::Path(path) => path
.path
.segments
.iter()
.last()
.map(|segment| segment.ident.clone())
.expect("element needs to have a name"),
NodeName::Block(_) => {
let span = tag_name.span();
proc_macro_error::emit_error!(
span,
"blocks not allowed in tag-name position"
);
Ident::new("", span)
}
_ => Ident::new(
&tag_name.to_string().replace(['-', ':'], "_"),
tag_name.span(),
),
} }
} }

View file

@ -1,151 +0,0 @@
use leptos_hot_reload::parsing::is_component_tag_name;
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use rstml::node::{NodeElement, NodeName};
use syn::spanned::Spanned;
/// Helper type to emit semantic info about tags, for IDE.
/// Implement `IntoIterator` with `Item="let _ = foo::docs;"`.
///
/// `IdeTagHelper` uses warning instead of errors everywhere,
/// it's aim is to add usability, not introduce additional typecheck in `view`/`template` code.
/// On stable `emit_warning` don't produce anything.
pub(crate) struct IdeTagHelper(Vec<TokenStream>);
// TODO: Unhandled cases:
// - svg::div, my_elements::foo - tags with custom paths, that doesnt look like component
// - my_component::Foo - components with custom paths
// - html:div - tags punctuated by `:`
// - {div}, {"div"} - any rust expression
impl IdeTagHelper {
pub fn new() -> Self {
Self(Vec::new())
}
/// Save stmts for tag name.
/// Emit warning if tag is component.
pub fn save_tag_completion(&mut self, name: &NodeName) {
if is_component_tag_name(name) {
proc_macro_error::emit_warning!(
name.span(),
"BUG: Component tag is used in regular tag completion."
);
}
for path in Self::completion_stmts(name) {
self.0.push(quote! {
let _ = #path;
});
}
}
/// Save stmts for open and close tags.
/// Emit warning if tag is component.
pub fn save_element_completion(&mut self, node: &NodeElement) {
self.save_tag_completion(node.name());
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
self.save_tag_completion(close_tag)
}
}
/* This has been (temporarily?) removed.
* Its purpose was simply to add syntax highlighting and IDE hints for
* component closing tags in debug mode by associating the closing tag
* ident with the component function.
*
* Doing this in a way that correctly inferred types, however, required
* duplicating the entire component constructor.
*
* In view trees with many nested components, this led to a massive explosion
* in compile times.
*
* See https://github.com/leptos-rs/leptos/issues/1283
*
/// Add completion to the closing tag of the component.
///
/// In order to ensure that generics are passed through correctly in the
/// current builder pattern, this clones the whole component constructor,
/// but it will never be used.
///
/// ```no_build
/// if false {
/// close_tag(unreachable!())
/// }
/// else {
/// open_tag(open_tag.props().slots().children().build())
/// }
/// ```
#[cfg(debug_assertions)]
pub fn add_component_completion(
component: &mut TokenStream,
node: &NodeElement,
) {
// emit ide helper info
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
*component = quote! {
{
let #close_tag = || #component;
#close_tag()
}
}
}
}
*/
/// Returns `syn::Path`-like `TokenStream` to the fn in docs.
/// If tag name is `Component` returns `None`.
fn create_regular_tag_fn_path(name: &Ident) -> TokenStream {
let tag_name = name.to_string();
let namespace = if crate::view::is_svg_element(&tag_name) {
quote! { ::leptos::leptos_dom::svg }
} else if crate::view::is_math_ml_element(&tag_name) {
quote! { ::leptos::leptos_dom::math }
} else {
// todo: check is html, and emit_warning in case of custom tag
quote! { ::leptos::leptos_dom::html }
};
quote! { #namespace::#name }
}
/// Returns `syn::Path`-like `TokenStream` to the `custom` section in docs.
fn create_custom_tag_fn_path(span: Span) -> TokenStream {
let custom_ident = Ident::new("custom", span);
quote! { ::leptos::leptos_dom::html::#custom_ident::<::leptos::leptos_dom::html::Custom> }
}
// Extract from NodeName completion idents.
// Custom tags (like foo-bar-baz) is mapped
// to vec!["custom", "custom",.. ] for each token in tag, even for "-".
// Only last ident from `Path` is used.
fn completion_stmts(name: &NodeName) -> Vec<TokenStream> {
match name {
NodeName::Block(_) => vec![],
NodeName::Punctuated(c) => c
.pairs()
.flat_map(|c| {
let mut idents =
vec![Self::create_custom_tag_fn_path(c.value().span())];
if let Some(p) = c.punct() {
idents.push(Self::create_custom_tag_fn_path(p.span()))
}
idents
})
.collect(),
NodeName::Path(e) => e
.path
.segments
.last()
.map(|p| &p.ident)
.map(Self::create_regular_tag_fn_path)
.into_iter()
.collect(),
}
}
}
impl IntoIterator for IdeTagHelper {
type Item = TokenStream;
type IntoIter = <Vec<TokenStream> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,745 +0,0 @@
use super::{
camel_case_tag_name,
component_builder::component_to_tokens,
fancy_class_name, fancy_style_name,
ide_helper::IdeTagHelper,
is_custom_element, is_math_ml_element, is_self_closing, is_svg_element,
parse_event_name,
slot_helper::{get_slot, slot_to_tokens},
};
use crate::attribute_value;
use leptos_hot_reload::parsing::{
block_to_primitive_expression, is_component_node, value_to_string,
};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{quote, quote_spanned};
use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
};
use std::collections::HashMap;
use syn::spanned::Spanned;
pub(crate) enum SsrElementChunks {
String {
template: String,
holes: Vec<TokenStream>,
},
View(TokenStream),
}
pub(crate) fn root_node_to_tokens_ssr(
node: &Node,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> TokenStream {
match node {
Node::Fragment(fragment) => fragment_to_tokens_ssr(
&fragment.children,
global_class,
view_marker,
),
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Text(node) => {
quote! {
::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) => {
quote! {
#node
}
}
Node::Element(node) => {
root_element_to_tokens_ssr(node, global_class, view_marker)
.unwrap_or_default()
}
}
}
pub(crate) fn fragment_to_tokens_ssr(
nodes: &[Node],
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> TokenStream {
let original_span = nodes
.first()
.zip(nodes.last())
.and_then(|(first, last)| first.span().join(last.span()))
.unwrap_or_else(Span::call_site);
let view_marker = if let Some(marker) = view_marker {
quote! { .with_view_marker(#marker) }
} else {
quote! {}
};
let nodes = nodes.iter().map(|node| {
let span = node.span();
let node = root_node_to_tokens_ssr(node, global_class, None);
let node = quote_spanned!(span=> { #node });
quote! {
::leptos::IntoView::into_view(#[allow(unused_braces)] #node)
}
});
quote_spanned! {original_span=>
{
::leptos::Fragment::lazy(|| ::std::vec![
#(#nodes),*
])
#view_marker
}
}
}
pub(crate) fn root_element_to_tokens_ssr(
node: &NodeElement,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> Option<TokenStream> {
// TODO: simplify, this is checked twice, second time in `element_to_tokens_ssr` body
if is_component_node(node) {
if let Some(slot) = get_slot(node) {
slot_to_tokens(node, slot, None, global_class);
None
} else {
Some(component_to_tokens(node, global_class))
}
} else {
let mut stmts_for_ide = IdeTagHelper::new();
let mut exprs_for_compiler = Vec::<TokenStream>::new();
let mut template = String::new();
let mut holes = Vec::new();
let mut chunks = Vec::new();
element_to_tokens_ssr(
node,
None,
&mut template,
&mut holes,
&mut chunks,
&mut stmts_for_ide,
&mut exprs_for_compiler,
true,
global_class,
);
// push final chunk
if !template.is_empty() {
chunks.push(SsrElementChunks::String { template, holes })
}
let chunks = chunks.into_iter().map(|chunk| match chunk {
SsrElementChunks::String { template, holes } => {
if holes.is_empty() {
let template = template.replace("\\{", "{").replace("\\}", "}");
quote! {
::leptos::leptos_dom::html::StringOrView::String(#template.into())
}
} else {
let template = template.replace("\\{", "{{").replace("\\}", "}}");
quote! {
::leptos::leptos_dom::html::StringOrView::String(
::std::format!(
#template,
#(#holes),*
).into()
)
}
}
}
SsrElementChunks::View(view) => {
quote! {
#[allow(unused_braces)]
{
let view = #view;
::leptos::leptos_dom::html::StringOrView::View(::std::rc::Rc::new(move || view.clone()))
}
}
},
});
let tag_name = node.name().to_string();
let is_custom_element = is_custom_element(&tag_name);
// Use any other span instead of node.name.span(), to avoid misunderstanding in IDE.
// We can use open_tag.span(), to provide similar (to name span) diagnostic
// in case of expansion error, but it will also highlight "<" token.
let typed_element_name = if is_custom_element {
Ident::new("Custom", Span::call_site())
} else {
let camel_cased = camel_case_tag_name(
tag_name
.trim_start_matches("svg::")
.trim_start_matches("math::")
.trim_end_matches('_'),
);
Ident::new(&camel_cased, Span::call_site())
};
let typed_element_name = if is_svg_element(&tag_name) {
quote! { svg::#typed_element_name }
} else if is_math_ml_element(&tag_name) {
quote! { math::#typed_element_name }
} else {
quote! { html::#typed_element_name }
};
let full_name = if is_custom_element {
quote! {
::leptos::leptos_dom::html::Custom::new(#tag_name)
}
} else {
quote! {
<::leptos::leptos_dom::#typed_element_name as ::std::default::Default>::default()
}
};
let view_marker = if let Some(marker) = view_marker {
quote! { .with_view_marker(#marker) }
} else {
quote! {}
};
let stmts_for_ide = stmts_for_ide.into_iter();
Some(quote! {
#[allow(unused_braces)]
{
#(#stmts_for_ide)*
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_chunks(#full_name, [#(#chunks),*])#view_marker
}
})
}
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens_ssr(
node: &NodeElement,
parent_slots: Option<&mut HashMap<String, Vec<TokenStream>>>,
template: &mut String,
holes: &mut Vec<TokenStream>,
chunks: &mut Vec<SsrElementChunks>,
stmts_for_ide: &mut IdeTagHelper,
exprs_for_compiler: &mut Vec<TokenStream>,
is_root: bool,
global_class: Option<&TokenTree>,
) {
if is_component_node(node) {
if let Some(slot) = get_slot(node) {
slot_to_tokens(node, slot, parent_slots, global_class);
return;
}
let component = component_to_tokens(node, global_class);
if !template.is_empty() {
chunks.push(SsrElementChunks::String {
template: std::mem::take(template),
holes: std::mem::take(holes),
})
}
chunks.push(SsrElementChunks::View(quote! {
::leptos::IntoView::into_view(#[allow(unused_braces)] {#component})
}));
} else {
let tag_name = node.name().to_string();
let tag_name = tag_name
.trim_start_matches("svg::")
.trim_start_matches("math::")
.trim_end_matches('_');
let is_script_or_style = tag_name == "script" || tag_name == "style";
template.push('<');
template.push_str(tag_name);
#[cfg(debug_assertions)]
stmts_for_ide.save_element_completion(node);
let mut inner_html = None;
for attr in node.attributes() {
if let NodeAttribute::Attribute(attr) = attr {
inner_html = attribute_to_tokens_ssr(
attr,
template,
holes,
exprs_for_compiler,
global_class,
);
}
}
for attr in node.attributes() {
use syn::{Expr, ExprRange, RangeLimits, Stmt};
if let NodeAttribute::Block(NodeBlock::ValidBlock(block)) = attr {
if let Some(Stmt::Expr(
Expr::Range(ExprRange {
start: None,
limits: RangeLimits::HalfOpen(_),
end: Some(end),
..
}),
_,
)) = block.stmts.first()
{
// should basically be the resolved attributes, joined on spaces, placed into
// the template
template.push_str(" {}");
let end_into_iter =
quote_spanned!(end.span()=> {#end}.into_iter());
holes.push(quote_spanned! {block.span()=>
#end_into_iter.filter_map(|(name, attr)| {
Some(::std::format!(
"{}=\"{}\"",
name,
::leptos::leptos_dom::ssr::escape_attr(&attr.as_nameless_value_string()?)
))
}).collect::<::std::vec::Vec<_>>().join(" ")
});
};
}
}
// insert hydration ID
let hydration_id = if is_root {
quote! { ::leptos::leptos_dom::HydrationCtx::peek() }
} else {
quote! { ::leptos::leptos_dom::HydrationCtx::id() }
};
template.push_str("{}");
holes.push(quote! {
#hydration_id.map(|id| ::std::format!(" data-hk=\"{id}\"")).unwrap_or_default()
});
set_class_attribute_ssr(node, template, holes, global_class);
set_style_attribute_ssr(node, template, holes);
if is_self_closing(node) {
template.push_str("/>");
} else {
template.push('>');
if let Some(inner_html) = inner_html {
template.push_str("{}");
let value = inner_html;
holes.push(quote! {
::leptos::IntoAttribute::into_attribute(#value).as_nameless_value_string().unwrap_or_default()
})
} else {
for child in &node.children {
match child {
Node::Element(child) => {
element_to_tokens_ssr(
child,
None,
template,
holes,
chunks,
stmts_for_ide,
exprs_for_compiler,
false,
global_class,
);
}
Node::Text(text) => {
let value = text.value_string();
let value = if is_script_or_style {
value.into()
} else {
html_escape::encode_safe(&value)
};
template.push_str(
&value.replace('{', "\\{").replace('}', "\\}"),
);
}
Node::RawText(r) => {
let value = r.to_string_best();
let value = if is_script_or_style {
value.into()
} else {
html_escape::encode_safe(&value)
};
template.push_str(
&value.replace('{', "\\{").replace('}', "\\}"),
);
}
Node::Block(NodeBlock::ValidBlock(block)) => {
if let Some(value) =
block_to_primitive_expression(block)
.and_then(value_to_string)
{
template.push_str(&value);
} else {
if !template.is_empty() {
chunks.push(SsrElementChunks::String {
template: std::mem::take(template),
holes: std::mem::take(holes),
})
}
chunks.push(SsrElementChunks::View(quote! {
::leptos::IntoView::into_view(#block)
}));
}
}
// Keep invalid blocks for faster IDE diff (on user type)
Node::Block(block @ NodeBlock::Invalid { .. }) => {
chunks.push(SsrElementChunks::View(quote! {
::leptos::IntoView::into_view(#block)
}));
}
Node::Fragment(_) => abort!(
Span::call_site(),
"You can't nest a fragment inside an element."
),
Node::Comment(_) | Node::Doctype(_) => {}
}
}
}
template.push_str("</");
template.push_str(tag_name);
template.push('>');
}
}
}
// returns `inner_html`
fn attribute_to_tokens_ssr<'a>(
attr: &'a KeyedAttribute,
template: &mut String,
holes: &mut Vec<TokenStream>,
exprs_for_compiler: &mut Vec<TokenStream>,
global_class: Option<&TokenTree>,
) -> 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(attr);
let (event_type, _, _) = parse_event_name(name);
exprs_for_compiler.push(quote! {
::leptos::leptos_dom::helpers::ssr_event_listener(::leptos::ev::#event_type, #handler);
})
} else if name.strip_prefix("prop:").is_some()
|| name.strip_prefix("class:").is_some()
|| name.strip_prefix("style:").is_some()
{
// ignore props for SSR
// ignore classes and sdtyles: we'll handle these separately
if name.starts_with("prop:") {
let value = attr.value();
exprs_for_compiler.push(quote! {
#[allow(unused_braces)]
{ _ = #value; }
});
}
} else if let Some(directive_name) = name.strip_prefix("use:") {
let handler = syn::Ident::new(directive_name, attr.key.span());
let value = attr.value();
let value = value.map(|value| {
quote! {
_ = #value;
}
});
exprs_for_compiler.push(quote! {
#[allow(unused_braces)]
{
_ = #handler;
#value
}
});
} else if name == "inner_html" {
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()
&& attr.value().and_then(value_to_string).is_none()
{
let span = attr.key.span();
proc_macro_error::emit_error!(span, "Combining a global class (view! { 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 \
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
};
if name != "class" && name != "style" {
template.push(' ');
if let Some(value) = attr.value() {
if let Some(value) = value_to_string(value) {
template.push_str(&name);
template.push_str("=\"");
template.push_str(&html_escape::encode_quoted_attribute(
&value,
));
template.push('"');
} else {
template.push_str("{}");
holes.push(quote! {
&::leptos::IntoAttribute::into_attribute(#value)
.as_nameless_value_string()
.map(|a| ::std::format!(
"{}=\"{}\"",
#name,
::leptos::leptos_dom::ssr::escape_attr(&a)
))
.unwrap_or_default()
})
}
} else {
template.push_str(&name);
}
}
};
None
}
fn set_class_attribute_ssr(
node: &NodeElement,
template: &mut String,
holes: &mut Vec<TokenStream>,
global_class: Option<&TokenTree>,
) {
let (static_global_class, dyn_global_class) = match global_class {
Some(TokenTree::Literal(lit)) => {
let str = lit.to_string();
// A lit here can be a string, byte_string, char, byte_char, int or float.
// If it's a string we remove the quotes so folks can use them directly
// without needing braces. E.g. view!{class="my-class", ... }
let str = if str.starts_with('"') && str.ends_with('"') {
str[1..str.len() - 1].to_string()
} else {
str
};
(str, None)
}
None => (String::new(), None),
Some(val) => (String::new(), Some(val)),
};
let static_class_attr = node
.attributes()
.iter()
.filter_map(|a| match a {
NodeAttribute::Attribute(attr)
if attr.key.to_string() == "class" =>
{
attr.value().and_then(value_to_string)
}
_ => None,
})
.chain(Some(static_global_class))
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
let dyn_class_attr = node
.attributes()
.iter()
.filter_map(|a| {
if let NodeAttribute::Attribute(a) = a {
if a.key.to_string() == "class" {
if a.value().and_then(value_to_string).is_some()
|| fancy_class_name(&a.key.to_string(), a).is_some()
{
None
} else {
Some((a.key.span(), a.value()))
}
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
let class_attrs = node
.attributes()
.iter()
.filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
let name = node.key.to_string();
if name == "class" {
return if let Some((_, name, value)) =
fancy_class_name(&name, node)
{
let span = node.key.span();
Some((span, name, value))
} else {
None
};
}
if name.starts_with("class:") || name.starts_with("class-") {
let name = if name.starts_with("class:") {
name.replacen("class:", "", 1)
} else if name.starts_with("class-") {
name.replacen("class-", "", 1)
} else {
name
};
let value = attribute_value(node);
let span = node.key.span();
Some((span, name, value))
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
if !static_class_attr.is_empty()
|| !dyn_class_attr.is_empty()
|| !class_attrs.is_empty()
|| dyn_global_class.is_some()
{
template.push_str(" class=\"");
template.push_str(&html_escape::encode_quoted_attribute(
&static_class_attr,
));
for (_span, value) in dyn_class_attr {
if let Some(value) = value {
template.push_str(" {}");
holes.push(quote! {
&::leptos::IntoAttribute::into_attribute(#value).as_nameless_value_string()
.map(|a| ::leptos::leptos_dom::ssr::escape_attr(&a).to_string())
.unwrap_or_default()
});
}
}
for (_span, name, value) in &class_attrs {
template.push_str(" {}");
holes.push(quote! {
::leptos::IntoClass::into_class(#value).as_value_string(#name)
});
}
if let Some(dyn_global_class) = dyn_global_class {
template.push_str(" {}");
holes.push(quote! { #dyn_global_class });
}
template.push('"');
}
}
fn set_style_attribute_ssr(
node: &NodeElement,
template: &mut String,
holes: &mut Vec<TokenStream>,
) {
let static_style_attr = node
.attributes()
.iter()
.find_map(|a| match a {
NodeAttribute::Attribute(attr)
if attr.key.to_string() == "style" =>
{
attr.value().and_then(value_to_string)
}
_ => None,
})
.map(|style| format!("{style};"));
let dyn_style_attr = node
.attributes()
.iter()
.filter_map(|a| {
if let NodeAttribute::Attribute(a) = a {
if a.key.to_string() == "style" {
if a.value().and_then(value_to_string).is_some()
|| fancy_style_name(&a.key.to_string(), a).is_some()
{
None
} else {
Some((a.key.span(), a.value()))
}
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
let style_attrs = node
.attributes()
.iter()
.filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
let name = node.key.to_string();
if name == "style" {
return if let Some((_, name, value)) =
fancy_style_name(&name, node)
{
let span = node.key.span();
Some((span, name, value))
} else {
None
};
}
if name.starts_with("style:") || name.starts_with("style-") {
let name = if name.starts_with("style:") {
name.replacen("style:", "", 1)
} else if name.starts_with("style-") {
name.replacen("style-", "", 1)
} else {
name
};
let value = attribute_value(node);
let span = node.key.span();
Some((span, name, value))
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
if static_style_attr.is_some()
|| !dyn_style_attr.is_empty()
|| !style_attrs.is_empty()
{
template.push_str(" style=\"");
template.push_str(&static_style_attr.unwrap_or_default());
for (_span, value) in dyn_style_attr {
if let Some(value) = value {
template.push_str(" {};");
holes.push(quote! {
&::leptos::IntoAttribute::into_attribute(#value).as_nameless_value_string()
.map(|a| ::leptos::leptos_dom::ssr::escape_attr(&a).to_string())
.unwrap_or_default()
});
}
}
for (_span, name, value) in &style_attrs {
template.push_str(" {}");
holes.push(quote! {
::leptos::IntoStyle::into_style(#value).as_value_string(#name).unwrap_or_default()
});
}
template.push('"');
}
}

View file

@ -1,14 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: pretty(result)
---
fn view() {
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
::leptos::component_props_builder(&SimpleCounter)
.initial_value(#[allow(unused_braces)] { 0 })
.step(#[allow(unused_braces)] { 1 })
.build(),
)
}

View file

@ -1,22 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: pretty(result)
---
fn view() {
::leptos::IntoView::into_view(
#[allow(unused_braces)]
{
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
::leptos::component_props_builder(&ExternalComponent).build(),
)
},
)
.on(
::leptos::leptos_dom::ev::undelegated(
::leptos::leptos_dom::ev::Custom::new("custom.event.clear"),
),
move |_: Event| set_value(0),
)
}

View file

@ -1,399 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: IntoView,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: into_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unused_braces,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Group {
delimiter: Brace,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: leptos,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: component_props_builder,
span: bytes(11..28),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
],
span: bytes(11..28),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: build,
span: bytes(11..28),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
span: bytes(11..28),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
},
Ident {
sym: on,
span: bytes(29..50),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos_dom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: ev,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: undelegated,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos_dom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: ev,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: Custom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: new,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
lit: "custom.event.clear",
},
],
},
],
},
Punct {
char: ',',
spacing: Alone,
},
Ident {
sym: move,
span: bytes(51..55),
},
Punct {
char: '|',
spacing: Alone,
span: bytes(56..57),
},
Ident {
sym: _,
span: bytes(57..58),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(58..59),
},
Ident {
sym: Event,
span: bytes(60..65),
},
Punct {
char: '|',
spacing: Alone,
span: bytes(65..66),
},
Ident {
sym: set_value,
span: bytes(67..76),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
lit: 0,
span: bytes(77..78),
},
],
span: bytes(76..79),
},
],
},
]

View file

@ -1,113 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: pretty(result)
---
fn view() {
{
thread_local! {
static __TEMPLATE : ::leptos::web_sys::HtmlTemplateElement = { let document =
::leptos::document(); let el = document.create_element("template").unwrap();
el
.set_inner_html("<div><button>Clear</button><button>-1</button><span>Value: <!>!</span><button>+1</button></div>");
::leptos::wasm_bindgen::JsCast::unchecked_into(el) }
}
let _ = ::leptos::leptos_dom::html::div;
let _ = ::leptos::leptos_dom::html::div;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::span;
let _ = ::leptos::leptos_dom::html::span;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::button;
let root = __TEMPLATE
.with(|tpl| tpl.content().clone_node_with_deep(true))
.unwrap()
.first_child()
.unwrap();
let _el1 = "div";
let _el1 = ::leptos::wasm_bindgen::JsCast::unchecked_into::<
::leptos::web_sys::Node,
>(root.clone());
let _el2 = "button";
let _el2 = _el1
.first_child()
.unwrap_or_else(|| ::std::panic!("error: {} => {}", "button", "firstChild"));
let _el3 = _el2
.first_child()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "{block}", "firstChild")
});
let _el4 = "button";
let _el4 = _el2
.next_sibling()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "button", "nextSibling")
});
let _el5 = _el4
.first_child()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "{block}", "firstChild")
});
let _el6 = "span";
let _el6 = _el4
.next_sibling()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "span", "nextSibling")
});
let _el7 = _el6
.first_child()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "{block}", "firstChild")
});
let _el8 = _el7
.next_sibling()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "{block}", "nextSibling")
});
let _el9 = _el8
.next_sibling()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "{block}", "nextSibling")
});
let _el10 = "button";
let _el10 = _el6
.next_sibling()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "button", "nextSibling")
});
let _el11 = _el10
.first_child()
.unwrap_or_else(|| {
::std::panic!("error : {} => {} ", "{block}", "firstChild")
});
::leptos::leptos_dom::add_event_helper(
::leptos::wasm_bindgen::JsCast::unchecked_ref(&_el2),
::leptos::leptos_dom::ev::click,
move |_| set_value(0),
);
::leptos::leptos_dom::add_event_helper(
::leptos::wasm_bindgen::JsCast::unchecked_ref(&_el4),
::leptos::leptos_dom::ev::click,
move |_| set_value.update(|value| *value -= step),
);
::leptos::leptos_dom::mount_child(
::leptos::leptos_dom::MountKind::Before(&_el8.clone()),
&::leptos::IntoView::into_view(#[allow(unused_braces)] { { value } }),
);
::leptos::leptos_dom::add_event_helper(
::leptos::wasm_bindgen::JsCast::unchecked_ref(&_el10),
::leptos::leptos_dom::ev::click,
move |_| set_value.update(|value| *value += step),
);
::leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
#[cfg(debug_assertions)]
name: "div".into(),
element: ::leptos::wasm_bindgen::JsCast::unchecked_into(root),
#[cfg(debug_assertions)]
view_marker: None,
})
}
}

View file

@ -1,14 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: pretty(result)
---
fn view() {
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
::leptos::component_props_builder(&SimpleCounter)
.initial_value(#[allow(unused_braces)] { 0 })
.step(#[allow(unused_braces)] { 1 })
.build(),
)
}

View file

@ -1,22 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: pretty(result)
---
fn view() {
::leptos::IntoView::into_view(
#[allow(unused_braces)]
{
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
::leptos::component_props_builder(&ExternalComponent).build(),
)
},
)
.on(
::leptos::leptos_dom::ev::undelegated(
::leptos::leptos_dom::ev::Custom::new("custom.event.clear"),
),
move |_: Event| set_value(0),
)
}

View file

@ -1,250 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..24),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: leptos,
span: bytes(11..24),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..24),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: component_props_builder,
span: bytes(11..24),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
],
span: bytes(11..24),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(37..52),
},
Ident {
sym: initial_value,
span: bytes(37..50),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(37..52),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(37..52),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unused_braces,
span: bytes(37..52),
},
],
span: bytes(37..52),
},
],
span: bytes(37..52),
},
Group {
delimiter: Brace,
stream: TokenStream [
Literal {
lit: 0,
span: bytes(51..52),
},
],
span: bytes(51..52),
},
],
span: bytes(37..52),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(65..71),
},
Ident {
sym: step,
span: bytes(65..69),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(65..71),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(65..71),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unused_braces,
span: bytes(65..71),
},
],
span: bytes(65..71),
},
],
span: bytes(65..71),
},
Group {
delimiter: Brace,
stream: TokenStream [
Literal {
lit: 1,
span: bytes(70..71),
},
],
span: bytes(70..71),
},
],
span: bytes(65..71),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: build,
span: bytes(11..24),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
span: bytes(11..24),
},
],
span: bytes(10..82),
},
]

View file

@ -1,399 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: IntoView,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: into_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unused_braces,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Group {
delimiter: Brace,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: leptos,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: component_props_builder,
span: bytes(11..28),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
],
span: bytes(11..28),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: build,
span: bytes(11..28),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
span: bytes(11..28),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
},
Ident {
sym: on,
span: bytes(29..50),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos_dom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: ev,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: undelegated,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos_dom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: ev,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: Custom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: new,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
lit: "custom.event.clear",
},
],
},
],
},
Punct {
char: ',',
spacing: Alone,
},
Ident {
sym: move,
span: bytes(51..55),
},
Punct {
char: '|',
spacing: Alone,
span: bytes(56..57),
},
Ident {
sym: _,
span: bytes(57..58),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(58..59),
},
Ident {
sym: Event,
span: bytes(60..65),
},
Punct {
char: '|',
spacing: Alone,
span: bytes(65..66),
},
Ident {
sym: set_value,
span: bytes(67..76),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
lit: 0,
span: bytes(77..78),
},
],
span: bytes(76..79),
},
],
},
]

View file

@ -1,56 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
#[allow(unused_braces)]
{
let _ = ::leptos::leptos_dom::html::div;
::leptos::leptos_dom::html::div()
.child(
#[allow(unused_braces)]
{
let _ = ::leptos::leptos_dom::html::button;
::leptos::leptos_dom::html::button()
.on(::leptos::ev::click, move |_| set_value(0))
.child("Clear")
},
)
.child(
#[allow(unused_braces)]
{
let _ = ::leptos::leptos_dom::html::button;
::leptos::leptos_dom::html::button()
.on(
::leptos::ev::click,
move |_| set_value.update(|value| *value -= step),
)
.child("-1")
},
)
.child(
#[allow(unused_braces)]
{
let _ = ::leptos::leptos_dom::html::span;
::leptos::leptos_dom::html::span()
.child("Value: ")
.child({ value })
.child("!")
},
)
.child(
#[allow(unused_braces)]
{
let _ = ::leptos::leptos_dom::html::button;
::leptos::leptos_dom::html::button()
.on(
::leptos::ev::click,
move |_| set_value.update(|value| *value += step),
)
.child("+1")
},
)
}
}

View file

@ -1,14 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: pretty(result)
---
fn view() {
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&SimpleCounter,
::leptos::component_props_builder(&SimpleCounter)
.initial_value(#[allow(unused_braces)] { 0 })
.step(#[allow(unused_braces)] { 1 })
.build(),
)
}

View file

@ -1,22 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: pretty(result)
---
fn view() {
::leptos::IntoView::into_view(
#[allow(unused_braces)]
{
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
&ExternalComponent,
::leptos::component_props_builder(&ExternalComponent).build(),
)
},
)
.on(
::leptos::leptos_dom::ev::undelegated(
::leptos::leptos_dom::ev::Custom::new("custom.event.clear"),
),
move |_: Event| set_value(0),
)
}

View file

@ -1,250 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..24),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: leptos,
span: bytes(11..24),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..24),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: component_props_builder,
span: bytes(11..24),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '&',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: SimpleCounter,
span: bytes(11..24),
},
],
span: bytes(11..24),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(37..52),
},
Ident {
sym: initial_value,
span: bytes(37..50),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(37..52),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(37..52),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unused_braces,
span: bytes(37..52),
},
],
span: bytes(37..52),
},
],
span: bytes(37..52),
},
Group {
delimiter: Brace,
stream: TokenStream [
Literal {
lit: 0,
span: bytes(51..52),
},
],
span: bytes(51..52),
},
],
span: bytes(37..52),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(65..71),
},
Ident {
sym: step,
span: bytes(65..69),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(65..71),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(65..71),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unused_braces,
span: bytes(65..71),
},
],
span: bytes(65..71),
},
],
span: bytes(65..71),
},
Group {
delimiter: Brace,
stream: TokenStream [
Literal {
lit: 1,
span: bytes(70..71),
},
],
span: bytes(70..71),
},
],
span: bytes(65..71),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(11..24),
},
Ident {
sym: build,
span: bytes(11..24),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
span: bytes(11..24),
},
],
span: bytes(10..82),
},
]

View file

@ -1,399 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
expression: result
---
TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: IntoView,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: into_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: unused_braces,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Group {
delimiter: Brace,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: leptos,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: component_view,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '#',
spacing: Alone,
span: bytes(10..82),
},
Group {
delimiter: Bracket,
stream: TokenStream [
Ident {
sym: allow,
span: bytes(10..82),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Ident {
sym: clippy,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(10..82),
},
Ident {
sym: needless_borrows_for_generic_args,
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
Punct {
char: ',',
spacing: Alone,
span: bytes(10..82),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: leptos,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Joint,
span: bytes(11..28),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: component_props_builder,
span: bytes(11..28),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: '&',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: ExternalComponent,
span: bytes(11..28),
},
],
span: bytes(11..28),
},
Punct {
char: '.',
spacing: Alone,
span: bytes(11..28),
},
Ident {
sym: build,
span: bytes(11..28),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
span: bytes(11..28),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
],
span: bytes(10..82),
},
Punct {
char: '.',
spacing: Alone,
},
Ident {
sym: on,
span: bytes(29..50),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos_dom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: ev,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: undelegated,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: leptos_dom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: ev,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: Custom,
},
Punct {
char: ':',
spacing: Joint,
},
Punct {
char: ':',
spacing: Alone,
},
Ident {
sym: new,
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
lit: "custom.event.clear",
},
],
},
],
},
Punct {
char: ',',
spacing: Alone,
},
Ident {
sym: move,
span: bytes(51..55),
},
Punct {
char: '|',
spacing: Alone,
span: bytes(56..57),
},
Ident {
sym: _,
span: bytes(57..58),
},
Punct {
char: ':',
spacing: Alone,
span: bytes(58..59),
},
Ident {
sym: Event,
span: bytes(60..65),
},
Punct {
char: '|',
spacing: Alone,
span: bytes(65..66),
},
Ident {
sym: set_value,
span: bytes(67..76),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
lit: 0,
span: bytes(77..78),
},
],
span: bytes(76..79),
},
],
},
]

View file

@ -1,67 +0,0 @@
---
source: leptos_macro/src/view/tests.rs
assertion_line: 101
expression: pretty(result)
---
fn view() {
#[allow(unused_braces)]
{
let _ = ::leptos::leptos_dom::html::div;
let _ = ::leptos::leptos_dom::html::div;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::span;
let _ = ::leptos::leptos_dom::html::span;
let _ = ::leptos::leptos_dom::html::button;
let _ = ::leptos::leptos_dom::html::button;
::leptos::leptos_dom::helpers::ssr_event_listener(
::leptos::ev::click,
move |_| set_value(0),
);
::leptos::leptos_dom::helpers::ssr_event_listener(
::leptos::ev::click,
move |_| set_value.update(|value| *value -= step),
);
::leptos::leptos_dom::helpers::ssr_event_listener(
::leptos::ev::click,
move |_| set_value.update(|value| *value += step),
);
::leptos::HtmlElement::from_chunks(
<::leptos::leptos_dom::html::Div as ::std::default::Default>::default(),
[
::leptos::leptos_dom::html::StringOrView::String(
::std::format!(
"<div{}><button{}>Clear</button><button{}>-1</button><span{}>Value: ",
::leptos::leptos_dom::HydrationCtx::peek().map(| id |
::std::format!(" data-hk=\"{id}\"")).unwrap_or_default(),
::leptos::leptos_dom::HydrationCtx::id().map(| id |
::std::format!(" data-hk=\"{id}\"")).unwrap_or_default(),
::leptos::leptos_dom::HydrationCtx::id().map(| id |
::std::format!(" data-hk=\"{id}\"")).unwrap_or_default(),
::leptos::leptos_dom::HydrationCtx::id().map(| id |
::std::format!(" data-hk=\"{id}\"")).unwrap_or_default()
)
.into(),
),
#[allow(unused_braces)]
{
let view = ::leptos::IntoView::into_view({ value });
::leptos::leptos_dom::html::StringOrView::View(
::std::rc::Rc::new(move || view.clone()),
)
},
::leptos::leptos_dom::html::StringOrView::String(
::std::format!(
"!</span><button{}>+1</button></div>",
::leptos::leptos_dom::HydrationCtx::id().map(| id |
::std::format!(" data-hk=\"{id}\"")).unwrap_or_default()
)
.into(),
),
],
)
}
}

View file

@ -1,119 +0,0 @@
use proc_macro2::TokenStream;
use std::str::FromStr;
use syn::parse_quote;
fn pretty(input: TokenStream) -> String {
let type_item: syn::Item = parse_quote! {
fn view(){
#input
}
};
let file = syn::File {
shebang: None,
attrs: vec![],
items: vec![type_item],
};
prettyplease::unparse(&file)
}
macro_rules! assert_snapshot
{
(@assert text $result:ident) => {
insta::assert_snapshot!(pretty($result))
};
(@assert full $result:ident) => {
insta::assert_debug_snapshot!($result)
};
(client_template($assert:ident) => $input: expr) => {
{
let tokens = TokenStream::from_str($input).unwrap();
let nodes = rstml::parse2(tokens).unwrap();
let result = crate::view::client_template::render_template(&&nodes);
assert_snapshot!(@assert $assert result)
}
};
(client_builder($assert:ident) => $input: expr) => {
{
let tokens = TokenStream::from_str($input).unwrap();
let nodes = rstml::parse2(tokens).unwrap();
let mode = crate::view::Mode::Client;
let global_class = None;
let call_site = None;
let result = crate::view::render_view(&&nodes, mode, global_class, call_site);
assert_snapshot!(@assert $assert result)
}
};
(server_template($assert:ident) => $input: expr) => {
{
let tokens = TokenStream::from_str($input).unwrap();
let nodes = rstml::parse2(tokens).unwrap();
let mode = crate::view::Mode::Ssr;
let global_class = None;
let call_site = None;
let result = crate::view::render_view(&&nodes, mode, global_class, call_site);
assert_snapshot!(@assert $assert result)
}
}
}
macro_rules! for_all_modes {
(@ $module: ident, $type: ident => $(
$test_name:ident => $raw_str:expr
),*
) => {
mod $module {
use super::*;
$(
#[test]
fn $test_name() {
assert_snapshot!($type(text) => $raw_str)
}
)*
mod full_span {
use super::*;
$(
#[test]
fn $test_name() {
assert_snapshot!($type(full) => $raw_str)
}
)*
}
}
};
( $(
$tts:tt
)*
) => {
for_all_modes!{@ csr, client_builder => $($tts)*}
for_all_modes!{@ client_template, client_template => $($tts)*}
for_all_modes!{@ ssr, server_template => $($tts)*}
};
}
for_all_modes! {
test_simple_counter => r#"
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
</div>
"#,
test_counter_component => r#"
<SimpleCounter
initial_value=0
step=1
/>
"#,
test_custom_event => r#"
<ExternalComponent on:custom.event.clear=move |_: Event| set_value(0) />
"#
}

View file

@ -0,0 +1,100 @@
use std::sync::Arc;
use tachydom::{
renderer::dom::Dom,
view::{
any_view::{AnyView, IntoAny},
RenderHtml,
},
};
/// The most common type for the `children` property on components,
/// which can only be called once.
pub type Children = Box<dyn FnOnce() -> AnyView<Dom>>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Arc<dyn Fn() -> AnyView<Dom>>;
/// A type for the `children` property on components that can be called
/// more than once, but may mutate the children.
pub type ChildrenFnMut = Box<dyn FnMut() -> AnyView<Dom>>;
// This is to still support components that accept `Box<dyn Fn() -> AnyView>` as a children.
type BoxedChildrenFn = Box<dyn Fn() -> AnyView<Dom>>;
#[doc(hidden)]
pub trait ToChildren<F> {
fn to_children(f: F) -> Self;
}
impl<F, C> ToChildren<F> for Children
where
F: FnOnce() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(move || f().into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFn
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Arc::new(move || f().into_any())
}
}
impl<F, C> ToChildren<F> for ChildrenFnMut
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(move || f().into_any())
}
}
impl<F, C> ToChildren<F> for BoxedChildrenFn
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
#[inline]
fn to_children(f: F) -> Self {
Box::new(move || f().into_any())
}
}
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
pub struct ViewFn(Arc<dyn Fn() -> AnyView<Dom>>);
impl Default for ViewFn {
fn default() -> Self {
Self(Arc::new(|| ().into_any()))
}
}
impl<F, C> From<F> for ViewFn
where
F: Fn() -> C + 'static,
C: RenderHtml<Dom> + 'static,
{
fn from(value: F) -> Self {
Self(Arc::new(move || value().into_any()))
}
}
impl ViewFn {
/// Execute the wrapped function
pub fn run(&self) -> AnyView<Dom> {
(self.0)()
}
}

View file

@ -0,0 +1,83 @@
//! Utility traits and functions that allow building components,
//! as either functions of their props or functions with no arguments,
//! without knowing the name of the props struct.
pub trait Component<P> {}
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}
#[doc(hidden)]
pub trait PropsOrNoPropsBuilder {
type Builder;
fn builder_or_not() -> Self::Builder;
}
#[doc(hidden)]
#[derive(Copy, Clone, Debug, Default)]
pub struct EmptyPropsBuilder {}
impl EmptyPropsBuilder {
pub fn build(self) {}
}
impl<P: Props> PropsOrNoPropsBuilder for P {
type Builder = <P as Props>::Builder;
fn builder_or_not() -> Self::Builder {
Self::builder()
}
}
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
type Builder = EmptyPropsBuilder;
fn builder_or_not() -> Self::Builder {
EmptyPropsBuilder {}
}
}
impl<F, R> Component<EmptyPropsBuilder> for F where F: FnOnce() -> R {}
impl<P, F, R> Component<P> for F
where
F: FnOnce(P) -> R,
P: Props,
{
}
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
_f: &impl Component<P>,
) -> <P as PropsOrNoPropsBuilder>::Builder {
<P as PropsOrNoPropsBuilder>::builder_or_not()
}
pub fn component_view<P, T>(f: impl ComponentConstructor<P, T>, props: P) -> T {
f.construct(props)
}
pub trait ComponentConstructor<P, T> {
fn construct(self, props: P) -> T;
}
impl<Func, T> ComponentConstructor<(), T> for Func
where
Func: FnOnce() -> T,
{
fn construct(self, (): ()) -> T {
(self)()
}
}
impl<Func, T, P> ComponentConstructor<P, T> for Func
where
Func: FnOnce(P) -> T,
P: PropsOrNoPropsBuilder,
{
fn construct(self, props: P) -> T {
(self)(props)
}
}

View file

@ -0,0 +1,63 @@
use std::{hash::Hash, marker::PhantomData};
use tachy_maccy::component;
use tachydom::{
renderer::Renderer,
view::{keyed::keyed, RenderHtml},
};
#[component]
pub fn For<Rndr, IF, I, T, EF, N, KF, K>(
/// Items over which the component should iterate.
each: IF,
/// A key function that will be applied to each item.
key: KF,
/// A function that takes the item, and returns the view that will be displayed for each item.
children: EF,
#[prop(optional)] _rndr: PhantomData<Rndr>,
) -> impl RenderHtml<Rndr>
where
IF: Fn() -> I + 'static,
I: IntoIterator<Item = T>,
EF: Fn(T) -> N + Clone + 'static,
N: RenderHtml<Rndr> + 'static,
KF: Fn(&T) -> K + Clone + 'static,
K: Eq + Hash + 'static,
T: 'static,
Rndr: Renderer + 'static,
Rndr::Node: Clone,
Rndr::Element: Clone,
{
move || keyed(each(), key.clone(), children.clone())
}
#[cfg(test)]
mod tests {
use crate::For;
use tachy_maccy::view;
use tachy_reaccy::{signal::RwSignal, signal_traits::SignalGet};
use tachydom::{
html::element::HtmlElement, prelude::ElementChild,
renderer::mock_dom::MockDom, view::Render,
};
#[test]
fn creates_list() {
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
let list: HtmlElement<_, _, _, MockDom> = view! {
<ol>
<For
each=move || values.get()
key=|i| *i
let:i
>
<li>{i}</li>
</For>
</ol>
};
let list = list.build();
assert_eq!(
list.el.to_debug_html(),
"<ol><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ol>"
);
}
}

View file

@ -0,0 +1,8 @@
(function (pkg_path, output_name, wasm_output_name) {
import(`/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
});
})
})

View file

@ -0,0 +1,26 @@
(function (pkg_path, output_name, wasm_output_name) {
function idle(c) {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(c);
} else {
c();
}
}
idle(() => {
import(`/${pkg_path}/${output_name}.js`)
.then(mod => {
mod.default(`/${pkg_path}/${wasm_output_name}.wasm`).then(() => {
mod.hydrate();
for (let e of document.querySelectorAll("leptos-island")) {
const l = e.dataset.component;
const islandFn = mod["_island_" + l];
if (islandFn) {
islandFn(e);
} else {
console.warn(`Could not find WASM function for the island ${l}.`);
}
}
});
})
});
})

View file

@ -0,0 +1,57 @@
#![allow(clippy::needless_lifetimes)]
use crate::prelude::*;
use leptos_config::LeptosOptions;
use tachydom::view::RenderHtml;
#[component]
pub fn AutoReload<'a>(
#[prop(optional)] disable_watch: bool,
#[prop(optional)] nonce: Option<&'a str>,
options: LeptosOptions,
) -> impl RenderHtml<Dom> + 'a {
(!disable_watch && std::env::var("LEPTOS_WATCH").is_ok()).then(|| {
let reload_port = match options.reload_external_port {
Some(val) => val,
None => options.reload_port,
};
let protocol = match options.reload_ws_protocol {
leptos_config::ReloadWSProtocol::WS => "'ws://'",
leptos_config::ReloadWSProtocol::WSS => "'wss://'",
};
let script = include_str!("reload_script.js");
view! {
<script crossorigin=nonce>
{format!("{script}({reload_port:?}, {protocol})")}
</script>
}
})
}
#[component]
pub fn HydrationScripts(
options: LeptosOptions,
#[prop(optional)] islands: bool,
) -> impl RenderHtml<Dom> {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
wasm_output_name.push_str("_bg");
}
let nonce = None::<String>; // use_nonce(); // TODO
let script = if islands {
include_str!("./island_script.js")
} else {
include_str!("./hydration_script.js")
};
view! {
<link rel="modulepreload" href=format!("/{pkg_path}/{output_name}.js") nonce=nonce.clone()/>
<link rel="preload" href=format!("/{pkg_path}/{wasm_output_name}.wasm") r#as="fetch" r#type="application/wasm" crossorigin=nonce.clone().unwrap_or_default()/>
<script type="module" nonce=nonce>
{format!("{script}({pkg_path:?}, {output_name:?}, {wasm_output_name:?})")}
</script>
}
}

View file

@ -0,0 +1,23 @@
(function (reload_port, protocol) {
let host = window.location.hostname;
let ws = new WebSocket(`${protocol}${host}:${reload_port}/live_reload`);
ws.onmessage = (ev) => {
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
found = true;
}
});
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`);
};
if(msg.view) {
patch(msg.view);
}
};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
})

View file

@ -0,0 +1,28 @@
use crate::children::{ChildrenFn, ViewFn};
use tachy_maccy::component;
use tachy_reaccy::{memo::ArcMemo, signal_traits::SignalGet};
use tachydom::{
renderer::dom::Dom,
view::{either::Either, RenderHtml},
};
#[component]
pub fn Show<W>(
/// The children will be shown whenever the condition in the `when` closure returns `true`.
children: ChildrenFn,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false. By default this is the empty view.
#[prop(optional, into)]
fallback: ViewFn,
) -> impl RenderHtml<Dom>
where
W: Fn() -> bool + Send + Sync + 'static,
{
let memoized_when = ArcMemo::new(move |_| when());
move || match memoized_when.get() {
true => Either::Left(children()),
false => Either::Right(fallback.run()),
}
}

View file

@ -81,6 +81,8 @@ pub mod owner;
pub mod signal; pub mod signal;
pub mod traits; pub mod traits;
pub use graph::untrack;
#[cfg(feature = "nightly")] #[cfg(feature = "nightly")]
mod nightly; mod nightly;

View file

@ -26,3 +26,13 @@ pub fn arc_signal<T>(value: T) -> (ArcReadSignal<T>, ArcWriteSignal<T>) {
pub fn signal<T: Send + Sync>(value: T) -> (ReadSignal<T>, WriteSignal<T>) { pub fn signal<T: Send + Sync>(value: T) -> (ReadSignal<T>, WriteSignal<T>) {
RwSignal::new(value).split() RwSignal::new(value).split()
} }
#[inline(always)]
#[track_caller]
#[deprecated = "This function is being renamed to `signal()` to conform to \
Rust idioms."]
pub fn create_signal<T: Send + Sync>(
value: T,
) -> (ReadSignal<T>, WriteSignal<T>) {
signal(value)
}

View file

@ -0,0 +1,46 @@
//! Implements the [`Render`] and [`RenderHtml`] traits for signal guard types.
use crate::{prelude::RenderHtml, renderer::Renderer, view::Render};
use reactive_graph::signal::SignalReadGuard;
impl<T, Rndr> Render<Rndr> for SignalReadGuard<T>
where
T: PartialEq + Clone + Render<Rndr>,
Rndr: Renderer,
{
type State = T::State;
fn build(self) -> Self::State {
todo!()
}
fn rebuild(self, state: &mut Self::State) {
todo!()
}
}
impl<T, Rndr> RenderHtml<Rndr> for SignalReadGuard<T>
where
T: PartialEq + Clone + RenderHtml<Rndr>,
Rndr: Renderer,
Rndr::Element: Clone,
Rndr::Node: Clone,
{
const MIN_LENGTH: usize = T::MIN_LENGTH;
fn to_html_with_buf(
self,
buf: &mut String,
position: &mut crate::view::Position,
) {
todo!()
}
fn hydrate<const FROM_SERVER: bool>(
self,
cursor: &crate::hydration::Cursor<Rndr>,
position: &crate::view::PositionState,
) -> Self::State {
todo!()
}
}

View file

@ -5,14 +5,14 @@ use crate::{
renderer::{DomRenderer, Renderer}, renderer::{DomRenderer, Renderer},
ssr::StreamBuilder, ssr::StreamBuilder,
view::{ view::{
FallibleRender, InfallibleRender, Mountable, Position, PositionState, InfallibleRender, Mountable, Position, PositionState, Render,
Render, RenderHtml, ToTemplate, RenderHtml, ToTemplate,
}, },
}; };
use reactive_graph::{computed::ScopedFuture, effect::RenderEffect}; use reactive_graph::{computed::ScopedFuture, effect::RenderEffect};
use std::mem;
mod class; mod class;
mod guards;
pub mod node_ref; pub mod node_ref;
mod style; mod style;