diff --git a/packages/core-macro/examples/html_test.rs b/packages/core-macro/examples/html_test.rs new file mode 100644 index 000000000..c8e33ef8b --- /dev/null +++ b/packages/core-macro/examples/html_test.rs @@ -0,0 +1,8 @@ +fn main() { + // use dioxus_core_macro::*; + // let r = html! { + //
+ // "hello world" + //
+ // }; +} diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index b491d74df..d01a27f35 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -182,17 +182,158 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// ``` #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { - match syn::parse::>(s) { + match syn::parse::>(s) { Err(e) => e.to_compile_error().into(), Ok(s) => s.to_token_stream().into(), } } /// The html! macro makes it easy for developers to write jsx-style markup in their components. -/// We aim to keep functional parity with html templates. +/// +/// ## Complete Reference Guide: +/// ``` +/// const Example: FC<()> = |(cx, props)|{ +/// let formatting = "formatting!"; +/// let formatting_tuple = ("a", "b"); +/// let lazy_fmt = format_args!("lazily formatted text"); +/// cx.render(html! { +///
+///
+///
+///

"Some text"

+///

"Some text with {formatting}"

+///

"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"

+///

+/// "Multiple" +/// "Text" +/// "Blocks" +/// "Use comments as separators in html" +///

+///
+///

"multiple"

+///

"nested"

+///

"elements"

+///
+///
+///

"Headers and attributes!"

+///
+///
+///
+///
+/// {rsx!(p { "More templating!" })} +/// {html!(

"Even HTML templating!!"

)} +/// {(0..10).map(|i| html!(
  • "{i}"
  • ))} +/// {{ +/// let data = std::collections::HashMap::<&'static str, &'static str>::new(); +/// // Iterators *should* have keys when you can provide them. +/// // Keys make your app run faster. Make sure your keys are stable, unique, and predictable. +/// // Using an "ID" associated with your data is a good idea. +/// data.into_iter().map(|(k, v)| rsx!(
  • "{v}"
  • )) +/// }} + +/// // Matching +/// // Matching will throw a Rust error about "no two closures are the same type" +/// // To fix this, call "render" method or use the "in" syntax to produce VNodes. +/// // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros) +/// {match true { +/// true => rsx!(cx,

    "Top text"

    ), +/// false => rsx!(cx,

    "Bottom text"

    ), +/// }} +/// +/// // Conditional rendering +/// // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals. +/// // You can convert a bool condition to rsx! with .then and .or +/// {true.then(|| html!(
    ))} +/// +/// // True conditions need to be rendered (same reasons as matching) +/// {if true { +/// html!(cx,

    "Top text"

    ) +/// } else { +/// html!(cx,

    "Bottom text"

    ) +/// }} +/// +/// // returning "None" is a bit noisy... but rare in practice +/// {None as Option<()>} +/// +/// // Use the Dioxus type-alias for less noise +/// {NONE_ELEMENT} +/// +/// // can also just use empty fragments +/// +/// +/// // Fragments let you insert groups of nodes without a parent. +/// // This lets you make components that insert elements as siblings without a container. +///
    "A"
    +/// +///
    "B"
    +///
    "C"
    +/// +/// "D" +/// +/// "heavily nested fragments is an antipattern" +/// "they cause Dioxus to do unnecessary work" +/// "don't use them carelessly if you can help it" +/// +/// +/// +/// // Components +/// // Can accept any paths +/// // Notice how you still get syntax highlighting and IDE support :) +/// +/// +/// +/// +/// // Can take properties +/// +/// +/// // Can take optional properties +/// +/// +/// // Can pass in props directly as an expression +/// {{ +/// let props = TallerProps {a: "hello"}; +/// html!() +/// }} +/// +/// // Spreading can also be overridden manually +/// +/// +/// // Can take children too! +/// +///
    "hello world!"
    +///
    +/// } +/// }) +/// }; +/// +/// mod baller { +/// use super::*; +/// pub struct BallerProps {} +/// +/// /// This component totally balls +/// pub fn Baller(cx: Context<()>) -> DomTree { +/// todo!() +/// } +/// } +/// +/// #[derive(Debug, PartialEq, Props)] +/// pub struct TallerProps { +/// a: &'static str, +/// } +/// +/// /// This component is taller than most :) +/// pub fn Taller(cx: Context) -> DomTree { +/// let b = true; +/// todo!() +/// } +/// ``` #[proc_macro] pub fn html(s: TokenStream) -> TokenStream { - match syn::parse::>(s) { + match syn::parse::>(s) { Err(e) => e.to_compile_error().into(), Ok(s) => s.to_token_stream().into(), } diff --git a/packages/core-macro/src/rsx/ambiguous.rs b/packages/core-macro/src/rsx/ambiguous.rs index 2fbfec7ed..cfa90fb10 100644 --- a/packages/core-macro/src/rsx/ambiguous.rs +++ b/packages/core-macro/src/rsx/ambiguous.rs @@ -26,7 +26,7 @@ impl Parse for AmbiguousElement { if input.peek(Token![::]) { return input .parse::>() - .map(|c| AmbiguousElement::Component(c)); + .map(AmbiguousElement::Component); } // If not an absolute path, then parse the ident and check if it's a valid tag @@ -35,7 +35,7 @@ impl Parse for AmbiguousElement { if pat.segments.len() > 1 { return input .parse::>() - .map(|c| AmbiguousElement::Component(c)); + .map(AmbiguousElement::Component); } } @@ -47,11 +47,11 @@ impl Parse for AmbiguousElement { if first_char.is_ascii_uppercase() { input .parse::>() - .map(|c| AmbiguousElement::Component(c)) + .map(AmbiguousElement::Component) } else { input .parse::>() - .map(|c| AmbiguousElement::Element(c)) + .map(AmbiguousElement::Element) } } else { Err(Error::new(input.span(), "Not a valid Html tag")) @@ -61,26 +61,11 @@ impl Parse for AmbiguousElement { impl Parse for AmbiguousElement { fn parse(input: ParseStream) -> Result { - // Try to parse as an absolute path and immediately defer to the componetn - if input.peek(Token![::]) { - return input - .parse::>() - .map(|c| AmbiguousElement::Component(c)); - } - - // If not an absolute path, then parse the ident and check if it's a valid tag - - if let Ok(pat) = input.fork().parse::() { - if pat.segments.len() > 1 { - return input - .parse::>() - .map(|c| AmbiguousElement::Component(c)); - } - } - - use syn::ext::IdentExt; - if let Ok(name) = input.fork().call(Ident::parse_any) { - let name_str = name.to_string(); + if input.peek(Token![<]) { + let forked = input.fork(); + forked.parse::().unwrap(); + let tag = forked.parse::()?; + let name_str = tag.to_string(); let first_char = name_str.chars().next().unwrap(); if first_char.is_ascii_uppercase() { @@ -95,6 +80,57 @@ impl Parse for AmbiguousElement { } else { Err(Error::new(input.span(), "Not a valid Html tag")) } + + // input.parse::]>()?; + + // let mut children = Vec::new(); + // while !input.peek(Token![<]) { + // children.push(input.parse::>()?); + // } + + // Ok(AmbiguousElement::Element(Element { + // name, + // key: todo!(), + // attributes: todo!(), + // listeners: todo!(), + // children, + // _is_static: todo!(), + // })) + + // // Try to parse as an absolute path and immediately defer to the componetn + // if input.peek(Token![::]) { + // return input + // .parse::>() + // .map(AmbiguousElement::Component); + // } + + // // If not an absolute path, then parse the ident and check if it's a valid tag + + // if let Ok(pat) = input.fork().parse::() { + // if pat.segments.len() > 1 { + // return input + // .parse::>() + // .map(AmbiguousElement::Component); + // } + // } + + // use syn::ext::IdentExt; + // if let Ok(name) = input.fork().call(Ident::parse_any) { + // let name_str = name.to_string(); + + // let first_char = name_str.chars().next().unwrap(); + // if first_char.is_ascii_uppercase() { + // input + // .parse::>() + // .map(AmbiguousElement::Component) + // } else { + // input + // .parse::>() + // .map(AmbiguousElement::Element) + // } + // } else { + // Err(Error::new(input.span(), "Not a valid Html tag")) + // } } } diff --git a/packages/core-macro/src/rsx/body.rs b/packages/core-macro/src/rsx/body.rs index e4fd35283..375e0344a 100644 --- a/packages/core-macro/src/rsx/body.rs +++ b/packages/core-macro/src/rsx/body.rs @@ -7,17 +7,16 @@ use syn::{ use super::*; -pub struct RsxBody { +pub struct CallBody { custom_context: Option, roots: Vec>, } /// The custom rusty variant of parsing rsx! -impl Parse for RsxBody { +impl Parse for CallBody { fn parse(input: ParseStream) -> Result { let custom_context = try_parse_custom_context(input)?; - let (_, roots, _) = - BodyParseConfig::::new_as_body().parse_component_body(&input)?; + let (_, roots, _) = BodyConfig::::new_call_body().parse_component_body(input)?; Ok(Self { custom_context, roots, @@ -26,11 +25,12 @@ impl Parse for RsxBody { } /// The HTML variant of parsing rsx! -impl Parse for RsxBody { +impl Parse for CallBody { fn parse(input: ParseStream) -> Result { let custom_context = try_parse_custom_context(input)?; - let (_, roots, _) = - BodyParseConfig::::new_as_body().parse_component_body(&input)?; + + // parsing the contents is almost like parsing the inner of any element, but with no props + let (_, roots, _) = BodyConfig::::new_call_body().parse_component_body(input)?; Ok(Self { custom_context, roots, @@ -50,7 +50,7 @@ fn try_parse_custom_context(input: ParseStream) -> Result> { } /// Serialize the same way, regardless of flavor -impl ToTokens for RsxBody { +impl ToTokens for CallBody { fn to_tokens(&self, out_tokens: &mut TokenStream2) { let inner = if self.roots.len() == 1 { let inner = &self.roots[0]; diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 1984c791f..8b7d863dc 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -41,7 +41,7 @@ impl Parse for Component { let content: ParseBuffer; syn::braced!(content in stream); - let cfg: BodyParseConfig = BodyParseConfig { + let cfg: BodyConfig = BodyConfig { allow_children: true, allow_fields: true, allow_manual_props: true, @@ -59,19 +59,73 @@ impl Parse for Component { } impl Parse for Component { fn parse(stream: ParseStream) -> Result { + let _l_tok = stream.parse::()?; let name = syn::Path::parse_mod_style(stream)?; - // parse the guts - let content: ParseBuffer; - syn::braced!(content in stream); + let mut manual_props = None; - let cfg: BodyParseConfig = BodyParseConfig { - allow_children: true, - allow_fields: true, - allow_manual_props: true, - }; + let mut body: Vec> = vec![]; + let mut children: Vec> = vec![]; - let (body, children, manual_props) = cfg.parse_component_body(&content)?; + if stream.peek(Token![..]) { + stream.parse::()?; + manual_props = Some(stream.parse::()?); + } + + while !stream.peek(Token![>]) { + // self-closing + if stream.peek(Token![/]) { + stream.parse::()?; + stream.parse::]>()?; + + return Ok(Self { + name, + manual_props, + body, + children, + }); + } + body.push(stream.parse::>()?); + } + + stream.parse::]>()?; + + 'parsing: loop { + if stream.peek(Token![<]) { + break 'parsing; + } + + // [1] Break if empty + if stream.is_empty() { + break 'parsing; + } + + children.push(stream.parse::>()?); + } + + // closing element + stream.parse::()?; + stream.parse::()?; + let close = syn::Path::parse_mod_style(stream)?; + if close != name { + return Err(Error::new_spanned( + close, + "closing element does not match opening", + )); + } + stream.parse::]>()?; + + // // parse the guts + // let content: ParseBuffer; + // syn::braced!(content in stream); + + // let cfg: BodyConfig = BodyConfig { + // allow_children: true, + // allow_fields: true, + // allow_manual_props: true, + // }; + + // let (body, children, manual_props) = cfg.parse_component_body(&content)?; Ok(Self { name, @@ -82,15 +136,15 @@ impl Parse for Component { } } -pub struct BodyParseConfig { +pub struct BodyConfig { pub allow_fields: bool, pub allow_children: bool, pub allow_manual_props: bool, } -impl BodyParseConfig { +impl BodyConfig { /// The configuration to parse the root - pub fn new_as_body() -> Self { + pub fn new_call_body() -> Self { Self { allow_children: true, allow_fields: false, @@ -99,7 +153,7 @@ impl BodyParseConfig { } } -impl BodyParseConfig { +impl BodyConfig { // todo: unify this body parsing for both elements and components // both are style rather ad-hoc, though components are currently more configured pub fn parse_component_body( @@ -156,7 +210,7 @@ impl BodyParseConfig { Ok((body, children, manual_props)) } } -impl BodyParseConfig { +impl BodyConfig { // todo: unify this body parsing for both elements and components // both are style rather ad-hoc, though components are currently more configured pub fn parse_component_body( @@ -201,6 +255,7 @@ impl BodyParseConfig { "This item is not allowed to accept children.", )); } + children.push(content.parse::>()?); } diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs index 47a89d98e..aa7893092 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/core-macro/src/rsx/element.rs @@ -78,7 +78,7 @@ impl Parse for Element { let mut attributes: Vec> = vec![]; let mut listeners: Vec> = vec![]; - let children: Vec> = vec![]; + let mut children: Vec> = vec![]; let key = None; while !stream.peek(Token![>]) { @@ -115,7 +115,8 @@ impl Parse for Element { match name_str.as_str() { "style" => {} "key" => {} - "classes" | "namespace" | "ref" | _ => { + _ => { + // "classes" | "namespace" | "ref" | _ => { let ty = if stream.peek(LitStr) { let rawtext = stream.parse::().unwrap(); AttrType::BumpText(rawtext) @@ -138,37 +139,30 @@ impl Parse for Element { } stream.parse::]>()?; + 'parsing: loop { + if stream.peek(Token![<]) { + break 'parsing; + } + + // [1] Break if empty + if stream.is_empty() { + break 'parsing; + } + + children.push(stream.parse::>()?); + } + // closing element stream.parse::()?; stream.parse::()?; let close = Ident::parse_any(stream)?; - if close.to_string() != el_name.to_string() { + if close != el_name { return Err(Error::new_spanned( close, "closing element does not match opening", )); } stream.parse::]>()?; - // 'parsing: loop { - // // if stream.peek(Token![>]) {} - - // // // [1] Break if empty - // // if content.is_empty() { - // // break 'parsing; - // // } - - // if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - // parse_element_body( - // &content, - // &mut attributes, - // &mut listeners, - // &mut key, - // name.clone(), - // )?; - // } else { - // children.push(stream.parse::>()?); - // } - // } Ok(Self { key, diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs index 57f3d9bf7..a9c6b824a 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/core-macro/src/rsx/mod.rs @@ -17,6 +17,7 @@ mod component; mod element; mod fragment; mod node; +mod parse_rsx; // Re-export the namespaces into each other pub use ambiguous::*; diff --git a/packages/core-macro/src/rsx/parse_rsx.rs b/packages/core-macro/src/rsx/parse_rsx.rs new file mode 100644 index 000000000..4b5c55c59 --- /dev/null +++ b/packages/core-macro/src/rsx/parse_rsx.rs @@ -0,0 +1,2 @@ +struct CallBody {} +pub fn parse_callbody() {} diff --git a/packages/core/examples/syntax.rs b/packages/core/examples/syntax.rs new file mode 100644 index 000000000..5b9879045 --- /dev/null +++ b/packages/core/examples/syntax.rs @@ -0,0 +1,30 @@ +use dioxus::component::Component; +use dioxus::events::on::MouseEvent; +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_core_macro::*; +use dioxus_html as dioxus_elements; + +fn main() {} + +fn html_usage() { + let r = html! { +
    + "hello world" + "hello world" + "hello world" + "hello world" +
    + }; +} + +fn rsx_uage() { + let r = html! { + + "hello world" + "hello world" + "hello world" + "hello world" + + }; +}