Feat: remove old macro

This commit is contained in:
Jonathan Kelley 2021-03-13 19:11:06 -05:00
parent e66827ec92
commit 9d0727edab
43 changed files with 208 additions and 2306 deletions

View file

@ -16,7 +16,7 @@ members = [
# TODO @Jon, share the validation code
# "packages/web",
# "packages/hooks",
"packages/cli",
# "packages/cli",
# "examples",
# "packages/html-macro",
# "packages/html-macro-2",

View file

@ -28,12 +28,12 @@
# Project: Web_sys renderer (TBD)
- [x] WebSys edit interpreter
- [ ] Event system using async channels
- [x] Event system using async channels
- [ ] Implement conversion of all event types into synthetic events
# Project: String Render (TBD)
> Implement a light-weight string renderer with basic caching
- [ ] (SSR) Implement stateful 3rd party string renderer
- [x] (Macro) Make VText nodes automatically capture and format IE allow "Text is {blah}"
- [ ] (SSR) Implement stateful 3rd party string renderer
# Project: Hooks + Context + Subscriptions (TBD)
> Implement the foundations for state management
@ -41,12 +41,12 @@
- [x] Implement use_state (rewrite to use the use_reducer api like rei)
- [x] Implement use_ref
- [x] Implement use_context (only the API, not the state management solution)
- [ ] Implement use_reducer
- [ ] Implement use_reducer (WIP)
# Project: QOL
> Make it easier to write components
- [x] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures which get boxed)
- [ ] (Macro) Tweak component syntax to accept a new custom element
- [x] (Macro) Tweak event syntax to not be dependent on wasm32 target (just return regular closures which get boxed/alloced)
- [x] (Macro) Tweak component syntax to accept a new custom element
- [ ] (Macro) Allow components to specify their props as function args
# Project: Initial VDOM support (TBD)
@ -58,6 +58,6 @@
- [x] (Core) Introduce the VDOM and patch API for 3rd party renderers
- [x] (Core) Implement lifecycle
- [x] (Core) Implement an event system
- [ ] (Core) Implement child nodes, scope creation
- [x] (Core) Implement child nodes, scope creation
- [ ] (Core) Implement dirty tagging and compression

View file

@ -36,8 +36,5 @@ log = "0.4.14"
serde = { version = "1.0.123", features = ["derive"], optional=true }
# todo, remove
uuid = {version = "0.8.2", features=["serde", "v4"]}
[features]
default = []

View file

@ -1,10 +1,10 @@
use dioxus_core::virtual_dom::VirtualDom;
use dioxus_core::debug_renderer::DebugRenderer;
use dioxus_core::{component::Properties, prelude::*};
fn main() -> Result<(), ()> {
let p1 = SomeProps { name: "bob".into() };
let _vdom = VirtualDom::new_with_props(Example, p1);
let _vdom = DebugRenderer::new_with_props(Example, p1);
Ok(())
}
@ -18,9 +18,6 @@ static Example: FC<SomeProps> = |ctx, _props| {
ctx.render(html! {
<div>
<h1> "hello world!" </h1>
<h1> "hello world!" </h1>
<h1> "hello world!" </h1>
<h1> "hello world!" </h1>
</div>
})
};

View file

@ -3,7 +3,7 @@
//!
//! Renderers don't actually need to own the virtual dom (it's up to the implementer).
use crate::virtual_dom::VirtualDom;
use crate::{events::EventTrigger, virtual_dom::VirtualDom};
use crate::{innerlude::Result, prelude::*};
pub struct DebugRenderer {
@ -33,6 +33,10 @@ impl DebugRenderer {
Self { internal_dom: dom }
}
pub fn handle_event(&mut self, trigger: EventTrigger) -> Result<()> {
Ok(())
}
pub fn step(&mut self, machine: &mut DiffMachine) -> Result<()> {
Ok(())
}

View file

@ -36,13 +36,13 @@ use crate::{innerlude::*, scope::Scope};
use bumpalo::Bump;
use fxhash::{FxHashMap, FxHashSet};
use generational_arena::Arena;
use uuid::Uuid;
use std::{
cell::{RefCell, RefMut},
cmp::Ordering,
collections::VecDeque,
rc::{Rc, Weak},
sync::atomic::AtomicU32,
};
/// The DiffState is a cursor internal to the VirtualDOM's diffing algorithm that allows persistence of state while
@ -66,13 +66,33 @@ pub struct DiffMachine<'a> {
pub enum LifeCycleEvent<'a> {
Mount {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
id: Uuid,
scope: Weak<VCompAssociatedScope>,
id: u32,
},
PropsChanged {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
scope: Weak<VCompAssociatedScope>,
id: u32,
},
SameProps {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
scope: Weak<VCompAssociatedScope>,
id: u32,
},
Replace {
caller: Weak<dyn for<'r> Fn(Context<'r>) -> DomTree + 'a>,
old_scope: Weak<VCompAssociatedScope>,
new_scope: Weak<VCompAssociatedScope>,
id: u32,
},
PropsChanged,
SameProps,
Remove,
}
static COUNTER: AtomicU32 = AtomicU32::new(1);
fn get_id() -> u32 {
COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
impl<'a> DiffMachine<'a> {
pub fn new() -> Self {
Self {
@ -128,12 +148,27 @@ impl<'a> DiffMachine<'a> {
}
(VNode::Component(cold), VNode::Component(cnew)) => {
todo!("should not happen")
// if cold.caller_ref != cnew.caller_ref {
// // todo: create a stable addr
// self.lifecycle_events.push_back(LifeCycleEvent::Mount);
// return;
// }
// todo!("should not happen")
if cold.user_fc == cnew.user_fc {
// todo: create a stable addr
let caller = Rc::downgrade(&cnew.caller);
let id = cold.stable_addr.borrow().unwrap();
let scope = Rc::downgrade(&cold.ass_scope);
self.lifecycle_events
.push_back(LifeCycleEvent::PropsChanged { caller, id, scope });
} else {
let caller = Rc::downgrade(&cnew.caller);
let id = cold.stable_addr.borrow().unwrap();
let old_scope = Rc::downgrade(&cold.ass_scope);
let new_scope = Rc::downgrade(&cnew.ass_scope);
self.lifecycle_events.push_back(LifeCycleEvent::Replace {
caller,
id,
old_scope,
new_scope,
});
}
// let comparator = &cnew.comparator.0;
// let old_props = cold.raw_props.as_ref();
@ -151,23 +186,7 @@ impl<'a> DiffMachine<'a> {
// todo: knock out any listeners
(_, VNode::Component(_new)) => {
// self.lifecycle_events.push_back(LifeCycleEvent::Mount);
// we have no stable reference to work from
// push the lifecycle event onto the queue
// self.lifecycle_events
// .borrow_mut()
// .push_back(LifecycleEvent {
// event_type: LifecycleType::Mount {
// props: new.props,
// to: self.cur_idx,
// },
// });
// we need to associaote this new component with a scope...
// self.change_list.save_known_root(id)
self.change_list.commit_traversal();
// push the current
}
(VNode::Component(_old), _) => {
@ -242,36 +261,18 @@ impl<'a> DiffMachine<'a> {
/*
todo: integrate re-entrace
*/
// NodeKind::Cached(ref c) => {
// cached_roots.insert(c.id);
// let (node, template) = cached_set.get(c.id);
// if let Some(template) = template {
// create_with_template(
// cached_set,
// self.change_list,
// registry,
// template,
// node,
// cached_roots,
// );
// } else {
// create(cached_set, change_list, registry, node, cached_roots);
// }
// }
VNode::Component(component) => {
self.change_list
.create_text_node("placeholder for vcomponent");
let id = uuid::Uuid::new_v4();
let id = get_id();
*component.stable_addr.as_ref().borrow_mut() = Some(id);
self.change_list.save_known_root(id);
let caller = Rc::downgrade(&component.caller);
// let broken_caller: Weak<dyn Fn(Context) -> DomTree + 'static> =
// unsafe { std::mem::transmute(caller) };
let scope = Rc::downgrade(&component.ass_scope);
self.lifecycle_events.push_back(LifeCycleEvent::Mount {
caller,
// caller: broken_caller,
caller: Rc::downgrade(&component.caller),
id,
scope
});
}
VNode::Suspended => {

View file

@ -3,7 +3,7 @@
//! 3rd party renderers are responsible for forming this virtual events from events.
//! The goal here is to provide a consistent event interface across all renderer types.
//!
//! also... websys is kinda bad for rust-analyzer so we coerce for you automatically :)
//! also... websys integerates poorly with rust analyzer, so we handle that for you automatically.
use crate::innerlude::ScopeIdx;

View file

@ -88,8 +88,8 @@ impl<'a> VNode<'a> {
VNode::Suspended => {
todo!()
}
VNode::Component(_) => {
todo!()
VNode::Component(c) => {
c.key
}
}
}
@ -107,27 +107,6 @@ pub struct VElement<'a> {
pub attributes: &'a [Attribute<'a>],
pub children: &'a [VNode<'a>],
pub namespace: Option<&'a str>,
// The HTML tag, such as "div"
// pub tag: &'a str,
// pub tag_name: &'a str,
// pub attributes: &'a [Attribute<'a>],
// todo: hook up listeners
// pub listeners: &'a [Listener<'a>],
// / HTML attributes such as id, class, style, etc
// pub attrs: HashMap<String, String>,
// TODO: @JON Get this to not heap allocate, but rather borrow
// pub attrs: HashMap<&'static str, &'static str>,
// TODO @Jon, re-enable "events"
//
// /// Events that will get added to your real DOM element via `.addEventListener`
// pub events: Events,
// pub events: HashMap<String, ()>,
// /// The children of this `VNode`. So a <div> <em></em> </div> structure would
// /// have a parent div and one child, em.
// pub children: Vec<VNode>,
}
impl<'a> VElement<'a> {
@ -264,10 +243,14 @@ impl<'a> VText<'a> {
/// Virtual Components for custom user-defined components
/// Only supports the functional syntax
pub type StableScopeAddres = Rc<RefCell<Option<uuid::Uuid>>>;
pub type StableScopeAddres = RefCell<Option<u32>>;
pub type VCompAssociatedScope = RefCell<Option<ScopeIdx>>;
pub struct VComponent<'src> {
pub stable_addr: StableScopeAddres,
pub key: NodeKey,
pub stable_addr: Rc<StableScopeAddres>,
pub ass_scope: Rc<VCompAssociatedScope>,
pub comparator: Rc<dyn Fn(&VComponent) -> bool + 'src>,
pub caller: Rc<dyn for<'r> Fn(Context<'r>) -> DomTree + 'src>,
@ -276,28 +259,15 @@ pub struct VComponent<'src> {
raw_props: *const (),
// a pointer to the raw fn typ
caller_ref: *const (),
pub user_fc: *const (),
_p: PhantomData<&'src ()>,
}
impl std::fmt::Debug for VComponent<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
todo!()
Ok(())
}
}
// pub struct Comparator(pub Rc<dyn Fn(&VComponent) -> bool>);
// impl std::fmt::Debug for Comparator {
// fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Ok(())
// }
// }
// pub struct Caller(pub Rc<dyn Fn(Context) -> DomTree>);
// impl std::fmt::Debug for Caller {
// fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Ok(())
// }
// }
impl<'a> VComponent<'a> {
// use the type parameter on props creation and move it into a portable context
@ -318,7 +288,7 @@ impl<'a> VComponent<'a> {
// Therefore, if the render functions are identical (by address), then so will be
// props type paramter (because it is the same render function). Therefore, we can be
// sure
if caller_ref == other.caller_ref {
if caller_ref == other.user_fc {
let real_other = unsafe { &*(other.raw_props as *const _ as *const P) };
real_other == props
} else {
@ -333,7 +303,9 @@ impl<'a> VComponent<'a> {
};
Self {
caller_ref,
key: NodeKey::NONE,
ass_scope: Rc::new(RefCell::new(None)),
user_fc: caller_ref,
raw_props: props as *const P as *const _,
_p: PhantomData,
caller: Rc::new(caller),

View file

@ -94,12 +94,12 @@ pub enum Edit<'src_bump> {
// ========================================================
// push a known node on to the stack
TraverseToKnown {
node: uuid::Uuid,
node: u32,
// node: ScopeIdx,
},
// Add the current top of the stack to the known nodes
MakeKnown {
node: uuid::Uuid,
node: u32,
// node: ScopeIdx,
},
// Remove the current top of the stack from the known nodes
@ -383,32 +383,16 @@ impl<'a> EditMachine<'a> {
// debug!("emit: remove_event_listener({:?})", event);
}
pub fn save_known_root(&mut self, id: uuid::Uuid) {
// pub fn save_known_root(&mut self, id: ScopeIdx) {
// pub fn save_known_root(&mut self, id: ScopeIdx) {
pub fn save_known_root(&mut self, id: u32) {
log::debug!("emit: save_known_root({:?})", id);
self.emitter.push(Edit::MakeKnown { node: id })
}
pub fn load_known_root(&mut self, id: uuid::Uuid) {
pub fn load_known_root(&mut self, id: u32) {
log::debug!("emit: TraverseToKnown({:?})", id);
self.emitter.push(Edit::TraverseToKnown { node: id })
}
// pub fn save_template(&mut self, id: CacheId) {
// debug_assert!(self.traversal_is_committed());
// debug_assert!(!self.has_template(id));
// // debug!("emit: save_template({:?})", id);
// self.templates.insert(id);
// self.emitter.save_template(id.into());
// }
// pub fn push_template(&mut self, id: CacheId) {
// debug_assert!(self.traversal_is_committed());
// debug_assert!(self.has_template(id));
// // debug!("emit: push_template({:?})", id);
// self.emitter.push_template(id.into());
// }
}
// Keeps track of where we are moving in a DOM tree, and shortens traversal

View file

@ -96,10 +96,11 @@ impl Scope {
scope: self.myidx,
listeners: &self.listeners,
};
let caller = self.caller.upgrade().expect("Failed to get caller");
todo!()
// todo!()
// Note that the actual modification of the vnode head element occurs during this call
// let _: DomTree = (self.caller.0.as_ref())(ctx);
let _: DomTree = (caller.as_ref())(ctx);
// let _: DomTree = (self.raw_caller)(ctx, &self.props);
/*
@ -114,11 +115,13 @@ impl Scope {
- Public API cannot drop or destructure VNode
*/
// frame.head_node = node_slot
// .deref()
// .borrow_mut()
// .take()
// .expect("Viewing did not happen");
frame.head_node = node_slot.as_ref()
// .deref()
.borrow_mut()
.take()
.expect("Viewing did not happen");
Ok(())
}
// A safe wrapper around calling listeners

View file

@ -3,10 +3,7 @@
use crate::{error::Error, innerlude::*};
use crate::{patch::Edit, scope::Scope};
use generational_arena::Arena;
use std::{
any::TypeId,
rc::{Rc, Weak},
};
use std::{any::TypeId, borrow::Borrow, rc::{Rc, Weak}};
use thiserror::private::AsDynError;
// We actually allocate the properties for components in their parent's properties
@ -82,19 +79,19 @@ impl VirtualDom {
.ok_or_else(|| Error::FatalInternal("Acquring base component should never fail"))?;
component.run_scope()?;
let component = &*component;
// let component = &*component;
// // get raw pointer to the arena
// let very_unsafe_components = &mut self.components as *mut generational_arena::Arena<Scope>;
// get raw pointer to the arena
let very_unsafe_components = &mut self.components as *mut generational_arena::Arena<Scope>;
// {
// let component = self
// .components
// .get(self.base_scope)
// .ok_or_else(|| Error::FatalInternal("Acquring base component should never fail"))?
{
let component = self
.components
.get(self.base_scope)
.ok_or_else(|| Error::FatalInternal("Acquring base component should never fail"))?;
// diff_machine.diff_node(component.old_frame(), component.new_frame());
// }
diff_machine.diff_node(component.old_frame(), component.new_frame());
}
// let p = &mut self.components;
// chew down the the lifecycle events until all dirty nodes are computed
while let Some(event) = diff_machine.lifecycle_events.pop_front() {
@ -102,31 +99,48 @@ impl VirtualDom {
// A new component has been computed from the diffing algorithm
// create a new component in the arena, run it, move the diffing machine to this new spot, and then diff it
// this will flood the lifecycle queue with new updates
LifeCycleEvent::Mount { caller, id } => {
LifeCycleEvent::Mount { caller, id, scope } => {
log::debug!("Mounting a new component");
diff_machine.change_list.load_known_root(id);
// We're modifying the component arena while holding onto references into the assoiated bump arenas of its children
// those references are stable, even if the component arena moves around in memory, thanks to the bump arenas.
// However, there is no way to convey this to rust, so we need to use unsafe to pierce through the lifetime.
unsafe {
// let p = &mut *(very_unsafe_components);
// let p = &mut self.components;
let p = &mut *(very_unsafe_components);
// todo, hook up the parent/child indexes properly
let idx = self.components.insert_with(|f| Scope::new(caller, f, None));
let c = self.components.get_mut(idx).unwrap();
// let idx = p.insert_with(|f| Scope::new(caller, f, None));
// let c = p.get_mut(idx).unwrap();
c.run_scope();
// diff_machine.diff_node(c.old_frame(), c.new_frame());
let idx = p.insert_with(|f| Scope::new(caller, f, None));
let c = p.get_mut(idx).unwrap();
let real_scope = scope.upgrade().unwrap();
*real_scope.as_ref().borrow_mut() = Some(idx);
c.run_scope()?;
diff_machine.change_list.load_known_root(id);
diff_machine.diff_node(c.old_frame(), c.new_frame());
}
}
LifeCycleEvent::PropsChanged => {
//
LifeCycleEvent::PropsChanged { caller, id, scope } => {
let idx = scope.upgrade().unwrap().as_ref().borrow().unwrap();
// We're modifying the component arena while holding onto references into the assoiated bump arenas of its children
// those references are stable, even if the component arena moves around in memory, thanks to the bump arenas.
// However, there is no way to convey this to rust, so we need to use unsafe to pierce through the lifetime.
unsafe {
let p = &mut *(very_unsafe_components);
// todo, hook up the parent/child indexes properly
// let idx = p.insert_with(|f| Scope::new(caller, f, None));
let c = p.get_mut(idx).unwrap();
c.run_scope()?;
diff_machine.change_list.load_known_root(id);
diff_machine.diff_node(c.old_frame(), c.new_frame());
}
// break 'render;
}
LifeCycleEvent::SameProps => {
LifeCycleEvent::SameProps { caller, id, scope } => {
//
// break 'render;
}
@ -134,6 +148,7 @@ impl VirtualDom {
//
// break 'render;
}
LifeCycleEvent::Replace { caller, id, .. } => {}
}
// } else {
@ -176,13 +191,14 @@ impl VirtualDom {
.expect("Borrowing should not fail");
component.call_listener(event);
component.run_scope();
component.run_scope()?;
let mut diff_machine = DiffMachine::new();
// let mut diff_machine = DiffMachine::new(&self.diff_bump);
diff_machine.diff_node(component.old_frame(), component.new_frame());
// diff_machine.diff_node(component.old_frame(), component.new_frame());
Ok(diff_machine.consume())
// Ok(diff_machine.consume())
self.rebuild()
}
}

View file

@ -1,11 +0,0 @@
[package]
name = "html-macro-test"
version = "0.1.0"
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
edition = "2018"
[dependencies]
html-macro = {path = "../html-macro"}
virtual-dom-rs = { path = "../virtual-dom-rs" }
virtual-node = {path = "../virtual-node"}
trybuild = "1.0"

View file

@ -1,3 +0,0 @@
# html-macro-test
Unit tests for the `html!` macro

View file

@ -1,13 +0,0 @@
//! Tests for our html! procedural macro
//!
//! To run all tests in this library:
//!
//! cargo test --color=always --package html-macro-test --lib "" -- --nocapture
// #![feature(proc_macro_hygiene)]
// TODO: Deny warnings to ensure that the macro isn't creating any warnings.
// #![deny(warnings)]
#[cfg(test)]
mod tests;

View file

@ -1,3 +0,0 @@
mod all_tests;
mod text;
mod ui;

View file

@ -1,377 +0,0 @@
//! This is a catch-all module to place new tests as we go.
//!
//! Over time we'll pull tests out of here and organize them.
//!
//! For example - there is a `text_tests.rs` module where all of our text node related
//! tests live.
use html_macro::html;
use std::collections::HashMap;
use virtual_node::{IterableNodes, VElement, VText, View, VirtualNode};
struct HtmlMacroTest {
generated: VirtualNode,
expected: VirtualNode,
}
impl HtmlMacroTest {
/// Ensure that the generated and the expected virtual node are equal.
fn test(self) {
assert_eq!(self.expected, self.generated);
}
}
#[test]
fn empty_div() {
HtmlMacroTest {
generated: html! { <div></div> },
expected: VirtualNode::element("div"),
}
.test();
}
#[test]
fn one_attr() {
let mut attrs = HashMap::new();
attrs.insert("id".to_string(), "hello-world".to_string());
let mut expected = VElement::new("div");
expected.attrs = attrs;
HtmlMacroTest {
generated: html! { <div id="hello-world"></div> },
expected: expected.into(),
}
.test();
}
/// Events are ignored in non wasm-32 targets
#[test]
fn ignore_events_on_non_wasm32_targets() {
HtmlMacroTest {
generated: html! {
<div onclick=|_: u8|{}></div>
},
expected: html! {<div></div>},
}
.test();
}
#[test]
fn child_node() {
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::element("span")];
HtmlMacroTest {
generated: html! { <div><span></span></div> },
expected: expected.into(),
}
.test();
}
#[test]
fn sibling_child_nodes() {
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::element("span"), VirtualNode::element("b")];
HtmlMacroTest {
generated: html! { <div><span></span><b></b></div> },
expected: expected.into(),
}
.test();
}
/// Nested 3 nodes deep
#[test]
fn three_nodes_deep() {
let mut child = VElement::new("span");
child.children = vec![VirtualNode::element("b")];
let mut expected = VElement::new("div");
expected.children = vec![child.into()];
HtmlMacroTest {
generated: html! { <div><span><b></b></span></div> },
expected: expected.into(),
}
.test()
}
#[test]
fn sibling_text_nodes() {
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::text("This is a text node")];
HtmlMacroTest {
generated: html! { <div>This is a text node</div> },
expected: expected.into(),
}
.test();
}
#[test]
fn nested_macro() {
let child_2 = html! { <b></b> };
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::element("span"), VirtualNode::element("b")];
HtmlMacroTest {
generated: html! {
<div>
{ html! { <span></span> } }
{ child_2 }
</div>
},
expected: expected.into(),
}
.test();
}
/// If the first thing we see is a block then we grab whatever is inside it.
#[test]
fn block_root() {
let em = html! { <em></em> };
let expected = VirtualNode::element("em");
HtmlMacroTest {
generated: html! {
{ em }
},
expected,
}
.test();
}
/// Text followed by a block
#[test]
fn text_next_to_block() {
let child = html! { <ul></ul> };
let mut expected = VElement::new("div");
expected.children = vec![
VirtualNode::text(" A bit of text "),
VirtualNode::element("ul"),
];
HtmlMacroTest {
generated: html! {
<div>
A bit of text
{ child }
</div>
},
expected: expected.into(),
}
.test();
}
/// Ensure that we maintain the correct spacing around punctuation tokens, since
/// they resolve into a separate TokenStream during parsing.
#[test]
fn punctuation_token() {
let text = "Hello, World";
HtmlMacroTest {
generated: html! { Hello, World },
expected: VirtualNode::text(text),
}
.test()
}
#[test]
fn vec_of_nodes() {
let children = vec![html! { <div> </div>}, html! { <strong> </strong>}];
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::element("div"), VirtualNode::element("strong")];
HtmlMacroTest {
generated: html! { <div> { children } </div> },
expected: expected.into(),
}
.test();
}
/// Just make sure that this compiles since async, for, loop, and type are keywords
#[test]
fn keyword_attribute() {
html! { <script src="/app.js" async="async" /> };
html! { <label for="username">Username:</label> };
html! { <audio loop="loop"><source src="/beep.mp3" type="audio/mpeg" /></audio> };
html! { <link rel="stylesheet" type="text/css" href="/app.css" /> };
}
/// For unquoted text apostrophes should be parsed correctly
#[test]
fn apostrophe() {
assert_eq!(html! { Aren't }, VText::new("Aren't").into());
assert_eq!(html! { Aren'ttt }, VText::new("Aren'ttt").into());
}
/// Verify that all of our self closing tags work without backslashes.
#[test]
fn self_closing_tag_without_backslash() {
let mut expected = VElement::new("div");
let children = vec![
"area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param", "command",
"keygen", "source",
]
.into_iter()
.map(|tag| VirtualNode::element(tag))
.collect();
expected.children = children;
HtmlMacroTest {
generated: html! {
<div>
<area> <base> <br> <col> <hr> <img> <input> <link> <meta> <param> <command>
<keygen> <source>
</div>
},
expected: expected.into(),
}
.test();
}
/// Verify that our self closing tags work with backslashes
#[test]
fn self_closing_tag_with_backslace() {
HtmlMacroTest {
generated: html! {
<br />
},
expected: VirtualNode::element("br"),
}
.test();
}
#[test]
fn if_true_block() {
let child_valid = html! { <b></b> };
let child_invalid = html! { <i></i> };
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::element("b")];
HtmlMacroTest {
generated: html! {
<div>
{if true {child_valid} else {child_invalid}}
</div>
},
expected: expected.into(),
}
.test();
}
#[test]
fn if_false_block() {
let child_valid = html! { <b></b> };
let child_invalid = html! { <i></i> };
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::element("i")];
HtmlMacroTest {
generated: html! {
<div>
{if false {
child_valid
} else {
child_invalid
}}
</div>
},
expected: expected.into(),
}
.test();
}
#[test]
fn single_branch_if_true_block() {
let child_valid = html! { <b></b> };
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::element("b")];
HtmlMacroTest {
generated: html! {
<div>{if true {child_valid}}</div>
},
expected: expected.into(),
}
.test();
}
#[test]
fn single_branch_if_false_block() {
let child_valid = html! { <b></b> };
let mut expected = VElement::new("div");
expected.children = vec![VirtualNode::text("")];
HtmlMacroTest {
generated: html! {
<div>{if false {child_valid}}</div>
},
expected: expected.into(),
}
.test();
}
#[test]
fn custom_component_props() {
struct Counter {
count: u8,
}
impl View for Counter {
fn render(&self) -> VirtualNode {
html! {
<span>Counter = {format!("{}", self.count)}</span>
}
}
}
let mut expected = VElement::new("div");
let mut child = VElement::new("span");
child.children = vec![VirtualNode::text("Counter = "), VirtualNode::text("1")];
expected.children = vec![child.into()];
HtmlMacroTest {
generated: html! {
<div><Counter count={1}/></div>
},
expected: expected.into(),
}
.test();
}
#[test]
fn custom_component_children() {
struct Child;
impl View for Child {
fn render(&self) -> VirtualNode {
html! {
<span></span>
}
}
}
let mut expected = VElement::new("div");
let mut child = VElement::new("span");
child.children = vec![VirtualNode::text("Hello World")];
expected.children = vec![child.into()];
HtmlMacroTest {
generated: html! {
<div>
<Child>Hello World</Child>
</div>
},
expected: expected.into(),
}
.test();
}

View file

@ -1,171 +0,0 @@
use html_macro::html;
use virtual_node::{IterableNodes, VirtualNode};
#[test]
fn text_root_node() {
assert_eq!(&html! { some text }.to_string(), "some text");
}
#[test]
fn text_variable_root() {
let text = "hello world";
assert_eq!(&html! { { text } }.to_string(), "hello world");
}
#[test]
fn raw_string_literal() {
assert_eq!(
&html! { <div>{ r#"Hello World"# }</div> }.to_string(),
"<div>Hello World</div>"
);
}
#[test]
fn text_variable_child() {
let text = "world";
assert_eq!(
&html! { <div>{ text }</div> }.to_string(),
"<div>world</div>"
)
}
#[test]
fn text_space_after_start_tag() {
assert_eq!(
&html! { <div> After Start Tag</div> }.to_string(),
"<div> After Start Tag</div>"
)
}
#[test]
fn text_space_before_end_tag() {
assert_eq!(
&html! { <div>Before End Tag </div> }.to_string(),
"<div>Before End Tag </div>"
)
}
#[test]
fn text_space_before_block() {
let text = "Before Block";
assert_eq!(
&html! { <div> {text}</div> }.to_string(),
"<div> Before Block</div>"
)
}
#[test]
fn text_space_after_block() {
let text = "Hello";
assert_eq!(
&html! { <div>{text} </div> }.to_string(),
"<div>Hello </div>"
)
}
#[test]
fn text_space_in_block_ignored() {
let text = "Hello";
assert_eq!(
&html! { <div>{ text }</div> }.to_string(),
"<div>Hello</div>"
)
}
#[test]
fn text_multiple_text_no_space_between() {
let hello = "Hello";
let world = "World";
assert_eq!(
&html! { <div>{ hello }{ world }</div> }.to_string(),
"<div>HelloWorld</div>"
)
}
#[test]
fn text_multiple_text_space_between() {
let hello = "Hello";
let world = "World";
assert_eq!(
&html! { <div>{ hello } { world }</div> }.to_string(),
"<div>Hello World</div>"
)
}
#[test]
fn text_multiple_text_space_around() {
let hello = "Hello";
let world = "World";
assert_eq!(
&html! { <div> { hello }{ world } </div> }.to_string(),
"<div> HelloWorld </div>"
)
}
#[test]
fn text_multiple_text_space_between_around() {
let hello = "Hello";
let world = "World";
assert_eq!(
&html! { <div> { hello } { world } </div> }.to_string(),
"<div> Hello World </div>"
)
}
#[test]
fn text_tokens_in_between_vars_without_space() {
let hello = "Hello";
let world = "World";
assert_eq!(
&html! { <div>{ hello }NoSpace{ world }</div> }.to_string(),
"<div>HelloNoSpaceWorld</div>"
)
}
#[test]
fn text_tokens_in_between_vars_with_space() {
let hello = "Hello";
let world = "World";
assert_eq!(
&html! { <div>{ hello } Space { world }</div> }.to_string(),
"<div>Hello Space World</div>"
)
}
#[test]
fn text_tokens_in_between_vars_space_around_between() {
let hello = "Hello";
let world = "World";
assert_eq!(
&html! { <div> { hello } Space { world } </div> }.to_string(),
"<div> Hello Space World </div>"
)
}
#[test]
fn text_space_before_next_open_tag() {
assert_eq!(
&html! { <div>Hello <img /> world</div> }.to_string(),
"<div>Hello <img> world</div>"
)
}
#[test]
fn text_no_space_before_open_tag() {
assert_eq!(
&html! { <div>Hello<img /> world</div> }.to_string(),
"<div>Hello<img> world</div>"
)
}

View file

@ -1,7 +0,0 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
let ui_tests = concat!(env!("CARGO_MANIFEST_DIR"), "/src/tests/ui/*.rs");
t.compile_fail(ui_tests);
}

View file

@ -1,11 +0,0 @@
#![feature(proc_macro_hygiene)]
extern crate virtual_dom_rs;
use virtual_dom_rs::prelude::*;
// Used a tag name that does not exist in the HTML spec
fn main() {
html! {
<invalidtagname></invalidtagname>
};
}

View file

@ -1,11 +0,0 @@
error: invalidtagname is not a valid HTML tag.
If you are trying to use a valid HTML tag, perhaps there's a typo?
If you are trying to use a custom component, please capitalize the component name.
custom components: https://chinedufn.github.io/percy/html-macro/custom-components/index.html
--> $DIR/invalid_html_tag.rs:9:10
|
9 | <invalidtagname></invalidtagname>
| ^^^^^^^^^^^^^^

View file

@ -1,12 +0,0 @@
#![feature(proc_macro_hygiene)]
extern crate virtual_dom_rs;
use virtual_dom_rs::prelude::*;
// We are using open and close tags for a tag that should
// actually be a self closing tag
fn main () {
html! {
<br></br>
};
}

View file

@ -1,5 +0,0 @@
error: br is a self closing tag. Try "<br>" or "<br />"
--> $DIR/should_be_self_closing_tag.rs:10:15
|
10 | <br></br>
| ^^

View file

@ -1,11 +0,0 @@
#![feature(proc_macro_hygiene)]
extern crate virtual_dom_rs;
use virtual_dom_rs::prelude::*;
// Expected a closing div tag, found a closing strong tag
fn main () {
html! {
<div> </strong>
};
}

View file

@ -1,5 +0,0 @@
error: Wrong closing tag. Try changing "strong" into "div"
--> $DIR/wrong_closing_tag.rs:9:17
|
9 | <div> </strong>
| ^^^^^^

View file

@ -1,22 +0,0 @@
[package]
name = "dioxus-html-macro"
version = "0.1.0"
description = "HTML-compliant macro for creating Dioxus VNodes"
authors = [
"Jonathan Kelley",
# Originally pulled from Percy - check it out!
"Chinedu Francis Nwafili <frankie.nwafili@gmail.com>",
]
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
license = "MIT/Apache-2.0"
edition = "2018"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { version = "0.4", features = ["span-locations"] }
quote = "0.6.11"
syn = { version = "0.15", features = ["full", "extra-traits"] }
lazy_static = "1.4.0"
# html-validation = { path = "../html-validation", version = "0.1.2" }

View file

@ -1,3 +0,0 @@
# html-macro
Disclaimer: much of this macro's source comes from Percy - another vdom implementation for Rust.

View file

@ -1,56 +0,0 @@
extern crate proc_macro;
use crate::parser::HtmlParser;
use crate::tag::Tag;
use syn::parse::{Parse, ParseStream, Result};
use syn::parse_macro_input;
mod parser;
mod tag;
pub(crate) mod validation;
/// Used to generate VirtualNode's from a TokenStream.
///
/// html! { <div> Welcome to the html! procedural macro! </div> }
#[proc_macro]
pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let parsed = parse_macro_input!(input as Html);
let mut html_parser = HtmlParser::new();
let parsed_tags_len = parsed.tags.len();
// Iterate over all of our parsed tags and push them into our HtmlParser one by one.
//
// As we go out HtmlParser will maintain some heuristics about what we've done so far
// since that will sometimes inform how to parse the next token.
for (idx, tag) in parsed.tags.iter().enumerate() {
let mut next_tag = None;
if parsed_tags_len - 1 > idx {
next_tag = Some(&parsed.tags[idx + 1])
}
html_parser.push_tag(tag, next_tag);
}
html_parser.finish().into()
}
#[derive(Debug)]
struct Html {
tags: Vec<Tag>,
}
impl Parse for Html {
fn parse(input: ParseStream) -> Result<Self> {
let mut tags = Vec::new();
while !input.is_empty() {
let tag: Tag = input.parse()?;
tags.push(tag);
}
Ok(Html { tags })
}
}

View file

@ -1,102 +0,0 @@
use crate::parser::HtmlParser;
use crate::tag::{Tag, TagKind};
use proc_macro2::Span;
use quote::quote;
use syn::spanned::Spanned;
use syn::Block;
impl HtmlParser {
/// Parse an incoming Tag::Braced text node
pub(crate) fn parse_braced(
&mut self,
block: &Box<Block>,
brace_span: &Span,
next_tag: Option<&Tag>,
) {
// We'll check to see if there is a space between this block and the previous open
// tag's closing brace.
//
// If so we'll then check if the node in this block is a text node. If it is we'll
// insert a single white space before it.
//
// let some_var = "hello"
// let another_var = "world";
//
// html! { <span>{some_var}</span> } -> would not get a " " inserted
//
// html! { <span> {some_var}</span> } -> would get a " " inserted
let mut insert_whitespace_before_text = false;
if let Some(open_tag_end) = self.recent_span_locations.most_recent_open_tag_end.as_ref() {
if self.last_tag_kind == Some(TagKind::Open)
&& self.separated_by_whitespace(open_tag_end, brace_span)
{
insert_whitespace_before_text = true;
}
}
// If
// 1. The next tag is a closing tag or another braced block
// 2. There is space between this brace and that next tag / braced block
//
// Then
// We'll insert some spacing after this brace.
//
// This ensures that we properly maintain spacing between two neighboring braced
// text nodes
//
// html! { <div>{ This Brace } { Space WILL be inserted }</div>
// -> <div>This Brace Space WILL be inserted</div>
//
// html! { <div>{ This Brace }{ Space WILL NOT be inserted }</div>
// -> <div>This BraceSpace WILL NOT be inserted</div>
let insert_whitespace_after_text = match next_tag {
Some(Tag::Close {
first_angle_bracket_span,
..
}) => self.separated_by_whitespace(brace_span, &first_angle_bracket_span),
Some(Tag::Braced {
brace_span: next_brace_span,
..
}) => self.separated_by_whitespace(brace_span, &next_brace_span),
_ => false,
};
// TODO: Only allow one statement per block. Put a quote_spanned! compiler error if
// there is more than 1 statement. Add a UI test for this.
block.stmts.iter().for_each(|stmt| {
if self.current_node_idx == 0 {
// Here we handle a block being the root node of an `html!` call
//
// html { { some_node } }
let node = quote! {
let node_0: VirtualNode = #stmt.into();
};
self.push_tokens(node);
} else {
self.parse_statement(stmt);
if insert_whitespace_before_text {
let node = self.current_virtual_node_ident(stmt.span());
let insert_whitespace = quote! {
#node.first().insert_space_before_text();
};
self.push_tokens(insert_whitespace);
}
if insert_whitespace_after_text {
let node = self.current_virtual_node_ident(stmt.span());
let insert_whitespace = quote! {
#node.last().insert_space_after_text();
};
self.push_tokens(insert_whitespace);
}
}
});
self.set_most_recent_block_start(brace_span.clone());
}
}

View file

@ -1,49 +0,0 @@
use crate::parser::{is_self_closing, HtmlParser};
use proc_macro2::Ident;
use quote::quote_spanned;
impl HtmlParser {
/// Parse an incoming Tag::Close
pub(crate) fn parse_close_tag(&mut self, name: &Ident) {
let parent_stack = &mut self.parent_stack;
let close_span = name.span();
let close_tag = name.to_string();
// For example, this should have been <br /> instead of </br>
if is_self_closing(&close_tag) {
let error = format!(
r#"{} is a self closing tag. Try "<{}>" or "<{} />""#,
close_tag, close_tag, close_tag
);
let error = quote_spanned! {close_span=> {
compile_error!(#error);
}};
self.push_tokens(error);
return;
}
let last_open_tag = parent_stack.pop().expect("Last open tag");
let last_open_tag = last_open_tag.1.to_string();
// TODO: 2 compile_error!'s one pointing to the open tag and one pointing to the
// closing tag. Update the ui test accordingly
//
// ex: if div != strong
if last_open_tag != close_tag {
let error = format!(
r#"Wrong closing tag. Try changing "{}" into "{}""#,
close_tag, last_open_tag
);
let error = quote_spanned! {close_span=> {
compile_error!(#error);
}};
// TODO: Abort early if we find an error. So we should be returning
// a Result.
self.push_tokens(error);
}
}
}

View file

@ -1,273 +0,0 @@
use crate::tag::TagKind;
use crate::Tag;
use quote::{quote, quote_spanned};
use std::collections::HashMap;
use syn::export::Span;
use syn::spanned::Spanned;
use syn::{Ident, Stmt};
mod braced;
mod close_tag;
mod open_tag;
mod statement;
mod text;
pub enum NodesToPush<'a> {
Stmt(&'a Stmt),
TokenStream(&'a Stmt, proc_macro2::TokenStream),
}
/// Used to parse [`Tag`]s that we've parsed and build a tree of `VirtualNode`s
///
/// [`Tag`]: enum.Tag.html
pub struct HtmlParser {
/// As we parse our macro tokens we'll generate new tokens to return back into the compiler
/// when we're done.
tokens: Vec<proc_macro2::TokenStream>,
/// Everytime we encounter a new node we'll use the current_node_idx to name it.
/// Then we'll increment the current_idx by one.
/// This gives every node that we encounter a unique name that we can use to find
/// it later when we want to push child nodes into parent nodes
current_node_idx: usize,
/// The order that we encountered nodes while parsing.
node_order: Vec<usize>,
/// Each time we encounter a new node that could possible be a parent node
/// we push it's node index onto the stack.
///
/// Text nodes cannot be parent nodes.
parent_stack: Vec<(usize, Ident)>,
/// Key -> index of the parent node within the HTML tree
/// Value -> vector of child node indices
parent_to_children: HashMap<usize, Vec<usize>>,
/// The locations of the most recent spans that we parsed.
/// Used to determine whether or not to put space around text nodes.
recent_span_locations: RecentSpanLocations,
/// The last kind of tag that we parsed.
/// Used to determine whether or not to put space around text nodes.
last_tag_kind: Option<TagKind>,
}
/// TODO: I've hit a good stopping point... but we can clean these methods up / split them up
/// a bit...
impl HtmlParser {
/// Create a new HtmlParser
pub fn new() -> HtmlParser {
let mut parent_to_children: HashMap<usize, Vec<usize>> = HashMap::new();
parent_to_children.insert(0, vec![]);
HtmlParser {
tokens: vec![],
current_node_idx: 0,
node_order: vec![],
parent_stack: vec![],
parent_to_children,
recent_span_locations: RecentSpanLocations::default(),
last_tag_kind: None,
}
}
/// Generate the tokens for the incoming Tag and update our parser's heuristics that keep
/// track of information about what we've parsed.
pub fn push_tag(&mut self, tag: &Tag, next_tag: Option<&Tag>) {
match tag {
Tag::Open {
name,
attrs,
closing_bracket_span,
is_self_closing,
..
} => {
self.parse_open_tag(name, closing_bracket_span, attrs, *is_self_closing);
self.last_tag_kind = Some(TagKind::Open);
}
Tag::Close { name, .. } => {
self.parse_close_tag(name);
self.last_tag_kind = Some(TagKind::Close);
}
Tag::Text {
text,
start_span,
end_span,
} => {
self.parse_text(text, start_span.unwrap(), end_span.unwrap(), next_tag);
self.last_tag_kind = Some(TagKind::Text);
}
Tag::Braced { block, brace_span } => {
self.parse_braced(block, brace_span, next_tag);
self.last_tag_kind = Some(TagKind::Braced);
}
};
}
/// 1. Pop a node off the stack
/// 2. Look up all of it's children in parent_to_children
/// 3. Append the children to this node
/// 4. Move on to the next node (as in, go back to step 1)
pub fn finish(&mut self) -> proc_macro2::TokenStream {
let node_order = &mut self.node_order;
let parent_to_children = &mut self.parent_to_children;
let tokens = &mut self.tokens;
if node_order.len() > 1 {
for _ in 0..(node_order.len()) {
let parent_idx = node_order.pop().unwrap();
// TODO: Figure out how to really use spans
let parent_name =
Ident::new(format!("node_{}", parent_idx).as_str(), Span::call_site());
let parent_to_children_indices = match parent_to_children.get(&parent_idx) {
Some(children) => children,
None => continue,
};
if parent_to_children_indices.len() > 0 {
for child_idx in parent_to_children_indices.iter() {
let children =
Ident::new(format!("node_{}", child_idx).as_str(), Span::call_site());
let unreachable = quote_spanned!(Span::call_site() => {
unreachable!("Non-elements cannot have children");
});
let push_children = quote! {
if let Some(ref mut element_node) = #parent_name.as_velement_mut() {
element_node.children.extend(#children.into_iter());
} else {
#unreachable;
}
};
tokens.push(push_children);
}
}
}
}
// Create a virtual node tree
let node = quote! {
{
move |bump| {
// __domtree_helper(move |bump| {
#(#tokens)*
// Root node is always named node_0
node_0
}
}
};
node
}
/// Add more tokens to our tokens that we'll eventually return to the compiler.
fn push_tokens(&mut self, tokens: proc_macro2::TokenStream) {
self.tokens.push(tokens);
}
/// Set the location of the most recent start tag's ending LineColumn
fn set_most_recent_open_tag_end(&mut self, span: Span) {
self.recent_span_locations.most_recent_open_tag_end = Some(span);
}
/// Set the location of the most recent start tag's ending LineColumn
fn set_most_recent_block_start(&mut self, span: Span) {
self.recent_span_locations.most_recent_block_start = Some(span);
}
/// Determine whether or not there is any space between the end of the first
/// span and the beginning of the second span.
///
/// There is space if they are on separate lines or if they have different columns.
///
/// html! { <div>Hello</div> } <--- no space between end of div and Hello
///
/// html! { <div> Hello</div> } <--- space between end of div and Hello
fn separated_by_whitespace(&self, first_span: &Span, second_span: &Span) -> bool {
if first_span.end().line != second_span.end().line {
return true;
}
second_span.start().column - first_span.end().column > 0
}
/// Create a new identifier for a VirtualNode and increment our node_idx so that next
/// time we call this our node will get a different name.
fn new_virtual_node_ident(&mut self, span: Span) -> Ident {
let node_name = format!("node_{}", self.current_node_idx);
let node_ident = Ident::new(node_name.as_str(), span);
// TODO: Increment before creating the new node, not after.
// This way the current virtual node ident won't need to do strange subtraction
self.current_node_idx += 1;
node_ident
}
/// Get the Ident for the current (last created) virtual node, without incrementing
/// the node index.
fn current_virtual_node_ident(&self, span: Span) -> Ident {
// TODO: Increment before creating the new node, not after.
// This way the current virtual node ident won't need to do strange subtraction
let node_name = format!("node_{}", self.current_node_idx - 1);
Ident::new(node_name.as_str(), span)
}
/// Generate virtual node tokens for a statement that came from in between braces
///
/// examples:
///
/// html! { <div> { some_var_in_braces } </div>
/// html! { <div> { some_other_variable } </div>
fn push_iterable_nodes(&mut self, nodes: NodesToPush) {
let node_idx = self.current_node_idx;
match nodes {
NodesToPush::Stmt(stmt) => {
let node_ident = self.new_virtual_node_ident(stmt.span());
self.push_tokens(quote! {
let mut #node_ident: IterableNodes = (#stmt).into();
});
}
NodesToPush::TokenStream(stmt, tokens) => {
let node_ident = self.new_virtual_node_ident(stmt.span());
self.push_tokens(quote! {
let mut #node_ident: IterableNodes = #tokens.into();
});
}
}
let parent_idx = *&self.parent_stack[self.parent_stack.len() - 1].0;
self.parent_to_children
.get_mut(&parent_idx)
.expect("Parent of these iterable nodes")
.push(node_idx);
self.node_order.push(node_idx);
}
}
/// Keep track of the locations of different kinds of tokens that we encounter.
///
/// This helps us determine whether or not to insert space before or after text tokens
/// in cases such as:
///
/// ```ignore
/// html! { <div> { Hello World } </div>
/// html! { <div>{Hello World}</div>
/// ```
#[derive(Default)]
struct RecentSpanLocations {
most_recent_open_tag_end: Option<Span>,
most_recent_block_start: Option<Span>,
}
fn is_self_closing(tag: &str) -> bool {
crate::validation::self_closing::is_self_closing(tag)
}
fn is_valid_tag(tag: &str) -> bool {
crate::validation::valid_tags::is_valid_tag(tag)
}

View file

@ -1,153 +0,0 @@
use crate::parser::{is_self_closing, is_valid_tag, HtmlParser};
use crate::tag::Attr;
use proc_macro2::{Ident, Span};
use quote::{quote, quote_spanned};
use syn::Expr;
impl HtmlParser {
/// Parse an incoming Tag::Open
pub(crate) fn parse_open_tag(
&mut self,
name: &Ident,
closing_span: &Span,
attrs: &Vec<Attr>,
is_self_closing_tag: bool,
) {
self.set_most_recent_open_tag_end(closing_span.clone());
let idx = &mut self.current_node_idx;
let parent_to_children = &mut self.parent_to_children;
let parent_stack = &mut self.parent_stack;
let tokens = &mut self.tokens;
let node_order = &mut self.node_order;
// The root node is named `node_0`. All of it's descendants are node_1.. node_2.. etc.
// This just comes from the `idx` variable
// TODO: Not sure what the span is supposed to be so I just picked something..
let var_name_node = Ident::new(format!("node_{}", idx).as_str(), name.span());
let html_tag = format!("{}", name);
let is_html_tag = is_valid_tag(&html_tag);
// TODO: Maybe this could be split up into two functions at some point, would have to pass
// a lot of vars around though, which isn't very nice.
if is_html_tag {
let node = quote! {
let mut #var_name_node = VirtualNode::element(#html_tag);
};
tokens.push(node);
for attr in attrs.iter() {
let key = format!("{}", attr.key);
let value = &attr.value;
match value {
Expr::Closure(closure) => {
// TODO: Use this to decide Box<FnMut(_, _, _, ...)
// After we merge the DomUpdater
let _arg_count = closure.inputs.len();
// NOTE: Closures don't work on non wasm32 targets so we only add
// events on wasm32 targets.
let add_closure = quote! {
#[cfg(target_arch = "wasm32")]
{
let closure = Closure::wrap(
Box::new(#value) as Box<FnMut(_)>
);
let closure_rc = std::rc::Rc::new(closure);
#var_name_node.as_velement_mut().expect("Not an element")
.events.0.insert(#key.to_string(), closure_rc);
}
};
tokens.push(add_closure);
}
_ => {
let insert_attribute = quote! {
#var_name_node.as_velement_mut().expect("Not an element")
.attrs.insert(#key.to_string(), #value.to_string());
};
tokens.push(insert_attribute);
}
};
}
} else if !html_tag.chars().next().unwrap().is_uppercase() {
let error = format!(
r#"{} is not a valid HTML tag.
If you are trying to use a valid HTML tag, perhaps there's a typo?
If you are trying to use a custom component, please capitalize the component name.
custom components: https://chinedufn.github.io/percy/html-macro/custom-components/index.html"#,
html_tag,
);
let span = name.span();
let invalid_tag_name_error = quote_spanned! {span=> {
compile_error!(#error);
}};
tokens.push(invalid_tag_name_error);
let node = quote! {
let mut #var_name_node = VirtualNode::text("error");
};
tokens.push(node);
} else {
let var_name_component = Ident::new(format!("component_{}", idx).as_str(), name.span());
let component_ident = Ident::new(format!("{}", html_tag).as_str(), name.span());
let component_props: Vec<proc_macro2::TokenStream> = attrs
.into_iter()
.map(|attr| {
let key = Ident::new(format!("{}", attr.key).as_str(), name.span());
let value = &attr.value;
quote! {
#key: #value,
}
})
.collect();
// TODO @Jon - this is where we need to start injecting new logic about props/children etc
let node = quote! {
let mut #var_name_component = #component_ident { #(#component_props),* };
let mut #var_name_node = #var_name_component.render();
};
tokens.push(node);
}
// The first open tag that we see is our root node so we won't worry about
// giving it a parent
if *idx == 0 {
node_order.push(0);
if !is_self_closing(&html_tag) && !is_self_closing_tag {
parent_stack.push((0, name.clone()));
}
*idx += 1;
return;
}
let parent_idx = *&parent_stack[parent_stack.len() - 1].0;
if !is_self_closing(&html_tag) && !is_self_closing_tag {
parent_stack.push((*idx, name.clone()));
}
node_order.push(*idx);
parent_to_children
.get_mut(&parent_idx)
.expect("Parent of this node")
.push(*idx);
parent_to_children.insert(*idx, vec![]);
*idx += 1;
}
}

View file

@ -1,73 +0,0 @@
use crate::parser::{HtmlParser, NodesToPush};
use quote::quote;
use syn::{Expr, ExprIf, Stmt};
impl HtmlParser {
/// Parse an incoming syn::Stmt node inside a block
pub(crate) fn parse_statement(&mut self, stmt: &Stmt) {
// Here we handle a block being a descendant within some html! call.
//
// The descendant should implement Into<IterableNodes>
//
// html { <div> { some_node } </div> }
match stmt {
Stmt::Expr(expr) => {
self.parse_expr(stmt, expr);
}
_ => {
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
}
};
}
/// Parse an incoming syn::Expr node inside a block
pub(crate) fn parse_expr(&mut self, stmt: &Stmt, expr: &Expr) {
match expr {
Expr::If(expr_if) => {
self.expand_if(stmt, expr_if);
}
_ => {
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
}
}
}
/// Expand an incoming Expr::If block
/// This enables us to use JSX-style conditions inside of blocks such as
/// the following example.
///
/// # Examples
///
/// ```rust,ignore
/// html! {
/// <div>
/// {if condition_is_true {
/// html! { <span>Hello World</span> }
/// }}
/// </div>
/// }
/// ```
///
/// Traditionally this would be possible as an if statement in rust is an
/// expression, so the then, and the else block have to return matching types.
/// Here we identify whether the block is missing the else and fill it in with
/// a blank VirtualNode::text("")
pub(crate) fn expand_if(&mut self, stmt: &Stmt, expr_if: &ExprIf) {
// Has else branch, we can parse the expression as normal.
if let Some(_else_branch) = &expr_if.else_branch {
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
} else {
let condition = &expr_if.cond;
let block = &expr_if.then_branch;
let tokens = quote! {
if #condition {
#block.into()
} else {
VirtualNode::text("")
}
};
self.push_iterable_nodes(NodesToPush::TokenStream(stmt, tokens));
}
}
}

View file

@ -1,96 +0,0 @@
use crate::parser::HtmlParser;
use crate::tag::{Tag, TagKind};
use proc_macro2::{Ident, Span};
use quote::quote;
impl HtmlParser {
/// Parse an incoming Tag::Text text node
pub(crate) fn parse_text(
&mut self,
text: &str,
text_start: Span,
text_end: Span,
next_tag: Option<&Tag>,
) {
let mut text = text.to_string();
if self.should_insert_space_before_text(&text_start) {
text = " ".to_string() + &text;
}
let should_insert_space_after_text = match next_tag {
Some(Tag::Close {
first_angle_bracket_span,
..
}) => self.separated_by_whitespace(&text_end, first_angle_bracket_span),
Some(Tag::Braced { brace_span, .. }) => {
self.separated_by_whitespace(&text_end, brace_span)
}
Some(Tag::Open {
open_bracket_span, ..
}) => self.separated_by_whitespace(&text_end, open_bracket_span),
_ => false,
};
if should_insert_space_after_text {
text += " ";
}
let idx = &mut self.current_node_idx;
let parent_to_children = &mut self.parent_to_children;
let parent_stack = &mut self.parent_stack;
let tokens = &mut self.tokens;
let node_order = &mut self.node_order;
if *idx == 0 {
node_order.push(0);
// TODO: This is just a consequence of bad code. We're pushing this to make
// things work but in reality a text node isn't a parent ever.
// Just need to make the code DRY / refactor so that we can make things make
// sense vs. just bolting things together.
parent_stack.push((0, Ident::new("unused", Span::call_site())));
}
let var_name = Ident::new(format!("node_{}", idx).as_str(), Span::call_site());
let text_node = quote! {
let mut #var_name = VirtualNode::text(#text);
};
tokens.push(text_node);
if *idx == 0 {
*idx += 1;
return;
}
let parent_idx = &parent_stack[parent_stack.len() - 1];
node_order.push(*idx);
parent_to_children
.get_mut(&parent_idx.0)
.expect("Parent of this text node")
.push(*idx);
*idx += 1;
}
/// If the last TagKind was a block or an open tag we check to see if there is space
/// between this text and that tag. If so we insert some space before this text.
fn should_insert_space_before_text(&self, text_start: &Span) -> bool {
if self.last_tag_kind == Some(TagKind::Braced) {
let most_recent_block_start = self.recent_span_locations.most_recent_block_start;
let most_recent_block_start = most_recent_block_start.as_ref().unwrap();
self.separated_by_whitespace(most_recent_block_start, text_start)
} else if self.last_tag_kind == Some(TagKind::Open) {
let most_recent_open_tag_end =
self.recent_span_locations.most_recent_open_tag_end.as_ref();
let most_recent_open_tag_end = most_recent_open_tag_end.unwrap();
self.separated_by_whitespace(most_recent_open_tag_end, text_start)
} else {
false
}
}
}

View file

@ -1,310 +0,0 @@
use proc_macro2::{Span, TokenStream, TokenTree};
use syn::parse::{Parse, ParseStream, Result};
use syn::spanned::Spanned;
use syn::token::Brace;
use syn::{braced, Block, Expr, Ident, Token};
/// The different kinds of tokens that we parse.
///
/// TODO: A better name than tag since not all of these are tags
#[derive(Debug)]
pub enum Tag {
/// <div id="app" class=*CSS>
/// <br />
Open {
name: Ident,
attrs: Vec<Attr>,
open_bracket_span: Span,
closing_bracket_span: Span,
is_self_closing: bool,
},
/// </div>
Close {
name: Ident,
first_angle_bracket_span: Span,
},
/// html! { <div> Hello World </div> }
///
/// -> Hello world
///
/// start_span -> the span for the first token within the text
/// end_span -> the span for the last token within the text
Text {
text: String,
start_span: Option<Span>,
end_span: Option<Span>,
},
/// let text_var = VirtualNode::text("3");
///
/// let iter_nodes =
/// vec![
/// html!{ <div></div> },
/// html! {<span> </span>}
/// ];
///
/// html! {
/// <div>
/// Here are some examples of blocks
/// { text_var }
/// { iter_nodes }
/// { html! { <div> </div> }
/// </div>
/// }
Braced { block: Box<Block>, brace_span: Span },
}
/// The different kinds of tokens that we parse.
///
/// TODO: A better name than tag since not all of these are tags
#[derive(Debug, Eq, PartialEq)]
pub enum TagKind {
Open,
Close,
Text,
Braced,
}
/// id="my-id"
/// class="some classes"
/// etc...
#[derive(Debug)]
pub struct Attr {
pub key: Ident,
pub value: Expr,
}
impl Parse for Tag {
fn parse(input: ParseStream) -> Result<Self> {
let mut input = input;
// If it starts with a `<` it's either an open or close tag.
// ex: <div>
// ex: </em>
if input.peek(Token![<]) {
let first_angle_bracket_span = input.parse::<Token![<]>()?;
let first_angle_bracket_span = first_angle_bracket_span.span();
let optional_close: Option<Token![/]> = input.parse()?;
let is_open_tag = optional_close.is_none();
if is_open_tag {
return parse_open_tag(&mut input, first_angle_bracket_span);
} else {
return parse_close_tag(&mut input, first_angle_bracket_span);
}
}
// { node_inside_block }
if input.peek(Brace) {
return parse_block(&mut input);
}
return parse_text_node(&mut input);
}
}
/// `<div id="app" class=*CSS>`
fn parse_open_tag(input: &mut ParseStream, open_bracket_span: Span) -> Result<Tag> {
let name: Ident = input.parse()?;
let attrs = parse_attributes(input)?;
let is_self_closing: Option<Token![/]> = input.parse()?;
let is_self_closing = is_self_closing.is_some();
let closing_bracket = input.parse::<Token![>]>()?;
let closing_bracket_span = closing_bracket.span();
Ok(Tag::Open {
name,
attrs,
open_bracket_span,
closing_bracket_span,
is_self_closing,
})
}
/// Parse the attributes starting from something like:
/// id="app" class=*CSS>
///
/// As soon as we see
/// >
/// We know that the element has no more attributes and our loop will end
fn parse_attributes(input: &mut ParseStream) -> Result<Vec<Attr>> {
let mut attrs = Vec::new();
// Do we see an identifier such as `id`? If so proceed
while input.peek(Ident)
|| input.peek(Token![async])
|| input.peek(Token![for])
|| input.peek(Token![loop])
|| input.peek(Token![type])
{
// <link rel="stylesheet" type="text/css"
// .. async, for, loop, type need to be handled specially since they are keywords
let maybe_async_key: Option<Token![async]> = input.parse()?;
let maybe_for_key: Option<Token![for]> = input.parse()?;
let maybe_loop_key: Option<Token![loop]> = input.parse()?;
let maybe_type_key: Option<Token![type]> = input.parse()?;
let key = if maybe_async_key.is_some() {
Ident::new("async", maybe_async_key.unwrap().span())
} else if maybe_for_key.is_some() {
Ident::new("for", maybe_for_key.unwrap().span())
} else if maybe_loop_key.is_some() {
Ident::new("loop", maybe_loop_key.unwrap().span())
} else if maybe_type_key.is_some() {
Ident::new("type", maybe_type_key.unwrap().span())
} else {
input.parse()?
};
// =
input.parse::<Token![=]>()?;
// Continue parsing tokens until we see the next attribute or a closing > tag
let mut value_tokens = TokenStream::new();
loop {
let tt: TokenTree = input.parse()?;
value_tokens.extend(Some(tt));
let has_attrib_key = input.peek(Ident)
|| input.peek(Token![async])
|| input.peek(Token![for])
|| input.peek(Token![loop])
|| input.peek(Token![type]);
let peek_start_of_next_attr = has_attrib_key && input.peek2(Token![=]);
let peek_end_of_tag = input.peek(Token![>]);
let peek_self_closing = input.peek(Token![/]);
if peek_end_of_tag || peek_start_of_next_attr || peek_self_closing {
break;
}
}
let value: Expr = syn::parse2(value_tokens)?;
attrs.push(Attr { key, value });
}
Ok(attrs)
}
/// </div>
fn parse_close_tag(input: &mut ParseStream, first_angle_bracket_span: Span) -> Result<Tag> {
let name: Ident = input.parse()?;
input.parse::<Token![>]>()?;
Ok(Tag::Close {
name,
first_angle_bracket_span,
})
}
fn parse_block(input: &mut ParseStream) -> Result<Tag> {
let content;
let brace_token = braced!(content in input);
let brace_span = brace_token.span;
let block_expr = content.call(Block::parse_within)?;
let block = Box::new(Block {
brace_token,
stmts: block_expr,
});
Ok(Tag::Braced { block, brace_span })
}
/// Parse a sequence of tokens until we run into a closing tag
/// html! { <div> Hello World </div> }
/// or a brace
/// html! { <div> Hello World { Braced } </div>
///
/// So, in the second case, there would be two VText nodes created. "Hello World" and "Braced".
///
/// Later in parser/text.rs we'll look at how close the VText nodes are to their neighboring tags
/// to determine whether or not to insert spacing.
///
/// So, in the examples above, since the opening "<div>" has a space after it we'll later transform
/// "Hello World" into " Hello World" in parser/tag.rs
fn parse_text_node(input: &mut ParseStream) -> Result<Tag> {
// Continue parsing tokens until we see a closing tag <
let _text_tokens = TokenStream::new();
let mut text = "".to_string();
let mut idx = 0;
let mut start_span = None;
let mut most_recent_span: Option<Span> = None;
loop {
if input.is_empty() {
break;
}
let tt: TokenTree = input.parse()?;
if idx == 0 {
start_span = Some(tt.span());
most_recent_span = Some(tt.span());
}
// TODO: Properly handle whitespace and new lines
// https://github.com/chinedufn/percy/pull/97#discussion_r263039215
if idx != 0 {
if let Some(most_recent_span) = most_recent_span {
let current_span_start = tt.span().start();
let most_recent_span_end = most_recent_span.end();
let spans_on_different_lines = current_span_start.line != most_recent_span_end.line;
// Contraptions such as "Aren't" give the "'" and the "t" the
// same span, even though they get parsed separately when calling
// input.parse::<TokenTree>().
// As in - it takes two input.parse calls to get the "'" and "t",
// even though they have the same span.
// This might be a bug in syn - but regardless we address this by
// not inserting a space in this case.
let span_comes_before_previous_span = current_span_start.column
< most_recent_span_end.column
&& !spans_on_different_lines;
// Spans are on different lines, insert space
if spans_on_different_lines {
text += " ";
} else if !span_comes_before_previous_span
&& current_span_start.column - most_recent_span_end.column > 0
{
text += " ";
}
}
}
text += &tt.to_string();
most_recent_span = Some(tt.span());
let peek_closing_tag = input.peek(Token![<]);
let peek_start_block = input.peek(Brace);
if peek_closing_tag || peek_start_block {
break;
}
idx += 1;
}
Ok(Tag::Text {
text,
start_span,
end_span: most_recent_span,
})
}

View file

@ -1,36 +0,0 @@
//! The html-validation crate provides method that can be used when validating html elements
//! and attributes.
//!
//! The original goal of this crate was to be used as a dependency in procedural macros that
//! validate html at compile time, but it is general purpose and can be used in other problem
//! spaces.
//!
//! ## Potential Strategy - Pessimistic Validation
//!
//! We might make the html-validation crate is pessimistic by nature.
//!
//! This means that as we develop the create we'll blacklist more and more things - but in general
//! we default to not saying that something is invalid until we've specifically encoded that it is
//! not allowed.
//!
//! This means that you'll see methods with names like `is_definitely_invalid_child` - hinting
//! that we're telling you that we're certain that the relationship is not allowed.
//!
//! Over time we'll cover more and more cases and this should become a non issue, but at the
//! beginning it will mean that our validation is less strict than it should really be.
//!
//! The reason behind this strategy is that it lets people get up and running from day one without
//! needing to wait until our validation is perfect.
//! A downside is that as we become more and more strict there might be situations where you have
//! to go back and tweak your html if you had something that we are now calling invalid.
//!
//! ## Potential Strategy - Optimistic Validation
//!
//! In this case we'd make html! generate a compile time error for anything that isn't certainly valid.
//! Then there would be a second macro such as html_unstrict! that would be a bit more permissive.
//!
//! Over time as our validation permitted more cases people could use html! more and more instead of html_loose!
pub mod self_closing;
pub mod svg_namespace;
pub mod valid_tags;

View file

@ -1,31 +0,0 @@
use lazy_static::lazy_static;
use std::collections::hash_set::HashSet;
use super::svg_namespace::is_self_closing_svg_tag;
// Used to uniquely identify elements that contain closures so that the DomUpdater can
// look them up by their unique id.
// When the DomUpdater sees that the element no longer exists it will drop all of it's
// Rc'd Closures for those events.
lazy_static! {
static ref SELF_CLOSING_TAGS: HashSet<&'static str> = [
"area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param", "command",
"keygen", "source",
]
.iter()
.cloned()
.collect();
}
/// Whether or not this tag is self closing
///
/// ```
/// use html_validation::is_self_closing;
///
/// assert_eq!(is_self_closing("br"), true);
///
/// assert_eq!(is_self_closing("div"), false);
/// ```
pub fn is_self_closing(tag: &str) -> bool {
SELF_CLOSING_TAGS.contains(tag) || is_self_closing_svg_tag(tag)
}

View file

@ -1,114 +0,0 @@
use lazy_static::lazy_static;
use std::collections::HashMap;
lazy_static! {
// list of svg elements
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element
// a hashmap of `(tag, is_self_closing)`
static ref SVG_NAMESPACED_TAGS: HashMap<&'static str, bool> = [
// TODO: can cause conflict with html `a`
//("a", true),
("animate", true),
("animateMotion", false),
("animateTransform", true),
("circle", true),
("clipPath",false),
// TODO: blocked with [issue](https://github.com/chinedufn/percy/issues/106)
//("color-profile",),
("defs", false),
("desc", false),
("discard", true),
("ellipse",true),
("feBlend", true),
("feColorMatrix", true),
("feComponentTransfer", false),
("feComposite", true),
("feConvolveMatrix", true),
("feDiffuseLighting", false),
("feDisplacementMap", true),
("feDistantLight", true),
("feDropShadow", true),
("feFlood", true),
("feFuncA", true),
("feFuncB", true),
("feFuncG", true),
("feFuncR", true),
("feGaussianBlur", true),
("feImage", true),
("feMerge", false),
("feMergeNode", true),
("feMorphology", true),
("feOffset", true),
("fePointLight", true),
("feSpecularLighting", false),
("feSpotLight", true),
("feTile", true),
("feTurbulence", true),
("filter", false),
("foreignObject", false),
("g",false),
("hatch", false),
("hatchpath", true),
("image", true),
("line", true),
("linearGradient", false),
("marker", false),
("mask", false),
// TODO: undocumented
//("mesh",),
// TODO: undocumented
//("meshgradient",),
// TODO: undocumented
//("meshpatch",),
// TODO: undocumented
//("meshrow",),
("metadata", false),
("mpath", true),
("path", true),
("pattern", false),
("polygon", true),
("polyline", true),
("radialGradient", false),
("rect", true),
// TODO: can cause conflict with html `script` tag
//("script", false),
("set", true),
("solidcolor", true),
("stop", true),
// TODO: can cause conflict with html `style` tag
//("style", false),
("svg", false),
("switch", false),
("symbol", false),
("text", false),
("textPath", false),
// TODO: can cause conflict with html `title` tag
//("title", false),
("tspan", false),
// TODO: undocumented
//("unknown",),
("use", true),
("view", true),
]
.iter()
.cloned()
.collect();
}
/// Whether or not this tag is part svg elements
/// ```
/// use html_validation::is_svg_namespace;
///
/// assert_eq!(is_svg_namespace("svg"), true);
///
/// assert_eq!(is_svg_namespace("circle"), true);
///
/// assert_eq!(is_svg_namespace("div"), false);
/// ```
pub fn is_svg_namespace(tag: &str) -> bool {
SVG_NAMESPACED_TAGS.contains_key(tag)
}
/// Whether or not this svg tag is self closing
pub(crate) fn is_self_closing_svg_tag(tag: &str) -> bool {
SVG_NAMESPACED_TAGS.get(tag).map(|v| *v).unwrap_or(false)
}

View file

@ -1,138 +0,0 @@
use lazy_static::lazy_static;
use std::collections::hash_set::HashSet;
use super::svg_namespace::is_svg_namespace;
lazy_static! {
static ref VALID_TAGS: HashSet<&'static str> = [
"a",
"abbr",
"address",
"area",
"article",
"aside",
"audio",
"b",
"base",
"bdi",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"cite",
"code",
"col",
"colgroup",
"command",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"keygen",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"menu",
"menuitem",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"param",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"script",
"section",
"select",
"small",
"source",
"span",
"strong",
"style",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"track",
"u",
"ul",
"var",
"video",
"wbr",
]
.iter()
.cloned()
.collect();
}
/// Whether or not this tag is valid
///
/// ```
/// use html_validation::is_valid_tag;
///
/// assert_eq!(is_valid_tag("br"), true);
///
/// assert_eq!(is_valid_tag("random"), false);
/// ```
pub fn is_valid_tag(tag: &str) -> bool {
VALID_TAGS.contains(tag) || is_svg_namespace(tag)
}

View file

@ -14,7 +14,7 @@ wasm-bindgen = "0.2.71"
# wasm-bindgen = "0.2.70"
lazy_static = "1.4.0"
wasm-bindgen-futures = "0.4.20"
futures = "0.3.12"
# futures = "0.3.12"
wasm-logger = "0.2.0"
log = "0.4.14"
fxhash = "0.2.1"
@ -25,6 +25,9 @@ wasm-bindgen-test = "0.3.21"
once_cell = "1.7.2"
# html-validation = { path = "../html-validation", version = "0.1.1" }
async-channel = "1.6.1"
# futures-lite = "1.11.3"
[dependencies.web-sys]
version = "0.3"
features = [

View file

@ -1,17 +1,19 @@
use dioxus_web::{WebsysRenderer, dioxus::prelude::*};
use dioxus_web::{dioxus::prelude::*, WebsysRenderer};
fn main() {
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
console_error_panic_hook::set_once();
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(CustomA))
wasm_bindgen_futures::spawn_local(WebsysRenderer::start(CustomA))
}
use components::CustomB;
fn CustomA<'a>(ctx: Context<'a>, props: &'a ()) -> DomTree {
let (val, set_val) = use_state(&ctx, || "abcdef".to_string());
ctx.render(rsx!{
ctx.render(rsx! {
div {
class: "m-8"
"CustomA {val}"
button {
"Upper"
@ -29,35 +31,41 @@ fn CustomA<'a>(ctx: Context<'a>, props: &'a ()) -> DomTree {
}
#[derive(Debug, Props, PartialEq)]
struct PropsB<'src> {
val: &'src str
}
mod components {
use super::*;
fn CustomB<'a>(ctx: Context<'a>, props: &'a PropsB<'a>) -> DomTree {
let (first, last) = props.val.split_at(3);
ctx.render(rsx!{
div {
"CustomB {props.val}"
CustomC {
val: first
#[derive(Debug, Props, PartialEq)]
pub struct PropsB<'src> {
val: &'src str,
}
pub fn CustomB<'a>(ctx: Context<'a>, props: &'a PropsB<'a>) -> DomTree {
let (first, last) = props.val.split_at(3);
ctx.render(rsx! {
div {
class: "m-8"
"CustomB {props.val}"
CustomC {
val: first
}
CustomC {
val: last
}
}
CustomC {
val: last
})
}
#[derive(Debug, Props, PartialEq)]
struct PropsC<'src> {
val: &'src str,
}
fn CustomC<'a>(ctx: Context<'a>, props: &'a PropsC<'a>) -> DomTree {
ctx.render(rsx! {
div {
class: "m-8"
"CustomC {props.val}"
}
}
})
}
#[derive(Debug, Props, PartialEq)]
struct PropsC<'src> {
val: &'src str
}
fn CustomC<'a>(ctx: Context<'a>, props: &'a PropsC<'a>) -> DomTree {
ctx.render(rsx!{
div {
"CustomC {props.val}"
}
})
})
}
}

View file

@ -8,7 +8,8 @@ fn main() {
console_error_panic_hook::set_once();
wasm_bindgen_futures::spawn_local(async {
WebsysRenderer::new_with_props(Example, ExampleProps { initial_name: "..?", blarg: Vec::new()})
let props = ExampleProps { initial_name: "..?", blarg: vec!["abc".to_string(), "abc".to_string()]};
WebsysRenderer::new_with_props(Example, props)
.run()
.await
.unwrap()
@ -38,17 +39,19 @@ static Example: FC<ExampleProps> = |ctx, props| {
"Hello, {name}"
}
CustomButton { name: sub, set_name: Box::new(move || set_name("jack")) }
// CustomButton { name: "Jack!", set_name: Box::new(move || set_name("jack")) }
// CustomButton { name: "Jill!", set_name: Box::new(move || set_name("jill")) }
// CustomButton { name: "Reset!", set_name: Box::new(move || set_name(props.initial_name)) }
// CustomButton { name: sub, set_name: Box::new(move || set_name("jack")) }
CustomButton { name: "Jack!", set_name: Box::new(move || set_name("jack")) }
CustomButton { name: "Jill!", set_name: Box::new(move || set_name("jill")) }
CustomButton { name: "Bill!", set_name: Box::new(move || set_name("Bill")) }
CustomButton { name: "Reset!", set_name: Box::new(move || set_name(props.initial_name)) }
}
})
};
#[derive(Props)]
struct ButtonProps<'src> {
name: &'src String,
name: &'src str,
// name: &'src str,
set_name: Box< dyn Fn() + 'src>
}

View file

@ -1,4 +1,4 @@
use std::{borrow::Borrow, convert::TryInto, fmt::Debug, sync::Arc};
use std::{borrow::Borrow, convert::TryInto, default, fmt::Debug, sync::Arc};
use dioxus_core::{
events::{EventTrigger, VirtualEvent},
@ -27,6 +27,8 @@ impl std::fmt::Debug for RootCallback {
pub(crate) struct PatchMachine {
pub(crate) stack: Stack,
pub(crate) known_roots: FxHashMap<u32, Node>,
pub(crate) root: Element,
pub(crate) temporaries: FxHashMap<u32, Node>,
@ -165,6 +167,7 @@ impl PatchMachine {
let events = EventDelegater::new(root.clone(), event_callback);
Self {
known_roots: Default::default(),
root,
events,
stack: Stack::with_capacity(20),
@ -476,8 +479,15 @@ impl PatchMachine {
el.set_class_name(class_name);
}
}
Edit::TraverseToKnown { node } => {}
Edit::MakeKnown { node } => {}
Edit::MakeKnown { node } => {
let domnode = self.stack.top();
self.known_roots.insert(node, domnode.clone());
}
Edit::TraverseToKnown { node } => {
let domnode = self.known_roots.get(&node).expect("Failed to pop know root");
self.stack.push(domnode.clone());
}
Edit::RemoveKnown => {}
}
}

View file

@ -9,7 +9,7 @@ use web_sys::{window, Document, Element, Event, Node};
use dioxus::virtual_dom::VirtualDom;
pub use dioxus_core as dioxus;
use dioxus_core::{events::EventTrigger, prelude::FC};
use futures::{channel::mpsc, SinkExt, StreamExt};
// use futures::{channel::mpsc, SinkExt, StreamExt};
pub mod interpreter;
@ -58,7 +58,7 @@ impl WebsysRenderer {
}
pub async fn run(&mut self) -> dioxus_core::error::Result<()> {
let (sender, mut receiver) = mpsc::unbounded::<EventTrigger>();
let (sender, mut receiver) = async_channel::unbounded::<EventTrigger>();
let body_element = prepare_websys_dom();
@ -85,7 +85,7 @@ impl WebsysRenderer {
// Event loop waits for the receiver to finish up
// TODO! Connect the sender to the virtual dom's suspense system
// Suspense is basically an external event that can force renders to specific nodes
while let Some(event) = receiver.next().await {
while let Ok(event) = receiver.recv().await {
// log::debug!("patch stack size before {:#?}", patch_machine.stack);
// patch_machine.reset();
// patch_machine.stack.push(root_node.clone());