Feat: found a fast solution to hook state

This commit is contained in:
Jonathan Kelley 2021-02-06 22:19:56 -05:00
parent 62d4ad5878
commit 4d01436c3f
9 changed files with 759 additions and 85 deletions

View file

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

View file

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

View file

@ -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"

View file

@ -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;

View file

@ -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
)
}

View file

@ -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;
}

View file

@ -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,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<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(())?;
/// 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<F> 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<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) {}
}
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<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
// }
// 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(ctx: &Context<()>) -> impl DomTree {
// // todo: helper should be part of html! macro
// html! { <div> </div> }
// }
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(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<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 {}

View file

@ -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" }

View file

@ -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();
}
}