mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 12:13:04 +00:00
Feat: remove old macro
This commit is contained in:
parent
e66827ec92
commit
9d0727edab
43 changed files with 208 additions and 2306 deletions
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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>
|
||||
})
|
||||
};
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
|
@ -1,3 +0,0 @@
|
|||
# html-macro-test
|
||||
|
||||
Unit tests for the `html!` macro
|
|
@ -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;
|
|
@ -1,3 +0,0 @@
|
|||
mod all_tests;
|
||||
mod text;
|
||||
mod ui;
|
|
@ -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();
|
||||
}
|
|
@ -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>"
|
||||
)
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
| ^^^^^^^^^^^^^^
|
|
@ -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>
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
| ^^
|
|
@ -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>
|
||||
};
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
error: Wrong closing tag. Try changing "strong" into "div"
|
||||
--> $DIR/wrong_closing_tag.rs:9:17
|
||||
|
|
||||
9 | <div> </strong>
|
||||
| ^^^^^^
|
|
@ -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" }
|
|
@ -1,3 +0,0 @@
|
|||
# html-macro
|
||||
|
||||
Disclaimer: much of this macro's source comes from Percy - another vdom implementation for Rust.
|
|
@ -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 })
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 = [
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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 => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue