Selaa lähdekoodia

Feat: found a fast solution to hook state

Jonathan Kelley 4 vuotta sitten
vanhempi
commit
4d01436

+ 1 - 0
Cargo.toml

@@ -18,6 +18,7 @@ members = [
     "packages/cli",
     "examples",
     "packages/html-macro",
+    "packages/html-macro-2",
     #
     #
     #

+ 1 - 1
packages/core/.vscode/settings.json

@@ -1,3 +1,3 @@
 {
-    "rust-analyzer.inlayHints.enable": false
+    "rust-analyzer.inlayHints.enable": true
 }

+ 8 - 1
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"

+ 61 - 18
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<Props>) -> VNode<'a> {
+fn component<'a>(ctx: &'a Context<Props>) -> 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! {<div> </div>},
-            Err(_) => html! {<div> </div>},
+            Ok(data) => html! { <div> </div>},
+            Err(_) => html! { <div> </div>},
         }
     });
 
-    ctx.view(html! {
-        <div>
-            // <h1> "Products" </h1>
-            // // Subnodes can even be suspended
-            // // When completely rendered, they won't cause the component itself to re-render, just their slot
-            // <p> { product_list } </p>
-        </div>
-    })
+    todo!()
+    // ctx.view(html! {
+    //     <div>
+    //         // <h1> "Products" </h1>
+    //         // // Subnodes can even be suspended
+    //         // // When completely rendered, they won't cause the component itself to re-render, just their slot
+    //         // <p> { product_list } </p>
+    //     </div>
+    // })
 }
 
-fn BuilderComp(ctx: Context<Props>) -> 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<Props>) -> 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<Props>) -> VNode {
+fn FullySuspended<'a>(ctx: &'a Context<Props>) -> VNode<'a> {
     ctx.suspend(async {
         let i: i32 = 0;
 

+ 43 - 0
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
+    )
+}

+ 13 - 1
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;
 }

+ 239 - 68
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! {<div> "hello world" </div>}
+function Component(ctx: Context<()>) -> VNode {
+    ctx.view(html! {<div> "hello world" </div>})
 }
 ```
 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! {<div> "hello world" </div>}
+    ctx.view(html! {<div> "hello world" </div>})
 }
 ```
 
@@ -50,71 +50,153 @@ 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<P: Properties> {
     /// 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<Scope>,
 
+    base_scope: Index,
+
     /// Components generate lifecycle events
     event_queue: Vec<LifecycleEvent>,
 
     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<P: Properties + 'static> VirtualDom<P> {
     /// 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<P: Properties>(root: FC<P>) -> Self {
+    pub fn new_with_props(root: FC<P>, 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(())?;
+
+        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
+            }
+
+            // The parent for this component generated new props and the component needs update
+            LifecycleType::PropsChanged {} => {}
 
-/// 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;
+            // 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<T: Properties>(&self) -> Context<T> {
+        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) {}
 }
 
-/// 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<F> DomTree for F
-where
-    F: FnOnce(&Bump) -> VNode,
-{
-    fn render(self, b: &Bump) -> VNode {
-        self(b)
+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
 /// We automatically implement Component for FC<T>
@@ -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>(b: &'a Bump, r: FC<P>) -> 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<'a>(ctx: &'a Context<()>) -> VNode<'a> {
+        // todo: helper should be part of html! macro
+        todo!()
+        // ctx.view(|bump| html! {bump,  <div> </div> })
+    }
+
+    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();
 
-    // // Make sure this function builds properly.
-    // fn test_static_fn<'a, P: Properties, F: DomTree>(b: &'a Bump, r: &FC<P, F>) -> 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
-    // }
-
-    // fn test_component(ctx: &Context<()>) -> impl DomTree {
-    //     // todo: helper should be part of html! macro
-    //     html! { <div> </div> }
-    // }
-
-    // fn test_component2(ctx: &Context<()>) -> impl DomTree {
-    //     __domtree_helper(move |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();
-
-    //     let g: FC<_, _> = test_component;
-    //     let nodes0 = test_static_fn(&b, &g);
-    //     // Happiness! The VNodes are now allocated onto the bump vdom
-
-    //     let g: FC<_, _> = test_component2;
-    //     let nodes1 = test_static_fn(&b, &g);
-    // }
+        // Happiness! The VNodes are now allocated onto the bump vdom
+        let nodes0 = test_static_fn(&b, test_component);
+
+        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<OLDHookState>,
+    props_type: TypeId,
 }
 
 impl Scope {
-    fn new<T>() -> Self {
+    // create a new scope from a function
+    fn new<T: 'static>(f: FC<T>) -> Self {
+        // Capture the props type
+        let props_type = TypeId::of::<T>();
+
+        // 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<Hook>,
+    pub hooks: RefCell<Vec<*mut Hook>>,
+    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<VNode> {
         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<dyn std::any::Any> = 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::<InternalHookState>()
+            .unwrap();
+
+        runner(internal_state)
+    }
 }
+
+pub struct Hook {
+    state: Box<dyn std::any::Any>,
+}
+
+impl Hook {
+    fn new(state: Box<dyn std::any::Any>) -> Self {
+        Self { state }
+    }
+}
+
+/// A CallbackEvent wraps any event returned from the renderer's event system.
+pub struct CallbackEvent {}
+
+pub struct EventListener {}

+ 17 - 0
packages/html-macro-2/Cargo.toml

@@ -0,0 +1,17 @@
+[package]
+name = "dodrio-derive"
+version = "0.1.0"
+authors = ["Richard Dodd <richard.o.dodd@gmail.com>"]
+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" }

+ 380 - 0
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<Self> {
+        let ctx: Ident = s.parse()?;
+        s.parse::<Token![,]>()?;
+        // 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<MaybeExpr<Node>> = vec![nodes_toks.parse()?];
+            while nodes_toks.peek(Token![,]) {
+                nodes_toks.parse::<Token![,]>()?;
+                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<MaybeExpr<Node>>);
+
+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<Self> {
+        Ok(if s.peek(Token![<]) {
+            Node::Element(s.parse()?)
+        } else {
+            Node::Text(s.parse()?)
+        })
+    }
+}
+
+struct Element {
+    name: Ident,
+    attrs: Vec<Attr>,
+    children: MaybeExpr<Vec<Node>>,
+}
+
+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<Self> {
+        s.parse::<Token![<]>()?;
+        let name = Ident::parse_any(s)?;
+        let mut attrs = vec![];
+        let mut children: Vec<Node> = vec![];
+
+        // keep looking for attributes
+        while !s.peek(Token![>]) {
+            // self-closing
+            if s.peek(Token![/]) {
+                s.parse::<Token![/]>()?;
+                s.parse::<Token![>]>()?;
+                return Ok(Self {
+                    name,
+                    attrs,
+                    children: MaybeExpr::Literal(vec![]),
+                });
+            }
+            attrs.push(s.parse()?);
+        }
+        s.parse::<Token![>]>()?;
+
+        // 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::<Token![<]>()?;
+        s.parse::<Token![/]>()?;
+        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::<Token![>]>()?;
+        Ok(Self {
+            name,
+            attrs,
+            children,
+        })
+    }
+}
+
+struct Attr {
+    name: Ident,
+    ty: AttrType,
+}
+
+impl Parse for Attr {
+    fn parse(s: ParseStream) -> Result<Self> {
+        let mut name = Ident::parse_any(s)?;
+        let name_str = name.to_string();
+        s.parse::<Token![=]>()?;
+        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<LitStr>),
+    Event(ExprClosure),
+    // todo Bool(MaybeExpr<LitBool>)
+}
+
+struct TextNode(MaybeExpr<LitStr>);
+
+impl Parse for TextNode {
+    fn parse(s: ParseStream) -> Result<Self> {
+        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<T> {
+    Literal(T),
+    Expr(Expr),
+}
+
+impl<T: Parse> Parse for MaybeExpr<T> {
+    fn parse(s: ParseStream) -> Result<Self> {
+        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<T>>
+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<U>(&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<super::HtmlRender> {
+        syn::parse_str(input)
+    }
+
+    #[test]
+    fn div() {
+        parse("bump, <div class=\"test\"/>").unwrap();
+    }
+
+    #[test]
+    fn nested() {
+        parse("bump, <div class=\"test\"><div />\"text\"</div>").unwrap();
+    }
+
+    #[test]
+    fn complex() {
+        parse(
+            "bump,
+            <section style={{
+                display: flex;
+                flex-direction: column;
+                max-width: 95%;
+            }} class=\"map-panel\">{contact_details}</section>
+        ",
+        )
+        .unwrap();
+    }
+}