diff --git a/Cargo.toml b/Cargo.toml index ee758c2ee..24c25ace4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "packages/cli", "examples", "packages/html-macro", + "packages/html-macro-2", # # # diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 80f51cff8..2f418701b 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } \ No newline at end of file diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 46400244e..dad4c75c8 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -10,6 +10,7 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# dodrio-derive = { path = "../html-macro-2", version = "0.1.0" } dioxus-html-macro = { path = "../html-macro", version = "0.1.0" } dioxus-core-macro = { path = "../core-macro" } # Backs some static data @@ -17,6 +18,12 @@ once_cell = "1.5.2" # Backs the scope creation and reutilization generational-arena = "0.2.8" - # Bumpalo backs the VNode creation bumpalo = { version = "3.6.0", features = ["collections"] } + +owning_ref = "0.4.1" + +# all the arenas 👿 +typed-arena = "2.0.1" +toolshed = "0.8.1" +id-arena = "2.2.1" diff --git a/packages/core/examples/macrosrc.rs b/packages/core/examples/macrosrc.rs index 455b489c9..84933f62a 100644 --- a/packages/core/examples/macrosrc.rs +++ b/packages/core/examples/macrosrc.rs @@ -1,41 +1,84 @@ #![allow(unused, non_upper_case_globals, non_snake_case)] use bumpalo::Bump; use dioxus_core::prelude::*; -use dioxus_core::{nodebuilder::*, virtual_dom::DomTree}; +use dioxus_core::{nodebuilder::*, virtual_dom::Properties}; use std::{collections::HashMap, future::Future, marker::PhantomData}; -fn main() {} +fn main() { + let mut vdom = VirtualDom::new_with_props( + component, + Props { + blah: false, + text: "blah", + }, + ); + + vdom.progress(); + + let somet = String::from("asd"); + let text = somet.as_str(); + + /* + this could be auto-generated via the macro + this props is allocated in this + but the component and props would like need to be cached + we could box this fn, abstracting away the props requirement and just keep the entrance and allocator requirement + How do we keep cached things around? + Need some sort of caching mechanism + + how do we enter into a childscope from a parent scope? + + Problems: + 1: Comp props need to be stored somewhere so we can re-evalute components when they receive updates + 2: Trees are not evaluated + + */ + let example_caller = move |ctx: &Bump| { + todo!() + // let p = Props { blah: true, text }; + // let c = Context { props: &p }; + // let r = component(&c); + }; + + // check the edit list +} // ~~~ Text shared between components via props can be done with lifetimes! ~~~ // Super duper efficient :) -struct Props { +struct Props<'src> { blah: bool, - text: String, + text: &'src str, +} +impl<'src> Properties for Props<'src> { + fn new() -> Self { + todo!() + } } -fn Component<'a>(ctx: &'a Context) -> VNode<'a> { +fn component<'a>(ctx: &'a Context) -> VNode<'a> { // Write asynchronous rendering code that immediately returns a "suspended" VNode // The concurrent API will then progress this component when the future finishes // You can suspend the entire component, or just parts of it let product_list = ctx.suspend(async { // Suspend the rendering that completes when the future is done match fetch_data().await { - Ok(data) => html! {
}, - Err(_) => html! {
}, + Ok(data) => html! {
}, + Err(_) => html! {
}, } }); - ctx.view(html! { -
- //

"Products"

- // // Subnodes can even be suspended - // // When completely rendered, they won't cause the component itself to re-render, just their slot - //

{ product_list }

-
- }) + todo!() + // ctx.view(html! { + //
+ // //

"Products"

+ // // // Subnodes can even be suspended + // // // When completely rendered, they won't cause the component itself to re-render, just their slot + // //

{ product_list }

+ //
+ // }) } -fn BuilderComp(ctx: Context) -> VNode { +fn BuilderComp<'a>(ctx: &'a Context<'a, Props>) -> VNode<'a> { // VNodes can be constructed via a builder or the html! macro // However, both of these are "lazy" - they need to be evaluated (aka, "viewed") // We can "view" them with Context for ultimate speed while inside components @@ -43,7 +86,7 @@ fn BuilderComp(ctx: Context) -> VNode { div(bump) .attr("class", "edit") .child(text("Hello")) - .child(text(ctx.props.text.as_str())) + .child(text(ctx.props.text)) .finish() }) } @@ -79,7 +122,7 @@ fn EffcComp(ctx: &Context, name: &str) -> VNode { }) } -fn FullySuspended(ctx: Context) -> VNode { +fn FullySuspended<'a>(ctx: &'a Context) -> VNode<'a> { ctx.suspend(async { let i: i32 = 0; diff --git a/packages/core/examples/sketch.rs b/packages/core/examples/sketch.rs new file mode 100644 index 000000000..3c9ad6cd2 --- /dev/null +++ b/packages/core/examples/sketch.rs @@ -0,0 +1,43 @@ +use bumpalo::Bump; +use dioxus_core::prelude::{Context, VNode}; +use std::{any::Any, cell::RefCell, rc::Rc}; +use std::{borrow::Borrow, sync::atomic::AtomicUsize}; +use typed_arena::Arena; + +fn main() { + let ar = Arena::new(); + + (0..5).for_each(|f| { + // Create the temporary context obect + let c = Context { + _p: std::marker::PhantomData {}, + props: (), + idx: 0.into(), + arena: &ar, + hooks: RefCell::new(Vec::new()), + }; + + component(c); + }); +} + +// we need to do something about props and context being borrowed from different sources.... +// kinda anooying +/// use_ref creates a new value when the component is created and then borrows that value on every render +fn component(ctx: Context<()>) { + (0..10).for_each(|f| { + let r = use_ref(&ctx, move || f); + assert_eq!(*r, f); + }); +} + +pub fn use_ref<'a, P, T: 'static>( + ctx: &'a Context<'a, P>, + initial_state_fn: impl FnOnce() -> T + 'static, +) -> &'a T { + ctx.use_hook( + || initial_state_fn(), // initializer + |state| state, // runner, borrows the internal value + |b| {}, // tear down + ) +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 5357c328a..4215e87ba 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -70,6 +70,10 @@ pub mod nodes; pub mod validation; pub mod virtual_dom; +pub mod builder { + pub use super::nodebuilder::*; +} + /// Re-export common types for ease of development use. /// Essential when working with the html! macro /// @@ -89,8 +93,16 @@ pub mod prelude { pub type VirtualNode<'a> = VNode<'a>; // Re-export from the macro crate - pub use dioxus_html_macro::html; + // pub use dodrio_derive::html; + + pub use bumpalo; + // pub use dioxus_html_macro::html; // Re-export the FC macro pub use dioxus_core_macro::fc; + pub use dioxus_html_macro::html; + + pub use crate as dioxus; + + pub use crate::nodebuilder as builder; } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index ccc971399..e76b14a25 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -7,8 +7,8 @@ the DomTree trait is simply an abstraction over a lazy dom builder, much like th This means we can accept DomTree anywhere as well as return it. All components therefore look like this: ```ignore -function Component(ctx: Context<()>) -> impl DomTree { - html! {
"hello world"
} +function Component(ctx: Context<()>) -> VNode { + ctx.view(html! {
"hello world"
}) } ``` It's not quite as sexy as statics, but there's only so much you can do. The goal is to get statics working with the FC macro, @@ -18,7 +18,7 @@ into its own lib (IE, lazy loading wasm chunks by function (exciting stuff!)) ```ignore #[fc] // gets translated into a function. static Component: FC = |ctx| { - html! {
"hello world"
} + ctx.view(html! {
"hello world"
}) } ``` @@ -50,70 +50,152 @@ A Context use crate::nodes::VNode; use crate::prelude::*; use bumpalo::Bump; -use generational_arena::Arena; -use std::future::Future; +use generational_arena::{Arena, Index}; +use std::{ + any::TypeId, + cell::{RefCell, UnsafeCell}, + future::Future, + sync::atomic::AtomicUsize, +}; /// An integrated virtual node system that progresses events and diffs UI trees. /// Differences are converted into patches which a renderer can use to draw the UI. -pub struct VirtualDom { +pub struct VirtualDom { /// All mounted components are arena allocated to make additions, removals, and references easy to work with /// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena. components: Arena, + base_scope: Index, + /// Components generate lifecycle events event_queue: Vec, buffers: [Bump; 2], + + selected_buf: u8, + + root_props: P, } -impl VirtualDom { +/// Implement VirtualDom with no props for components that initialize their state internal to the VDom rather than externally. +impl VirtualDom<()> { /// Create a new instance of the Dioxus Virtual Dom with no properties for the root component. /// /// This means that the root component must either consumes its own context, or statics are used to generate the page. /// The root component can access things like routing in its context. pub fn new(root: FC<()>) -> Self { - Self::new_with_props(root) + Self::new_with_props(root, ()) } +} +/// Implement the VirtualDom for any Properties +impl VirtualDom

{ /// Start a new VirtualDom instance with a dependent props. /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders. /// /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive /// to toss out the entire tree. - pub fn new_with_props(root: FC

) -> Self { + pub fn new_with_props(root: FC

, root_props: P) -> Self { + // 1. Create the buffers + // 2. Create the component arena + // 3. Create the base scope (can never be removed) + // 4. Create the lifecycle queue + // 5. Create the event queue + let buffers = [Bump::new(), Bump::new()]; + + // Arena allocate all the components + // This should make it *really* easy to store references in events and such + let mut components = Arena::new(); + + // Create a reference to the component in the arena + let base_scope = components.insert(Scope::new(root)); + + // Create an event queue with a mount for the base scope + let event_queue = vec![]; + Self { - components: Arena::new(), - event_queue: vec![], - buffers: [Bump::new(), Bump::new()], + components, + base_scope, + event_queue, + buffers, + root_props, + selected_buf: 0, } } /// Pop an event off the even queue and process it - pub fn progress_event() {} -} + pub fn progress(&mut self) -> Result<(), ()> { + let LifecycleEvent { index, event_type } = self.event_queue.pop().ok_or(())?; -/// The internal lifecycle event system is managed by these -/// All events need to be confused before swapping doms over -pub enum LifecycleEvent { - Add {}, -} + let scope = self.components.get(index).ok_or(())?; -/// Anything that takes a "bump" and returns VNodes is a "DomTree" -/// This is used as a "trait alias" for function return types to look less hair -pub trait DomTree { - fn render(self, b: &Bump) -> VNode; -} + match event_type { + // Component needs to be mounted to the virtual dom + LifecycleType::Mount {} => { + // todo! run the FC with the bump allocator + // Run it with its properties + } -/// Implement DomTree for the type returned by the html! macro. -/// This lets the caller of the static function evaluate the builder closure with its own bump. -/// It keeps components pretty and removes the need for the user to get too involved with allocation. -impl DomTree for F -where - F: FnOnce(&Bump) -> VNode, -{ - fn render(self, b: &Bump) -> VNode { - self(b) + // The parent for this component generated new props and the component needs update + LifecycleType::PropsChanged {} => {} + + // Component was successfully mounted to the dom + LifecycleType::Mounted {} => {} + + // Component was removed from the DOM + // Run any destructors and cleanup for the hooks and the dump the component + LifecycleType::Removed {} => { + let f = self.components.remove(index); + } + + // Component was moved around in the DomTree + // Doesn't generate any event but interesting to keep track of + LifecycleType::Moved {} => {} + + // Component was messaged via the internal subscription service + LifecycleType::Messaged => {} + } + + Ok(()) } + + /// Update the root props, causing a full event cycle + pub fn update_props(&mut self, new_props: P) {} + + /// Run through every event in the event queue until the events are empty. + /// Function is asynchronous to allow for async components to finish their work. + pub async fn progess_completely() {} + + /// Create a new context object for a given component and scope + fn new_context(&self) -> Context { + todo!() + } + + /// Stop writing to the current buffer and start writing to the new one. + /// This should be done inbetween CallbackEvent handling, but not between lifecycle events. + pub fn swap_buffers(&mut self) {} +} + +pub struct LifecycleEvent { + pub index: Index, + pub event_type: LifecycleType, +} +impl LifecycleEvent { + fn mount(index: Index) -> Self { + Self { + index, + event_type: LifecycleType::Mount, + } + } +} +/// The internal lifecycle event system is managed by these +pub enum LifecycleType { + Mount, + PropsChanged, + Mounted, + Removed, + Moved, + Messaged, } /// The `Component` trait refers to any struct or funciton that can be used as a component @@ -152,37 +234,38 @@ impl Properties for () { #[cfg(test)] mod fc_test { use super::*; + use crate::prelude::*; - // // Make sure this function builds properly. - // fn test_static_fn<'a, P: Properties, F: DomTree>(b: &'a Bump, r: &FC) -> VNode<'a> { - // let p = P::new(); // new props - // let c = Context { props: p }; // new context with props - // let g = r(&c); // calling function with context - // g.render(&b) // rendering closure with bump allocator - // } + // Make sure this function builds properly. + fn test_static_fn<'a, P: Properties>(b: &'a Bump, r: FC

) -> VNode<'a> { + todo!() + // let p = P::new(); // new props + // let c = Context { props: &p }; // new context with props + // let g = r(&c); // calling function with context + // g + } - // fn test_component(ctx: &Context<()>) -> impl DomTree { - // // todo: helper should be part of html! macro - // html! {

} - // } + fn test_component<'a>(ctx: &'a Context<()>) -> VNode<'a> { + // todo: helper should be part of html! macro + todo!() + // ctx.view(|bump| html! {bump,
}) + } - // fn test_component2(ctx: &Context<()>) -> impl DomTree { - // __domtree_helper(move |bump: &Bump| VNode::text("blah")) - // } + fn test_component2<'a>(ctx: &'a Context<()>) -> VNode<'a> { + ctx.view(|bump: &Bump| VNode::text("blah")) + } - // #[test] - // fn ensure_types_work() { - // // TODO: Get the whole casting thing to work properly. - // // For whatever reason, FC is not auto-implemented, depsite it being a static type - // let b = Bump::new(); + #[test] + fn ensure_types_work() { + // TODO: Get the whole casting thing to work properly. + // For whatever reason, FC is not auto-implemented, depsite it being a static type + let b = Bump::new(); - // let g: FC<_, _> = test_component; - // let nodes0 = test_static_fn(&b, &g); - // // Happiness! The VNodes are now allocated onto the bump vdom + // Happiness! The VNodes are now allocated onto the bump vdom + let nodes0 = test_static_fn(&b, test_component); - // let g: FC<_, _> = test_component2; - // let nodes1 = test_static_fn(&b, &g); - // } + let nodes1 = test_static_fn(&b, test_component2); + } } /// The Scope that wraps a functional component @@ -190,19 +273,30 @@ mod fc_test { /// The actualy contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently. pub struct Scope { hook_idx: i32, - hooks: Vec<()>, + hooks: Vec, + props_type: TypeId, } impl Scope { - fn new() -> Self { + // create a new scope from a function + fn new(f: FC) -> Self { + // Capture the props type + let props_type = TypeId::of::(); + + // Obscure the function Self { hook_idx: 0, hooks: vec![], + props_type, } } + + /// Create a new context and run the component with references from the Virtual Dom + /// This function downcasts the function pointer based on the stored props_type + fn run() {} } -pub struct HookState {} +pub struct OLDHookState {} /// Components in Dioxus use the "Context" object to interact with their lifecycle. /// This lets components schedule updates, integrate hooks, and expose their context via the context api. @@ -224,13 +318,16 @@ pub struct HookState {} /// ``` // todo: force lifetime of source into T as a valid lifetime too // it's definitely possible, just needs some more messing around -pub struct Context<'source, T> { +pub struct Context<'src, T> { /// Direct access to the properties used to create this component. - pub props: &'source T, + pub props: T, + pub idx: AtomicUsize, + pub arena: &'src typed_arena::Arena, + pub hooks: RefCell>, + pub _p: std::marker::PhantomData<&'src ()>, } impl<'a, T> Context<'a, T> { - // impl<'a, T> Context<'a, T> { /// Access the children elements passed into the component pub fn children(&self) -> Vec { todo!("Children API not yet implemented for component Context") @@ -271,4 +368,78 @@ impl<'a, T> Context<'a, T> { ) -> VNode<'a> { todo!() } + + /// use_hook provides a way to store data between renders for functional components. + pub fn use_hook<'comp, InternalHookState: 'static, Output: 'static>( + &'comp self, + // The closure that builds the hook state + initializer: impl FnOnce() -> InternalHookState, + // The closure that takes the hookstate and returns some value + runner: impl for<'b> FnOnce(&'comp mut InternalHookState) -> &'comp Output, + // The closure that cleans up whatever mess is left when the component gets torn down + // TODO: add this to the "clean up" group for when the component is dropped + tear_down: impl FnOnce(InternalHookState), + ) -> &'comp Output { + let raw_hook = { + let idx = self.idx.load(std::sync::atomic::Ordering::Relaxed); + + // Mutate hook list if necessary + let mut hooks = self.hooks.borrow_mut(); + + // Initialize the hook by allocating it in the typed arena. + // We get a reference from the arena which is owned by the component scope + // This is valid because "Context" is only valid while the scope is borrowed + if idx >= hooks.len() { + let new_state = initializer(); + let boxed_state: Box = Box::new(new_state); + let hook = self.arena.alloc(Hook::new(boxed_state)); + + // Push the raw pointer instead of the &mut + // A "poor man's OwningRef" + hooks.push(hook); + } + self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + + *hooks.get(idx).unwrap() + }; + + /* + ** UNSAFETY ALERT ** + Here, we dereference a raw pointer. Normally, we aren't guaranteed that this is okay. + + However, typed-arena gives a mutable reference to the stored data which is stable for any inserts + into the arena. During the first call of the function, we need to add the mutable reference given to use by + the arena into our list of hooks. The arena provides stability of the &mut references and is only deallocated + when the component itself is deallocated. + + This is okay because: + - The lifetime of the component arena is tied to the lifetime of these raw hooks + - Usage of the raw hooks is tied behind the Vec refcell + - Output is static, meaning it can't take a reference to the data + - We don't expose the raw hook pointer outside of the scope of use_hook + */ + let borrowed_hook: &'comp mut _ = unsafe { raw_hook.as_mut().unwrap() }; + + let internal_state = borrowed_hook + .state + .downcast_mut::() + .unwrap(); + + runner(internal_state) + } } + +pub struct Hook { + state: Box, +} + +impl Hook { + fn new(state: Box) -> Self { + Self { state } + } +} + +/// A CallbackEvent wraps any event returned from the renderer's event system. +pub struct CallbackEvent {} + +pub struct EventListener {} diff --git a/packages/html-macro-2/Cargo.toml b/packages/html-macro-2/Cargo.toml new file mode 100644 index 000000000..3cd9b0e51 --- /dev/null +++ b/packages/html-macro-2/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dodrio-derive" +version = "0.1.0" +authors = ["Richard Dodd "] +edition = "2018" + +[lib] +proc-macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +syn = "1.0.18" +quote = "1.0.3" +proc-macro-hack = "0.5.15" +proc-macro2 = "1.0.10" +style-shared = { git = "https://github.com/derekdreery/style" } diff --git a/packages/html-macro-2/src/lib.rs b/packages/html-macro-2/src/lib.rs new file mode 100644 index 000000000..e848ea2ee --- /dev/null +++ b/packages/html-macro-2/src/lib.rs @@ -0,0 +1,380 @@ +use ::{ + proc_macro::TokenStream, + proc_macro2::{Span, TokenStream as TokenStream2}, + proc_macro_hack::proc_macro_hack, + quote::{quote, ToTokens, TokenStreamExt}, + style_shared::Styles, + syn::{ + ext::IdentExt, + parse::{Parse, ParseStream}, + token, Error, Expr, ExprClosure, Ident, LitBool, LitStr, Path, Result, Token, + }, +}; + +#[proc_macro] +pub fn html(s: TokenStream) -> TokenStream { + let html: HtmlRender = match syn::parse(s) { + Ok(s) => s, + Err(e) => return e.to_compile_error().into(), + }; + html.to_token_stream().into() +} + +struct HtmlRender { + ctx: Ident, + kind: NodeOrList, +} + +impl Parse for HtmlRender { + fn parse(s: ParseStream) -> Result { + let ctx: Ident = s.parse()?; + s.parse::()?; + // if elements are in an array, return a bumpalo::collections::Vec rather than a Node. + let kind = if s.peek(token::Bracket) { + let nodes_toks; + syn::bracketed!(nodes_toks in s); + let mut nodes: Vec> = vec![nodes_toks.parse()?]; + while nodes_toks.peek(Token![,]) { + nodes_toks.parse::()?; + nodes.push(nodes_toks.parse()?); + } + NodeOrList::List(NodeList(nodes)) + } else { + NodeOrList::Node(s.parse()?) + }; + Ok(HtmlRender { ctx, kind }) + } +} + +impl ToTokens for HtmlRender { + fn to_tokens(&self, tokens: &mut TokenStream2) { + ToToksCtx::new(&self.ctx, &self.kind).to_tokens(tokens) + } +} + +enum NodeOrList { + Node(Node), + List(NodeList), +} + +impl ToTokens for ToToksCtx<'_, &NodeOrList> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self.inner { + NodeOrList::Node(node) => self.recurse(node).to_tokens(tokens), + NodeOrList::List(list) => self.recurse(list).to_tokens(tokens), + } + } +} + +struct NodeList(Vec>); + +impl ToTokens for ToToksCtx<'_, &NodeList> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ctx = &self.ctx; + let nodes = self.inner.0.iter().map(|node| self.recurse(node)); + tokens.append_all(quote! { + dioxus::bumpalo::vec![in #ctx.bump; + #(#nodes),* + ] + }); + } +} + +enum Node { + Element(Element), + Text(TextNode), +} + +impl ToTokens for ToToksCtx<'_, &Node> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match &self.inner { + Node::Element(el) => self.recurse(el).to_tokens(tokens), + Node::Text(txt) => self.recurse(txt).to_tokens(tokens), + } + } +} + +impl Node { + fn peek(s: ParseStream) -> bool { + (s.peek(Token![<]) && !s.peek2(Token![/])) || s.peek(token::Brace) || s.peek(LitStr) + } +} + +impl Parse for Node { + fn parse(s: ParseStream) -> Result { + Ok(if s.peek(Token![<]) { + Node::Element(s.parse()?) + } else { + Node::Text(s.parse()?) + }) + } +} + +struct Element { + name: Ident, + attrs: Vec, + children: MaybeExpr>, +} + +impl ToTokens for ToToksCtx<'_, &Element> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ctx = self.ctx; + let name = &self.inner.name; + tokens.append_all(quote! { + dioxus::builder::#name(&#ctx) + }); + for attr in self.inner.attrs.iter() { + self.recurse(attr).to_tokens(tokens); + } + match &self.inner.children { + MaybeExpr::Expr(expr) => tokens.append_all(quote! { + .children(#expr) + }), + MaybeExpr::Literal(nodes) => { + let mut children = nodes.iter(); + if let Some(child) = children.next() { + let mut inner_toks = TokenStream2::new(); + self.recurse(child).to_tokens(&mut inner_toks); + while let Some(child) = children.next() { + quote!(,).to_tokens(&mut inner_toks); + self.recurse(child).to_tokens(&mut inner_toks); + } + tokens.append_all(quote! { + .children([#inner_toks]) + }); + } + } + } + tokens.append_all(quote! { + .finish() + }); + } +} + +impl Parse for Element { + fn parse(s: ParseStream) -> Result { + s.parse::()?; + let name = Ident::parse_any(s)?; + let mut attrs = vec![]; + let mut children: Vec = vec![]; + + // keep looking for attributes + while !s.peek(Token![>]) { + // self-closing + if s.peek(Token![/]) { + s.parse::()?; + s.parse::]>()?; + return Ok(Self { + name, + attrs, + children: MaybeExpr::Literal(vec![]), + }); + } + attrs.push(s.parse()?); + } + s.parse::]>()?; + + // Contents of an element can either be a brace (in which case we just copy verbatim), or a + // sequence of nodes. + let children = if s.peek(token::Brace) { + // expr + let content; + syn::braced!(content in s); + MaybeExpr::Expr(content.parse()?) + } else { + // nodes + let mut children = vec![]; + while !(s.peek(Token![<]) && s.peek2(Token![/])) { + children.push(s.parse()?); + } + MaybeExpr::Literal(children) + }; + + // closing element + s.parse::()?; + s.parse::()?; + let close = Ident::parse_any(s)?; + if close.to_string() != name.to_string() { + return Err(Error::new_spanned( + close, + "closing element does not match opening", + )); + } + s.parse::]>()?; + Ok(Self { + name, + attrs, + children, + }) + } +} + +struct Attr { + name: Ident, + ty: AttrType, +} + +impl Parse for Attr { + fn parse(s: ParseStream) -> Result { + let mut name = Ident::parse_any(s)?; + let name_str = name.to_string(); + s.parse::()?; + let ty = if name_str.starts_with("on") { + // remove the "on" bit + name = Ident::new(&name_str.trim_start_matches("on"), name.span()); + let content; + syn::braced!(content in s); + AttrType::Event(content.parse()?) + } else { + let lit_str = if name_str == "style" && s.peek(token::Brace) { + // special-case to deal with literal styles. + let outer; + syn::braced!(outer in s); + // double brace for inline style. + if outer.peek(token::Brace) { + let inner; + syn::braced!(inner in outer); + let styles: Styles = inner.parse()?; + MaybeExpr::Literal(LitStr::new(&styles.to_string(), Span::call_site())) + } else { + // just parse as an expression + MaybeExpr::Expr(outer.parse()?) + } + } else { + s.parse()? + }; + AttrType::Value(lit_str) + }; + Ok(Attr { name, ty }) + } +} + +impl ToTokens for ToToksCtx<'_, &Attr> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = self.inner.name.to_string(); + let mut attr_stream = TokenStream2::new(); + match &self.inner.ty { + AttrType::Value(value) => { + let value = self.recurse(value); + tokens.append_all(quote! { + .attr(#name, #value) + }); + } + AttrType::Event(event) => { + tokens.append_all(quote! { + .on(#name, #event) + }); + } + } + } +} + +enum AttrType { + Value(MaybeExpr), + Event(ExprClosure), + // todo Bool(MaybeExpr) +} + +struct TextNode(MaybeExpr); + +impl Parse for TextNode { + fn parse(s: ParseStream) -> Result { + Ok(Self(s.parse()?)) + } +} + +impl ToTokens for ToToksCtx<'_, &TextNode> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let mut token_stream = TokenStream2::new(); + self.recurse(&self.inner.0).to_tokens(&mut token_stream); + tokens.append_all(quote! { + dioxus::builder::text(#token_stream) + }); + } +} + +enum MaybeExpr { + Literal(T), + Expr(Expr), +} + +impl Parse for MaybeExpr { + fn parse(s: ParseStream) -> Result { + if s.peek(token::Brace) { + let content; + syn::braced!(content in s); + Ok(MaybeExpr::Expr(content.parse()?)) + } else { + Ok(MaybeExpr::Literal(s.parse()?)) + } + } +} + +impl<'a, T> ToTokens for ToToksCtx<'a, &'a MaybeExpr> +where + T: 'a, + ToToksCtx<'a, &'a T>: ToTokens, +{ + fn to_tokens(&self, tokens: &mut TokenStream2) { + match &self.inner { + MaybeExpr::Literal(v) => self.recurse(v).to_tokens(tokens), + MaybeExpr::Expr(expr) => expr.to_tokens(tokens), + } + } +} + +/// ToTokens context +struct ToToksCtx<'a, T> { + inner: T, + ctx: &'a Ident, +} + +impl<'a, T> ToToksCtx<'a, T> { + fn new(ctx: &'a Ident, inner: T) -> Self { + ToToksCtx { ctx, inner } + } + + fn recurse(&self, inner: U) -> ToToksCtx<'a, U> { + ToToksCtx { + ctx: &self.ctx, + inner, + } + } +} + +impl ToTokens for ToToksCtx<'_, &LitStr> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + self.inner.to_tokens(tokens) + } +} + +#[cfg(test)] +mod test { + fn parse(input: &str) -> super::Result { + syn::parse_str(input) + } + + #[test] + fn div() { + parse("bump,
").unwrap(); + } + + #[test] + fn nested() { + parse("bump,
\"text\"
").unwrap(); + } + + #[test] + fn complex() { + parse( + "bump, +
{contact_details}
+ ", + ) + .unwrap(); + } +}