Pārlūkot izejas kodu

feat: full html support

Jonathan Kelley 3 gadi atpakaļ
vecāks
revīzija
79503f15c5

+ 1 - 0
packages/core-macro/Cargo.toml

@@ -12,6 +12,7 @@ description = "Core macro for Dioxus Virtual DOM"
 proc-macro = true
 
 [dependencies]
+bumpalo = { version = "3.7.1", features = ["collections"] }
 once_cell = "1.8"
 proc-macro2 = { version = "1.0.6" }
 quote = "1.0"

+ 8 - 0
packages/core-macro/examples/html_test.rs

@@ -0,0 +1,8 @@
+fn main() {
+    // use dioxus_core_macro::*;
+    // let r = html! {
+    //     <div>
+    //         "hello world"
+    //     </div>
+    // };
+}

+ 144 - 3
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::<rsx::RsxBody<AS_RSX>>(s) {
+    match syn::parse::<rsx::CallBody<AS_RSX>>(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! {
+///         <div>
+///             <div />
+///             <div> </div>
+///             <h1>"Some text"</h1>
+///             <h1>"Some text with {formatting}"</h1>
+///             <h1>"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"</h1>
+///             <h2>
+///                 "Multiple"
+///                 "Text"
+///                 "Blocks"
+///                 "Use comments as separators in html"
+///             </h2>
+///             <div>
+///                 <h1>"multiple"</h1>
+///                 <h2>"nested"</h2>
+///                 <h3>"elements"</h3>
+///             </div>
+///             <div class="my special div">
+///                 <h1>"Headers and attributes!"</h1>
+///             </div>
+///             <div class: lazy_fmt, id=format_args!("attributes can be passed lazily with std::fmt::Arguments")>
+///                 <div class: {
+///                     const WORD: &str = "expressions";
+///                     format_args!("Arguments can be passed in through curly braces for complex {}", WORD)
+///                 } />
+///             </div>
+///             {rsx!(p { "More templating!" })}
+///             {html!(<p>"Even HTML templating!!"</p>)}
+///             {(0..10).map(|i| html!(<li>"{i}"</li>))}
+///             {{
+///                 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!(<li key="{k}"> "{v}" </li>))
+///             }}
+
+///             // 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, <h1>"Top text"</h1>),
+///                 false => rsx!(cx, <h1>"Bottom text"</h1>),
+///             }}
+///
+///             // 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!(<div />))}
+///
+///             // True conditions need to be rendered (same reasons as matching)
+///             {if true {
+///                 html!(cx, <h1>"Top text"</h1>)
+///             } else {
+///                 html!(cx, <h1>"Bottom text"</h1>)
+///             }}
+///
+///             // 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
+///             <Fragment />
+///
+///             // Fragments let you insert groups of nodes without a parent.
+///             // This lets you make components that insert elements as siblings without a container.
+///             <div> "A" </div>
+///             <Fragment>
+///                 <div> "B" </div>
+///                 <div> "C" </div>
+///                 <Fragment>
+///                     "D"
+///                     <Fragment>
+///                         "heavily nested fragments is an antipattern"
+///                         "they cause Dioxus to do unnecessary work"
+///                         "don't use them carelessly if you can help it"
+///                     </Fragment>
+///                 </Fragment
+///             </Fragment>
+///
+///             // Components
+///             // Can accept any paths
+///             // Notice how you still get syntax highlighting and IDE support :)
+///             <Baller />
+///             <baller::Baller />
+///             <crate::baller::Baller />
+///
+///             // Can take properties
+///             <Taller a="asd" />
+///
+///             // Can take optional properties
+///             <Taller a="asd" />
+///
+///             // Can pass in props directly as an expression
+///             {{
+///                 let props = TallerProps {a: "hello"};
+///                 html!(<Taller ..{props} />)
+///             }}
+///
+///             // Spreading can also be overridden manually
+///             <Taller {..TallerProps { a: "ballin!" }} a="not ballin!" />
+///
+///             // Can take children too!
+///             <Taller a="asd">
+///                 <div> "hello world!" </div>
+///             </Taller>
+///         }
+///     })
+/// };
+///
+/// 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<TallerProps>) -> DomTree {
+///     let b = true;
+///     todo!()
+/// }
+/// ```
 #[proc_macro]
 pub fn html(s: TokenStream) -> TokenStream {
-    match syn::parse::<rsx::RsxBody<AS_HTML>>(s) {
+    match syn::parse::<rsx::CallBody<AS_HTML>>(s) {
         Err(e) => e.to_compile_error().into(),
         Ok(s) => s.to_token_stream().into(),
     }

+ 60 - 24
packages/core-macro/src/rsx/ambiguous.rs

@@ -26,7 +26,7 @@ impl Parse for AmbiguousElement<AS_RSX> {
         if input.peek(Token![::]) {
             return input
                 .parse::<Component<AS_RSX>>()
-                .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<AS_RSX> {
             if pat.segments.len() > 1 {
                 return input
                     .parse::<Component<AS_RSX>>()
-                    .map(|c| AmbiguousElement::Component(c));
+                    .map(AmbiguousElement::Component);
             }
         }
 
@@ -47,11 +47,11 @@ impl Parse for AmbiguousElement<AS_RSX> {
             if first_char.is_ascii_uppercase() {
                 input
                     .parse::<Component<AS_RSX>>()
-                    .map(|c| AmbiguousElement::Component(c))
+                    .map(AmbiguousElement::Component)
             } else {
                 input
                     .parse::<Element<AS_RSX>>()
-                    .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<AS_RSX> {
 
 impl Parse for AmbiguousElement<AS_HTML> {
     fn parse(input: ParseStream) -> Result<Self> {
-        // Try to parse as an absolute path and immediately defer to the componetn
-        if input.peek(Token![::]) {
-            return input
-                .parse::<Component<AS_HTML>>()
-                .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::<syn::Path>() {
-            if pat.segments.len() > 1 {
-                return input
-                    .parse::<Component<AS_HTML>>()
-                    .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::<Token![<]>().unwrap();
+            let tag = forked.parse::<Ident>()?;
+            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<AS_HTML> {
         } else {
             Err(Error::new(input.span(), "Not a valid Html tag"))
         }
+
+        // input.parse::<Token![>]>()?;
+
+        // let mut children = Vec::new();
+        // while !input.peek(Token![<]) {
+        //     children.push(input.parse::<BodyNode<AS_HTML>>()?);
+        // }
+
+        // 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::<Component<AS_HTML>>()
+        //         .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::<syn::Path>() {
+        //     if pat.segments.len() > 1 {
+        //         return input
+        //             .parse::<Component<AS_HTML>>()
+        //             .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::<Component<AS_HTML>>()
+        //             .map(AmbiguousElement::Component)
+        //     } else {
+        //         input
+        //             .parse::<Element<AS_HTML>>()
+        //             .map(AmbiguousElement::Element)
+        //     }
+        // } else {
+        //     Err(Error::new(input.span(), "Not a valid Html tag"))
+        // }
     }
 }
 

+ 10 - 8
packages/core-macro/src/rsx/body.rs

@@ -1,3 +1,4 @@
+use bumpalo::Bump;
 use proc_macro2::TokenStream as TokenStream2;
 use quote::{quote, ToTokens, TokenStreamExt};
 use syn::{
@@ -7,17 +8,17 @@ use syn::{
 
 use super::*;
 
-pub struct RsxBody<const AS: HtmlOrRsx> {
+pub struct CallBody<const AS: HtmlOrRsx> {
     custom_context: Option<Ident>,
     roots: Vec<BodyNode<AS>>,
 }
 
 /// The custom rusty variant of parsing rsx!
-impl Parse for RsxBody<AS_RSX> {
+impl Parse for CallBody<AS_RSX> {
     fn parse(input: ParseStream) -> Result<Self> {
         let custom_context = try_parse_custom_context(input)?;
-        let (_, roots, _) =
-            BodyParseConfig::<AS_RSX>::new_as_body().parse_component_body(&input)?;
+        let arena = Bump::new();
+        let (_, roots, _) = BodyConfig::<AS_RSX>::new_call_body().parse_component_body(input)?;
         Ok(Self {
             custom_context,
             roots,
@@ -26,11 +27,12 @@ impl Parse for RsxBody<AS_RSX> {
 }
 
 /// The HTML variant of parsing rsx!
-impl Parse for RsxBody<AS_HTML> {
+impl Parse for CallBody<AS_HTML> {
     fn parse(input: ParseStream) -> Result<Self> {
         let custom_context = try_parse_custom_context(input)?;
-        let (_, roots, _) =
-            BodyParseConfig::<AS_HTML>::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::<AS_HTML>::new_call_body().parse_component_body(input)?;
         Ok(Self {
             custom_context,
             roots,
@@ -50,7 +52,7 @@ fn try_parse_custom_context(input: ParseStream) -> Result<Option<Ident>> {
 }
 
 /// Serialize the same way, regardless of flavor
-impl<const A: HtmlOrRsx> ToTokens for RsxBody<A> {
+impl<const A: HtmlOrRsx> ToTokens for CallBody<A> {
     fn to_tokens(&self, out_tokens: &mut TokenStream2) {
         let inner = if self.roots.len() == 1 {
             let inner = &self.roots[0];

+ 70 - 15
packages/core-macro/src/rsx/component.rs

@@ -41,7 +41,7 @@ impl Parse for Component<AS_RSX> {
         let content: ParseBuffer;
         syn::braced!(content in stream);
 
-        let cfg: BodyParseConfig<AS_RSX> = BodyParseConfig {
+        let cfg: BodyConfig<AS_RSX> = BodyConfig {
             allow_children: true,
             allow_fields: true,
             allow_manual_props: true,
@@ -59,19 +59,73 @@ impl Parse for Component<AS_RSX> {
 }
 impl Parse for Component<AS_HTML> {
     fn parse(stream: ParseStream) -> Result<Self> {
+        let _l_tok = stream.parse::<Token![<]>()?;
         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<AS_HTML> = BodyParseConfig {
-            allow_children: true,
-            allow_fields: true,
-            allow_manual_props: true,
-        };
+        let mut body: Vec<ComponentField<AS_HTML>> = vec![];
+        let mut children: Vec<BodyNode<AS_HTML>> = vec![];
 
-        let (body, children, manual_props) = cfg.parse_component_body(&content)?;
+        if stream.peek(Token![..]) {
+            stream.parse::<Token![..]>()?;
+            manual_props = Some(stream.parse::<Expr>()?);
+        }
+
+        while !stream.peek(Token![>]) {
+            // self-closing
+            if stream.peek(Token![/]) {
+                stream.parse::<Token![/]>()?;
+                stream.parse::<Token![>]>()?;
+
+                return Ok(Self {
+                    name,
+                    manual_props,
+                    body,
+                    children,
+                });
+            }
+            body.push(stream.parse::<ComponentField<AS_HTML>>()?);
+        }
+
+        stream.parse::<Token![>]>()?;
+
+        'parsing: loop {
+            if stream.peek(Token![<]) {
+                break 'parsing;
+            }
+
+            // [1] Break if empty
+            if stream.is_empty() {
+                break 'parsing;
+            }
+
+            children.push(stream.parse::<BodyNode<AS_HTML>>()?);
+        }
+
+        // closing element
+        stream.parse::<Token![<]>()?;
+        stream.parse::<Token![/]>()?;
+        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::<Token![>]>()?;
+
+        // // parse the guts
+        // let content: ParseBuffer;
+        // syn::braced!(content in stream);
+
+        // let cfg: BodyConfig<AS_HTML> = 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<AS_HTML> {
     }
 }
 
-pub struct BodyParseConfig<const AS: HtmlOrRsx> {
+pub struct BodyConfig<const AS: HtmlOrRsx> {
     pub allow_fields: bool,
     pub allow_children: bool,
     pub allow_manual_props: bool,
 }
 
-impl<const AS: HtmlOrRsx> BodyParseConfig<AS> {
+impl<const AS: HtmlOrRsx> BodyConfig<AS> {
     /// 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<const AS: HtmlOrRsx> BodyParseConfig<AS> {
     }
 }
 
-impl BodyParseConfig<AS_RSX> {
+impl BodyConfig<AS_RSX> {
     // 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<AS_RSX> {
         Ok((body, children, manual_props))
     }
 }
-impl BodyParseConfig<AS_HTML> {
+impl BodyConfig<AS_HTML> {
     // 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<AS_HTML> {
                         "This item is not allowed to accept children.",
                     ));
                 }
+
                 children.push(content.parse::<BodyNode<AS_HTML>>()?);
             }
 

+ 17 - 23
packages/core-macro/src/rsx/element.rs

@@ -78,7 +78,7 @@ impl Parse for Element<AS_HTML> {
 
         let mut attributes: Vec<ElementAttr<AS_HTML>> = vec![];
         let mut listeners: Vec<ElementAttr<AS_HTML>> = vec![];
-        let children: Vec<BodyNode<AS_HTML>> = vec![];
+        let mut children: Vec<BodyNode<AS_HTML>> = vec![];
         let key = None;
 
         while !stream.peek(Token![>]) {
@@ -115,7 +115,8 @@ impl Parse for Element<AS_HTML> {
                 match name_str.as_str() {
                     "style" => {}
                     "key" => {}
-                    "classes" | "namespace" | "ref" | _ => {
+                    _ => {
+                        // "classes" | "namespace" | "ref" | _ => {
                         let ty = if stream.peek(LitStr) {
                             let rawtext = stream.parse::<LitStr>().unwrap();
                             AttrType::BumpText(rawtext)
@@ -138,37 +139,30 @@ impl Parse for Element<AS_HTML> {
         }
         stream.parse::<Token![>]>()?;
 
+        'parsing: loop {
+            if stream.peek(Token![<]) {
+                break 'parsing;
+            }
+
+            // [1] Break if empty
+            if stream.is_empty() {
+                break 'parsing;
+            }
+
+            children.push(stream.parse::<BodyNode<AS_HTML>>()?);
+        }
+
         // closing element
         stream.parse::<Token![<]>()?;
         stream.parse::<Token![/]>()?;
         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::<Token![>]>()?;
-        // '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::<BodyNode<AS_HTML>>()?);
-        //     }
-        // }
 
         Ok(Self {
             key,

+ 1 - 0
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::*;

+ 2 - 0
packages/core-macro/src/rsx/parse_rsx.rs

@@ -0,0 +1,2 @@
+struct CallBody {}
+pub fn parse_callbody() {}

+ 30 - 0
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! {
+        <div>
+            "hello world"
+            "hello world"
+            "hello world"
+            "hello world"
+        </div>
+    };
+}
+
+fn rsx_uage() {
+    let r = html! {
+        <Fragment>
+            "hello world"
+            "hello world"
+            "hello world"
+            "hello world"
+        </Fragment>
+    };
+}