Feat: docs, code frm percy
This commit is contained in:
commit
2b9c8d09d9
72 files changed
+5130
No files matched your search
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "dioxus-core"
|
||||
version = "0.1.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
generational-arena = "0.2.8"
|
||||
typed-arena = "2.0.1"
|
||||
@@ -0,0 +1,22 @@
|
||||
# Dioxus-core
|
||||
|
||||
In the world of Rust UI + wasm UI toolkits, Dioxus is taking a fairly opinionated stance by being functional-only with hooks/context as the primary method of interacting with state.
|
||||
|
||||
To focus on building an effective user experience,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Sources
|
||||
---
|
||||
percy: https://github.com/chinedufn/percy/blob/master/crates/virtual-dom-rs/src/patch/mod.rs
|
||||
yew:
|
||||
dodrio:
|
||||
rsx:
|
||||
|
||||
react:
|
||||
fre:
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
mod old {
|
||||
|
||||
// #![feature(type_alias_impl_trait)]
|
||||
//
|
||||
use std::future::Future;
|
||||
|
||||
trait Props {}
|
||||
struct Context<T: Props> {
|
||||
_props: std::marker::PhantomData<T>,
|
||||
}
|
||||
struct VNode {}
|
||||
|
||||
// type FC<T: Props> = fn(&mut Context<T>) -> VNode;
|
||||
// type FC<T: Props> = fn(&mut Context<T>) -> Box<dyn Future<Output = VNode>>;
|
||||
|
||||
impl Props for () {}
|
||||
|
||||
// async fn some_component(g: &mut Context<()>) -> VNode {
|
||||
// rsx! {
|
||||
// <div>
|
||||
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
// Absolve ourselves of any type data about the context itself
|
||||
trait ContextApplier {
|
||||
fn use_hook<O, H>(
|
||||
&mut self,
|
||||
initializer: impl FnOnce() -> H,
|
||||
runner: impl Fn(&mut H) -> O,
|
||||
tear_down: impl Fn(&mut H),
|
||||
) -> O;
|
||||
}
|
||||
impl<T: Props> ContextApplier for Context<T> {
|
||||
fn use_hook<O, H>(
|
||||
&mut self,
|
||||
initializer: impl FnOnce() -> H,
|
||||
runner: impl Fn(&mut H) -> O,
|
||||
tear_down: impl Fn(&mut H),
|
||||
) -> O {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn use_state<T>(c: &mut impl ContextApplier, g: impl Fn() -> T) -> T {
|
||||
c.use_hook(|| {}, |_| {}, |_| {});
|
||||
g()
|
||||
}
|
||||
|
||||
enum SomeComponent {
|
||||
Imperative,
|
||||
Async,
|
||||
}
|
||||
|
||||
// impl<F, G> From<F> for SomeComponent
|
||||
// where
|
||||
// F: Fn() -> G,
|
||||
// G: Future<Output = ()>,
|
||||
// {
|
||||
// fn from(_: F) -> Self {
|
||||
// SomeComponent::Async
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl From<fn() -> ()> for SomeComponent {
|
||||
// fn from(_: F) -> Self {
|
||||
// SomeComponent::Async
|
||||
// }
|
||||
// }
|
||||
// impl<F> Into<SomeComponent> for fn() -> F
|
||||
// where
|
||||
// F: Future<Output = ()>,
|
||||
// {
|
||||
// fn into(self) -> SomeComponent {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test() {
|
||||
// let b: SomeComponent = test_comp.into();
|
||||
// }
|
||||
|
||||
// Does this make sense?
|
||||
// Any component labeled with async can halt its rendering, but won't be able to process updates?
|
||||
// Or, those updates can still happen virtually, just not propogated into the view?
|
||||
// async fn test_comp() -> () {
|
||||
// timer::new(300).await;
|
||||
// html! {
|
||||
// <div>
|
||||
// "hello world!"
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn use_state<T: Props>(c: &mut Context<T>) {}
|
||||
|
||||
// async fn another_component(ctx: &mut Context<()>) -> VNode {
|
||||
// // delay the re-render until component when the future is ready
|
||||
// // "use_future" loads the promise and provides a value (aka a loadable)
|
||||
// let value = use_effect(move || async {
|
||||
// get_value().join(timer::new(300));
|
||||
// set_value(blah);
|
||||
// });
|
||||
|
||||
// rsx! {
|
||||
// <Suspense fallback={<div>"Loading..."</div>}>
|
||||
// <div>
|
||||
// "hello {name}!"
|
||||
// </div>
|
||||
// <Suspense />
|
||||
// }
|
||||
// }
|
||||
|
||||
/*
|
||||
|
||||
Rationale
|
||||
Today, you can do use_async and do some async operations,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
// type FC<P: Props> = fn(&mut Context<P>) -> VNode;
|
||||
|
||||
// static Example: FC<()> = |_| async {
|
||||
// // some async work
|
||||
// };
|
||||
|
||||
// type FC2 = fn() -> impl Future<Output = ()>;
|
||||
// struct fc<P: Props>(fn(&mut Context<P>) -> G);
|
||||
// fn blah<P: Props, G: Future<Output = VNode>>(a: fn(&mut Context<P>) -> G) {}
|
||||
|
||||
// static Example2: FC2<()> = fc(|_| async { VNode {} });
|
||||
// static Example2: () = blah(|_: &mut Context<()>| async { VNode {} });
|
||||
|
||||
// static Example: FC<()> = |_| {
|
||||
// let g = async { VNode {} };
|
||||
// Box::new(g)
|
||||
// };
|
||||
|
||||
// static Example2: = || {};
|
||||
|
||||
// type FA<R: Future<Output = i32>> = fn(i32) -> R;
|
||||
|
||||
// async fn my_component()
|
||||
// static MyThing: FA<dyn Future<Output = i32>> = |_| async { 10 };
|
||||
|
||||
// type SomeFn = fn() -> ();
|
||||
|
||||
// static MyFn: SomeFn = || {};
|
||||
}
|
||||
|
||||
mod old2 {
|
||||
mod vdom {
|
||||
//! Virtual DOM implementation
|
||||
use super::*;
|
||||
|
||||
pub struct VDom {
|
||||
patches: Vec<Patch>,
|
||||
}
|
||||
|
||||
impl VDom {
|
||||
// fn new(root: ComponentFn) -> Self {
|
||||
// let scope = Scope::new();
|
||||
// Self {}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
mod nodes {}
|
||||
|
||||
mod patch {}
|
||||
|
||||
mod scope {
|
||||
//! Wrappers around components
|
||||
|
||||
pub struct Scope {}
|
||||
|
||||
impl Scope {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod context {}
|
||||
|
||||
struct EventListener {}
|
||||
|
||||
struct VNode {
|
||||
/// key-value pairs of attributes
|
||||
attributes: Vec<(&'static str, &'static str)>,
|
||||
|
||||
/// onclick/onhover/on etc listeners
|
||||
/// goal is to standardize around a set of cross-platform listeners?
|
||||
listeners: Vec<EventListener>,
|
||||
|
||||
/// Direct children, non arena-allocated
|
||||
children: Vec<VNode>,
|
||||
}
|
||||
|
||||
enum ElementType {
|
||||
div,
|
||||
p,
|
||||
a,
|
||||
img,
|
||||
}
|
||||
|
||||
struct ComponentContext {}
|
||||
type ComponentFn = fn(ctx: &ComponentContext) -> VNode;
|
||||
|
||||
enum Patch {}
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Ensure components can be made from the raw components
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
fn component(ctx: &ComponentContext) -> VNode {
|
||||
println!("Running component");
|
||||
VNode {}
|
||||
}
|
||||
|
||||
let dom = VDom::new(component);
|
||||
}
|
||||
|
||||
/// Ensure components can be made from the raw components
|
||||
#[test]
|
||||
fn simple_test_closure() {
|
||||
let component: ComponentFn = |ctx| {
|
||||
println!("Running component");
|
||||
VNode {}
|
||||
};
|
||||
|
||||
let dom = VDom::new(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "dioxus-hooks"
|
||||
version = "0.0.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[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"
|
||||
@@ -0,0 +1,3 @@
|
||||
# html-macro-test
|
||||
|
||||
Unit tests for the `html!` macro
|
||||
@@ -0,0 +1,13 @@
|
||||
//! 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;
|
||||
@@ -0,0 +1,3 @@
|
||||
mod all_tests;
|
||||
mod text;
|
||||
mod ui;
|
||||
@@ -0,0 +1,377 @@
|
||||
//! 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();
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
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>"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#[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);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#![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>
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
| ^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1,12 @@
|
||||
#![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>
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: br is a self closing tag. Try "<br>" or "<br />"
|
||||
--> $DIR/should_be_self_closing_tag.rs:10:15
|
||||
|
|
||||
10 | <br></br>
|
||||
| ^^
|
||||
@@ -0,0 +1,11 @@
|
||||
#![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>
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: Wrong closing tag. Try changing "strong" into "div"
|
||||
--> $DIR/wrong_closing_tag.rs:9:17
|
||||
|
|
||||
9 | <div> </strong>
|
||||
| ^^^^^^
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "html-macro"
|
||||
version = "0.1.9"
|
||||
description = "html macro"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/chinedufn/percy"
|
||||
documentation = "https://chinedufn.github.io/percy/api/html_macro/"
|
||||
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"] }
|
||||
html-validation = {path = "../html-validation", version = "0.1.2"}
|
||||
@@ -0,0 +1,20 @@
|
||||
# html-macro
|
||||
|
||||
```rust
|
||||
use html_macro::*;
|
||||
|
||||
fn main () {
|
||||
let component = html! { <div id='component'>Some component</div> };
|
||||
|
||||
let text_var = "You can interpolate text variables";
|
||||
|
||||
let html = html! {
|
||||
<div onclick=|_ev: web_sys::MouseEvent| {}>
|
||||
You can type text right into the elements
|
||||
{ component }
|
||||
{ text_var }
|
||||
</div>
|
||||
};
|
||||
println!("{}", node);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
|
||||
/// 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 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
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! {
|
||||
{
|
||||
#(#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 {
|
||||
html_validation::is_self_closing(tag)
|
||||
}
|
||||
|
||||
fn is_valid_tag(tag: &str) -> bool {
|
||||
html_validation::is_valid_tag(tag)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use crate::parser::{HtmlParser, NodesToPush};
|
||||
use quote::quote;
|
||||
use syn::{Stmt, Expr, ExprIf};
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "html-validation"
|
||||
version = "0.1.2"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
description = "Validation for HTML elements and attributes"
|
||||
keywords = ["html", "validation", "valid", "dom", "virtual"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/chinedufn/percy"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.0"
|
||||
@@ -0,0 +1,42 @@
|
||||
//! 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!
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub use self_closing::is_self_closing;
|
||||
pub use svg_namespace::is_svg_namespace;
|
||||
pub use valid_tags::is_valid_tag;
|
||||
|
||||
mod self_closing;
|
||||
mod svg_namespace;
|
||||
mod valid_tags;
|
||||
@@ -0,0 +1,31 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "dioxus-macro"
|
||||
version = "0.0.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
Whitespace-only changes.
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "dioxus-recoil"
|
||||
version = "0.0.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "dioxus-redux"
|
||||
version = "0.0.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# virtual-dom-rs Changelog
|
||||
|
||||
Types of changes:
|
||||
|
||||
- `[added]` for new features.
|
||||
- `[changed]` for changes in existing functionality.
|
||||
- `[deprecated]` for once-stable features removed in upcoming releases.
|
||||
- `[removed]` for deprecated features removed in this release.
|
||||
- `[fixed]` for any bug fixes.
|
||||
- `[security]` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
## Not Yet Published
|
||||
|
||||
_Here we list notable things that have been merged into the master branch but have not been released yet._
|
||||
|
||||
- [added] SVG support [#104](https://github.com/chinedufn/percy/pull/104)
|
||||
- ...
|
||||
|
||||
## 0.6.9 - May 23, 2019
|
||||
|
||||
- [added] `on_create_elem` [Docs](https://chinedufn.github.io/percy/html-macro/real-elements-and-nodes/on-create-elem/index.html)
|
||||
- [added] `inner_html` [Docs](https://chinedufn.github.io/percy/html-macro/setting-inner-html/index.html)
|
||||
|
||||
## 0.6.7 - Mar 16, 2019
|
||||
|
||||
- [fixed] Spacing between elements is done by inserting space before and after existing text nodes instead of creating new ones.
|
||||
|
||||
## 0.6.6 - Mar 6, 2019
|
||||
|
||||
- [fixed] Proper spacing in between text nodes and elements in most use cases [PR](https://github.com/chinedufn/percy/pull/97)
|
||||
- Still need to address [#98](https://github.com/chinedufn/percy/issues/98) and then we should have all cases handled.
|
||||
|
||||
## 0.6.5 - Mar 4, 2019
|
||||
|
||||
- [added] Start supporting braced text in the `html!` macro [#96](https://github.com/chinedufn/percy/pull/96)
|
||||
- [removed] Removed the `text!` macro
|
||||
|
||||
```rust
|
||||
let hello = "hello world";
|
||||
html! { {hello} }
|
||||
```
|
||||
|
||||
|
||||
## 0.6.4 - Feb 24, 2019
|
||||
|
||||
- [fixed] Using the `html!` macro to create an event now uses the fully qualified path to `std::rc::Rc`
|
||||
- [added] Started adding key support. If a VirtualNode's key attribute changes it will lead to a `Replace` patch.
|
||||
|
||||
```rust
|
||||
// example
|
||||
html! { <div key="5"></div> }`;
|
||||
````
|
||||
|
||||
## 0.6.1 - Feb 22, 2019
|
||||
|
||||
- [fixed] Fix DomUpdater not storing closures for nodes that were created during `Patch::AppendChildren`
|
||||
and `Patch::Replace`
|
||||
- [Issue](https://github.com/chinedufn/percy/issues/70)
|
||||
@@ -0,0 +1,47 @@
|
||||
[package]
|
||||
name = "virtual-dom-rs"
|
||||
version = "0.6.14"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
description = "A standalone Virtual DOM creation, diffing and patching implementation"
|
||||
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/chinedufn/percy"
|
||||
documentation = "https://chinedufn.github.io/percy/api/virtual_dom_rs/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "0.2.33"
|
||||
virtual-node = { path = "../virtual-node", version = "0.2.7" }
|
||||
html-macro = { path = "../html-macro", version = "0.1.8"}
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Comment",
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlElement",
|
||||
"EventTarget",
|
||||
"HtmlCollection",
|
||||
"Node",
|
||||
"NodeList",
|
||||
"Text",
|
||||
"CharacterData",
|
||||
"Window",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.2.33"
|
||||
console_error_panic_hook = "0.1.5"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"DomTokenList",
|
||||
"HtmlInputElement",
|
||||
"Event",
|
||||
"MouseEvent",
|
||||
"InputEvent",
|
||||
"console",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
# virtual-dom-rs
|
||||
|
||||
> A standalone Virtual DOM creation, diffing and patching implementation
|
||||
@@ -0,0 +1,26 @@
|
||||
//! Kept in it's own file to more easily import into the Percy book.
|
||||
|
||||
use crate::diff::diff;
|
||||
use crate::patch::Patch;
|
||||
use virtual_node::VirtualNode;
|
||||
|
||||
/// Test that we generate the right Vec<Patch> for some start and end virtual dom.
|
||||
pub struct DiffTestCase<'a> {
|
||||
// ex: "Patching root level nodes works"
|
||||
pub description: &'static str,
|
||||
// ex: html! { <div> </div> }
|
||||
pub old: VirtualNode,
|
||||
// ex: html! { <strong> </strong> }
|
||||
pub new: VirtualNode,
|
||||
// ex: vec![Patch::Replace(0, &html! { <strong></strong> })],
|
||||
pub expected: Vec<Patch<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> DiffTestCase<'a> {
|
||||
pub fn test(&self) {
|
||||
// ex: vec![Patch::Replace(0, &html! { <strong></strong> })],
|
||||
let patches = diff(&self.old, &self.new);
|
||||
|
||||
assert_eq!(patches, self.expected, "{}", self.description);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
use crate::Patch;
|
||||
use crate::VirtualNode;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
|
||||
/// Given two VirtualNode's generate Patch's that would turn the old virtual node's
|
||||
/// real DOM node equivalent into the new VirtualNode's real DOM node equivalent.
|
||||
pub fn diff<'a>(old: &'a VirtualNode, new: &'a VirtualNode) -> Vec<Patch<'a>> {
|
||||
diff_recursive(&old, &new, &mut 0)
|
||||
}
|
||||
|
||||
fn diff_recursive<'a, 'b>(
|
||||
old: &'a VirtualNode,
|
||||
new: &'a VirtualNode,
|
||||
cur_node_idx: &'b mut usize,
|
||||
) -> Vec<Patch<'a>> {
|
||||
let mut patches = vec![];
|
||||
let mut replace = false;
|
||||
|
||||
// Different enum variants, replace!
|
||||
if mem::discriminant(old) != mem::discriminant(new) {
|
||||
replace = true;
|
||||
}
|
||||
|
||||
if let (VirtualNode::Element(old_element), VirtualNode::Element(new_element)) = (old, new) {
|
||||
// Replace if there are different element tags
|
||||
if old_element.tag != new_element.tag {
|
||||
replace = true;
|
||||
}
|
||||
|
||||
// Replace if two elements have different keys
|
||||
// TODO: More robust key support. This is just an early stopgap to allow you to force replace
|
||||
// an element... say if it's event changed. Just change the key name for now.
|
||||
// In the future we want keys to be used to create a Patch::ReOrder to re-order siblings
|
||||
if old_element.attrs.get("key").is_some()
|
||||
&& old_element.attrs.get("key") != new_element.attrs.get("key")
|
||||
{
|
||||
replace = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle replacing of a node
|
||||
if replace {
|
||||
patches.push(Patch::Replace(*cur_node_idx, &new));
|
||||
if let VirtualNode::Element(old_element_node) = old {
|
||||
for child in old_element_node.children.iter() {
|
||||
increment_node_idx_for_children(child, cur_node_idx);
|
||||
}
|
||||
}
|
||||
return patches;
|
||||
}
|
||||
|
||||
// The following comparison can only contain identical variants, other
|
||||
// cases have already been handled above by comparing variant
|
||||
// discriminants.
|
||||
match (old, new) {
|
||||
// We're comparing two text nodes
|
||||
(VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => {
|
||||
if old_text != new_text {
|
||||
patches.push(Patch::ChangeText(*cur_node_idx, &new_text));
|
||||
}
|
||||
}
|
||||
|
||||
// We're comparing two element nodes
|
||||
(VirtualNode::Element(old_element), VirtualNode::Element(new_element)) => {
|
||||
let mut add_attributes: HashMap<&str, &str> = HashMap::new();
|
||||
let mut remove_attributes: Vec<&str> = vec![];
|
||||
|
||||
// TODO: -> split out into func
|
||||
for (new_attr_name, new_attr_val) in new_element.attrs.iter() {
|
||||
match old_element.attrs.get(new_attr_name) {
|
||||
Some(ref old_attr_val) => {
|
||||
if old_attr_val != &new_attr_val {
|
||||
add_attributes.insert(new_attr_name, new_attr_val);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
add_attributes.insert(new_attr_name, new_attr_val);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: -> split out into func
|
||||
for (old_attr_name, old_attr_val) in old_element.attrs.iter() {
|
||||
if add_attributes.get(&old_attr_name[..]).is_some() {
|
||||
continue;
|
||||
};
|
||||
|
||||
match new_element.attrs.get(old_attr_name) {
|
||||
Some(ref new_attr_val) => {
|
||||
if new_attr_val != &old_attr_val {
|
||||
remove_attributes.push(old_attr_name);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
remove_attributes.push(old_attr_name);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if add_attributes.len() > 0 {
|
||||
patches.push(Patch::AddAttributes(*cur_node_idx, add_attributes));
|
||||
}
|
||||
if remove_attributes.len() > 0 {
|
||||
patches.push(Patch::RemoveAttributes(*cur_node_idx, remove_attributes));
|
||||
}
|
||||
|
||||
let old_child_count = old_element.children.len();
|
||||
let new_child_count = new_element.children.len();
|
||||
|
||||
if new_child_count > old_child_count {
|
||||
let append_patch: Vec<&'a VirtualNode> =
|
||||
new_element.children[old_child_count..].iter().collect();
|
||||
patches.push(Patch::AppendChildren(*cur_node_idx, append_patch))
|
||||
}
|
||||
|
||||
if new_child_count < old_child_count {
|
||||
patches.push(Patch::TruncateChildren(*cur_node_idx, new_child_count))
|
||||
}
|
||||
|
||||
let min_count = min(old_child_count, new_child_count);
|
||||
for index in 0..min_count {
|
||||
*cur_node_idx = *cur_node_idx + 1;
|
||||
let old_child = &old_element.children[index];
|
||||
let new_child = &new_element.children[index];
|
||||
patches.append(&mut diff_recursive(&old_child, &new_child, cur_node_idx))
|
||||
}
|
||||
if new_child_count < old_child_count {
|
||||
for child in old_element.children[min_count..].iter() {
|
||||
increment_node_idx_for_children(child, cur_node_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
(VirtualNode::Text(_), VirtualNode::Element(_))
|
||||
| (VirtualNode::Element(_), VirtualNode::Text(_)) => {
|
||||
unreachable!("Unequal variant discriminants should already have been handled");
|
||||
}
|
||||
};
|
||||
|
||||
// new_root.create_element()
|
||||
patches
|
||||
}
|
||||
|
||||
fn increment_node_idx_for_children<'a, 'b>(old: &'a VirtualNode, cur_node_idx: &'b mut usize) {
|
||||
*cur_node_idx += 1;
|
||||
if let VirtualNode::Element(element_node) = old {
|
||||
for child in element_node.children.iter() {
|
||||
increment_node_idx_for_children(&child, cur_node_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod diff_test_case;
|
||||
#[cfg(test)]
|
||||
use self::diff_test_case::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{html, VText, VirtualNode};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn replace_node() {
|
||||
DiffTestCase {
|
||||
description: "Replace the root if the tag changed",
|
||||
old: html! { <div> </div> },
|
||||
new: html! { <span> </span> },
|
||||
expected: vec![Patch::Replace(0, &html! { <span></span> })],
|
||||
}
|
||||
.test();
|
||||
DiffTestCase {
|
||||
description: "Replace a child node",
|
||||
old: html! { <div> <b></b> </div> },
|
||||
new: html! { <div> <strong></strong> </div> },
|
||||
expected: vec![Patch::Replace(1, &html! { <strong></strong> })],
|
||||
}
|
||||
.test();
|
||||
DiffTestCase {
|
||||
description: "Replace node with a child",
|
||||
old: html! { <div> <b>1</b> <b></b> </div> },
|
||||
new: html! { <div> <i>1</i> <i></i> </div>},
|
||||
expected: vec![
|
||||
Patch::Replace(1, &html! { <i>1</i> }),
|
||||
Patch::Replace(3, &html! { <i></i> }),
|
||||
], //required to check correct index
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_children() {
|
||||
DiffTestCase {
|
||||
description: "Added a new node to the root node",
|
||||
old: html! { <div> <b></b> </div> },
|
||||
new: html! { <div> <b></b> <span></span> </div> },
|
||||
expected: vec![Patch::AppendChildren(0, vec![&html! { <span></span> }])],
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_nodes() {
|
||||
DiffTestCase {
|
||||
description: "Remove all child nodes at and after child sibling index 1",
|
||||
old: html! { <div> <b></b> <span></span> </div> },
|
||||
new: html! { <div> </div> },
|
||||
expected: vec![Patch::TruncateChildren(0, 0)],
|
||||
}
|
||||
.test();
|
||||
DiffTestCase {
|
||||
description: "Remove a child and a grandchild node",
|
||||
old: html! {
|
||||
<div>
|
||||
<span>
|
||||
<b></b>
|
||||
// This `i` tag will get removed
|
||||
<i></i>
|
||||
</span>
|
||||
// This `strong` tag will get removed
|
||||
<strong></strong>
|
||||
</div> },
|
||||
new: html! {
|
||||
<div>
|
||||
<span>
|
||||
<b></b>
|
||||
</span>
|
||||
</div> },
|
||||
expected: vec![Patch::TruncateChildren(0, 1), Patch::TruncateChildren(1, 1)],
|
||||
}
|
||||
.test();
|
||||
DiffTestCase {
|
||||
description: "Removing child and change next node after parent",
|
||||
old: html! { <div> <b> <i></i> <i></i> </b> <b></b> </div> },
|
||||
new: html! { <div> <b> <i></i> </b> <i></i> </div>},
|
||||
expected: vec![
|
||||
Patch::TruncateChildren(1, 1),
|
||||
Patch::Replace(4, &html! { <i></i> }),
|
||||
], //required to check correct index
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_attributes() {
|
||||
let mut attributes = HashMap::new();
|
||||
attributes.insert("id", "hello");
|
||||
|
||||
DiffTestCase {
|
||||
old: html! { <div> </div> },
|
||||
new: html! { <div id="hello"> </div> },
|
||||
expected: vec![Patch::AddAttributes(0, attributes.clone())],
|
||||
description: "Add attributes",
|
||||
}
|
||||
.test();
|
||||
|
||||
DiffTestCase {
|
||||
old: html! { <div id="foobar"> </div> },
|
||||
new: html! { <div id="hello"> </div> },
|
||||
expected: vec![Patch::AddAttributes(0, attributes)],
|
||||
description: "Change attribute",
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_attributes() {
|
||||
DiffTestCase {
|
||||
old: html! { <div id="hey-there"></div> },
|
||||
new: html! { <div> </div> },
|
||||
expected: vec![Patch::RemoveAttributes(0, vec!["id"])],
|
||||
description: "Add attributes",
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_attribute() {
|
||||
let mut attributes = HashMap::new();
|
||||
attributes.insert("id", "changed");
|
||||
|
||||
DiffTestCase {
|
||||
description: "Add attributes",
|
||||
old: html! { <div id="hey-there"></div> },
|
||||
new: html! { <div id="changed"> </div> },
|
||||
expected: vec![Patch::AddAttributes(0, attributes)],
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_text_node() {
|
||||
DiffTestCase {
|
||||
description: "Replace text node",
|
||||
old: html! { Old },
|
||||
new: html! { New },
|
||||
expected: vec![Patch::ChangeText(0, &VText::new("New"))],
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
// Initially motivated by having two elements where all that changed was an event listener
|
||||
// because right now we don't patch event listeners. So.. until we have a solution
|
||||
// for that we can just give them different keys to force a replace.
|
||||
#[test]
|
||||
fn replace_if_different_keys() {
|
||||
DiffTestCase {
|
||||
description: "If two nodes have different keys always generate a full replace.",
|
||||
old: html! { <div key="1"> </div> },
|
||||
new: html! { <div key="2"> </div> },
|
||||
expected: vec![Patch::Replace(0, &html! {<div key="2"> </div>})],
|
||||
}
|
||||
.test()
|
||||
}
|
||||
|
||||
// // TODO: Key support
|
||||
// #[test]
|
||||
// fn reorder_chldren() {
|
||||
// let mut attributes = HashMap::new();
|
||||
// attributes.insert("class", "foo");
|
||||
//
|
||||
// let old_children = vec![
|
||||
// // old node 0
|
||||
// html! { <div key="hello", id="same-id", style="",></div> },
|
||||
// // removed
|
||||
// html! { <div key="gets-removed",> { "This node gets removed"} </div>},
|
||||
// // old node 2
|
||||
// html! { <div key="world", class="changed-class",></div>},
|
||||
// // removed
|
||||
// html! { <div key="this-got-removed",> { "This node gets removed"} </div>},
|
||||
// ];
|
||||
//
|
||||
// let new_children = vec![
|
||||
// html! { <div key="world", class="foo",></div> },
|
||||
// html! { <div key="new",> </div>},
|
||||
// html! { <div key="hello", id="same-id",></div>},
|
||||
// ];
|
||||
//
|
||||
// test(DiffTestCase {
|
||||
// old: html! { <div> { old_children } </div> },
|
||||
// new: html! { <div> { new_children } </div> },
|
||||
// expected: vec![
|
||||
// // TODO: Come up with the patch structure for keyed nodes..
|
||||
// // keying should only work if all children have keys..
|
||||
// ],
|
||||
// description: "Add attributes",
|
||||
// })
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Diff virtual-doms and patch the real DOM
|
||||
|
||||
use crate::diff::diff;
|
||||
use crate::patch::patch;
|
||||
use std::collections::HashMap;
|
||||
use virtual_node::DynClosure;
|
||||
use virtual_node::VirtualNode;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// Closures that we are holding on to to make sure that they don't get invalidated after a
|
||||
/// VirtualNode is dropped.
|
||||
///
|
||||
/// The u32 is a unique identifier that is associated with the DOM element that this closure is
|
||||
/// attached to.
|
||||
///
|
||||
/// TODO: Periodically check if the DOM element is still there, and if not drop the closure.
|
||||
/// Maybe whenever a DOM node is replaced or truncated we figure out all of it's
|
||||
/// descendants somehow and invalidate those closures..? Need to plan this out..
|
||||
/// At it stands now this hashmap will grow anytime a new element with closures is
|
||||
/// appended or replaced and we will never free those closures.
|
||||
pub type ActiveClosures = HashMap<u32, Vec<DynClosure>>;
|
||||
|
||||
/// Used for keeping a real DOM node up to date based on the current VirtualNode
|
||||
/// and a new incoming VirtualNode that represents our latest DOM state.
|
||||
pub struct DomUpdater {
|
||||
current_vdom: VirtualNode,
|
||||
/// The closures that are currently attached to elements in the page.
|
||||
///
|
||||
/// We keep these around so that they don't get dropped (and thus stop working);
|
||||
///
|
||||
/// FIXME: Drop them when the element is no longer in the page. Need to figure out
|
||||
/// a good strategy for when to do this.
|
||||
pub active_closures: ActiveClosures,
|
||||
root_node: Node,
|
||||
}
|
||||
|
||||
impl DomUpdater {
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created but not added to your DOM.
|
||||
pub fn new(current_vdom: VirtualNode) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created and appended (as a child) to your passed
|
||||
/// in mount element.
|
||||
pub fn new_append_to_mount(current_vdom: VirtualNode, mount: &Element) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
mount
|
||||
.append_child(&created_node.node)
|
||||
.expect("Could not append child to mount");
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created and it will replace your passed in mount
|
||||
/// element.
|
||||
pub fn new_replace_mount(current_vdom: VirtualNode, mount: Element) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
mount
|
||||
.replace_with_with_node_1(&created_node.node)
|
||||
.expect("Could not replace mount element");
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Diff the current virtual dom with the new virtual dom that is being passed in.
|
||||
///
|
||||
/// Then use that diff to patch the real DOM in the user's browser so that they are
|
||||
/// seeing the latest state of the application.
|
||||
pub fn update(&mut self, new_vdom: VirtualNode) {
|
||||
let patches = diff(&self.current_vdom, &new_vdom);
|
||||
|
||||
let active_closures = patch(self.root_node.clone(), &patches).unwrap();
|
||||
|
||||
self.active_closures.extend(active_closures);
|
||||
|
||||
self.current_vdom = new_vdom;
|
||||
}
|
||||
|
||||
/// Return the root node of your application, the highest ancestor of all other nodes in
|
||||
/// your real DOM tree.
|
||||
pub fn root_node(&self) -> Node {
|
||||
// Note that we're cloning the `web_sys::Node`, not the DOM element.
|
||||
// So we're effectively cloning a pointer here, which is fast.
|
||||
self.root_node.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//! virtual-dom-rs provides a virtual dom implementation as well as an `html!` macro
|
||||
//! that you can use to generate a virtual dom.
|
||||
//!
|
||||
//! The virtual dom works on both the client and server. On the client we'll render
|
||||
//! to an `HtmlElement`, and on the server we render to a `String`.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
// #![cfg_attr(test, feature(proc_macro_hygiene))]
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
|
||||
// Used so that `html!` calls work when people depend on this crate since `html!` needs
|
||||
// access to `Closure` when creating event handlers.
|
||||
pub use wasm_bindgen::prelude::Closure;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_bindgen::JsCast;
|
||||
|
||||
pub extern crate web_sys;
|
||||
pub use web_sys::*;
|
||||
|
||||
pub use virtual_node::*;
|
||||
|
||||
mod diff;
|
||||
pub use crate::diff::*;
|
||||
|
||||
mod patch;
|
||||
pub use crate::patch::*;
|
||||
|
||||
pub use html_macro::html;
|
||||
|
||||
mod dom_updater;
|
||||
pub use self::dom_updater::DomUpdater;
|
||||
|
||||
/// Exports structs and macros that you'll almost always want access to in a virtual-dom
|
||||
/// powered application
|
||||
pub mod prelude {
|
||||
pub use crate::dom_updater::DomUpdater;
|
||||
pub use crate::VirtualNode;
|
||||
pub use html_macro::html;
|
||||
pub use std::vec::IntoIter;
|
||||
pub use virtual_node::IterableNodes;
|
||||
pub use virtual_node::View;
|
||||
pub use wasm_bindgen::prelude::Closure;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
use crate::patch::Patch;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::dom_updater::ActiveClosures;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{Element, Node, Text};
|
||||
|
||||
/// Apply all of the patches to our old root node in order to create the new root node
|
||||
/// that we desire.
|
||||
/// This is usually used after diffing two virtual nodes.
|
||||
pub fn patch<N: Into<Node>>(root_node: N, patches: &Vec<Patch>) -> Result<ActiveClosures, JsValue> {
|
||||
let root_node: Node = root_node.into();
|
||||
|
||||
let mut cur_node_idx = 0;
|
||||
|
||||
let mut nodes_to_find = HashSet::new();
|
||||
|
||||
for patch in patches {
|
||||
nodes_to_find.insert(patch.node_idx());
|
||||
}
|
||||
|
||||
let mut element_nodes_to_patch = HashMap::new();
|
||||
let mut text_nodes_to_patch = HashMap::new();
|
||||
|
||||
// Closures that were added to the DOM during this patch operation.
|
||||
let mut active_closures = HashMap::new();
|
||||
|
||||
find_nodes(
|
||||
root_node,
|
||||
&mut cur_node_idx,
|
||||
&mut nodes_to_find,
|
||||
&mut element_nodes_to_patch,
|
||||
&mut text_nodes_to_patch,
|
||||
);
|
||||
|
||||
for patch in patches {
|
||||
let patch_node_idx = patch.node_idx();
|
||||
|
||||
if let Some(element) = element_nodes_to_patch.get(&patch_node_idx) {
|
||||
let new_closures = apply_element_patch(&element, &patch)?;
|
||||
active_closures.extend(new_closures);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(text_node) = text_nodes_to_patch.get(&patch_node_idx) {
|
||||
apply_text_patch(&text_node, &patch)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
unreachable!("Getting here means we didn't find the element or next node that we were supposed to patch.")
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
|
||||
fn find_nodes(
|
||||
root_node: Node,
|
||||
cur_node_idx: &mut usize,
|
||||
nodes_to_find: &mut HashSet<usize>,
|
||||
element_nodes_to_patch: &mut HashMap<usize, Element>,
|
||||
text_nodes_to_patch: &mut HashMap<usize, Text>,
|
||||
) {
|
||||
if nodes_to_find.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// We use child_nodes() instead of children() because children() ignores text nodes
|
||||
let children = root_node.child_nodes();
|
||||
let child_node_count = children.length();
|
||||
|
||||
// If the root node matches, mark it for patching
|
||||
if nodes_to_find.get(&cur_node_idx).is_some() {
|
||||
match root_node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
element_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into());
|
||||
}
|
||||
Node::TEXT_NODE => {
|
||||
text_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into());
|
||||
}
|
||||
other => unimplemented!("Unsupported root node type: {}", other),
|
||||
}
|
||||
nodes_to_find.remove(&cur_node_idx);
|
||||
}
|
||||
|
||||
*cur_node_idx += 1;
|
||||
|
||||
for i in 0..child_node_count {
|
||||
let node = children.item(i).unwrap();
|
||||
|
||||
match node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
find_nodes(
|
||||
node,
|
||||
cur_node_idx,
|
||||
nodes_to_find,
|
||||
element_nodes_to_patch,
|
||||
text_nodes_to_patch,
|
||||
);
|
||||
}
|
||||
Node::TEXT_NODE => {
|
||||
if nodes_to_find.get(&cur_node_idx).is_some() {
|
||||
text_nodes_to_patch.insert(*cur_node_idx, node.unchecked_into());
|
||||
}
|
||||
|
||||
*cur_node_idx += 1;
|
||||
}
|
||||
Node::COMMENT_NODE => {
|
||||
// At this time we do not support user entered comment nodes, so if we see a comment
|
||||
// then it was a delimiter created by virtual-dom-rs in order to ensure that two
|
||||
// neighboring text nodes did not get merged into one by the browser. So we skip
|
||||
// over this virtual-dom-rs generated comment node.
|
||||
}
|
||||
_other => {
|
||||
// Ignoring unsupported child node type
|
||||
// TODO: What do we do with this situation? Log a warning?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_element_patch(node: &Element, patch: &Patch) -> Result<ActiveClosures, JsValue> {
|
||||
let active_closures = HashMap::new();
|
||||
|
||||
match patch {
|
||||
Patch::AddAttributes(_node_idx, attributes) => {
|
||||
for (attrib_name, attrib_val) in attributes.iter() {
|
||||
node.set_attribute(attrib_name, attrib_val)?;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::RemoveAttributes(_node_idx, attributes) => {
|
||||
for attrib_name in attributes.iter() {
|
||||
node.remove_attribute(attrib_name)?;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::Replace(_node_idx, new_node) => {
|
||||
let created_node = new_node.create_dom_node();
|
||||
|
||||
node.replace_with_with_node_1(&created_node.node)?;
|
||||
|
||||
Ok(created_node.closures)
|
||||
}
|
||||
Patch::TruncateChildren(_node_idx, num_children_remaining) => {
|
||||
let children = node.child_nodes();
|
||||
let mut child_count = children.length();
|
||||
|
||||
// We skip over any separators that we placed between two text nodes
|
||||
// -> `<!--ptns-->`
|
||||
// and trim all children that come after our new desired `num_children_remaining`
|
||||
let mut non_separator_children_found = 0;
|
||||
|
||||
for index in 0 as u32..child_count {
|
||||
let child = children
|
||||
.get(min(index, child_count - 1))
|
||||
.expect("Potential child to truncate");
|
||||
|
||||
// If this is a comment node then we know that it is a `<!--ptns-->`
|
||||
// text node separator that was created in virtual_node/mod.rs.
|
||||
if child.node_type() == Node::COMMENT_NODE {
|
||||
continue;
|
||||
}
|
||||
|
||||
non_separator_children_found += 1;
|
||||
|
||||
if non_separator_children_found <= *num_children_remaining as u32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.remove_child(&child).expect("Truncated children");
|
||||
child_count -= 1;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::AppendChildren(_node_idx, new_nodes) => {
|
||||
let parent = &node;
|
||||
|
||||
let mut active_closures = HashMap::new();
|
||||
|
||||
for new_node in new_nodes {
|
||||
let created_node = new_node.create_dom_node();
|
||||
|
||||
parent.append_child(&created_node.node)?;
|
||||
|
||||
active_closures.extend(created_node.closures);
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::ChangeText(_node_idx, _new_node) => {
|
||||
unreachable!("Elements should not receive ChangeText patches.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_text_patch(node: &Text, patch: &Patch) -> Result<(), JsValue> {
|
||||
match patch {
|
||||
Patch::ChangeText(_node_idx, new_node) => {
|
||||
node.set_node_value(Some(&new_node.text));
|
||||
}
|
||||
Patch::Replace(_node_idx, new_node) => {
|
||||
node.replace_with_with_node_1(&new_node.create_dom_node().node)?;
|
||||
}
|
||||
other => unreachable!(
|
||||
"Text nodes should only receive ChangeText or Replace patches, not {:?}.",
|
||||
other,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Our Patch enum is intentionally kept in it's own file for easy inclusion into
|
||||
//! The Percy Book.
|
||||
|
||||
use crate::{VText, VirtualNode};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod apply_patches;
|
||||
pub use apply_patches::patch;
|
||||
|
||||
/// A Patch encodes an operation that modifies a real DOM element.
|
||||
///
|
||||
/// To update the real DOM that a user sees you'll want to first diff your
|
||||
/// old virtual dom and new virtual dom.
|
||||
///
|
||||
/// This diff operation will generate `Vec<Patch>` with zero or more patches that, when
|
||||
/// applied to your real DOM, will make your real DOM look like your new virtual dom.
|
||||
///
|
||||
/// Each Patch has a u32 node index that helps us identify the real DOM node that it applies to.
|
||||
///
|
||||
/// Our old virtual dom's nodes are indexed depth first, as shown in this illustration
|
||||
/// (0 being the root node, 1 being it's first child, 2 being it's first child's first child).
|
||||
///
|
||||
/// ```text
|
||||
/// .─.
|
||||
/// ( 0 )
|
||||
/// `┬'
|
||||
/// ┌────┴──────┐
|
||||
/// │ │
|
||||
/// ▼ ▼
|
||||
/// .─. .─.
|
||||
/// ( 1 ) ( 4 )
|
||||
/// `┬' `─'
|
||||
/// ┌────┴───┐ │
|
||||
/// │ │ ├─────┬─────┐
|
||||
/// ▼ ▼ │ │ │
|
||||
/// .─. .─. ▼ ▼ ▼
|
||||
/// ( 2 ) ( 3 ) .─. .─. .─.
|
||||
/// `─' `─' ( 5 ) ( 6 ) ( 7 )
|
||||
/// `─' `─' `─'
|
||||
/// ```
|
||||
///
|
||||
/// The patching process is tested in a real browser in crates/virtual-dom-rs/tests/diff_patch.rs
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Patch<'a> {
|
||||
/// Append a vector of child nodes to a parent node id.
|
||||
AppendChildren(NodeIdx, Vec<&'a VirtualNode>),
|
||||
/// For a `node_i32`, remove all children besides the first `len`
|
||||
TruncateChildren(NodeIdx, usize),
|
||||
/// Replace a node with another node. This typically happens when a node's tag changes.
|
||||
/// ex: <div> becomes <span>
|
||||
Replace(NodeIdx, &'a VirtualNode),
|
||||
/// Add attributes that the new node has that the old node does not
|
||||
AddAttributes(NodeIdx, HashMap<&'a str, &'a str>),
|
||||
/// Remove attributes that the old node had that the new node doesn't
|
||||
RemoveAttributes(NodeIdx, Vec<&'a str>),
|
||||
/// Change the text of a Text node.
|
||||
ChangeText(NodeIdx, &'a VText),
|
||||
}
|
||||
|
||||
type NodeIdx = usize;
|
||||
|
||||
impl<'a> Patch<'a> {
|
||||
/// Every Patch is meant to be applied to a specific node within the DOM. Get the
|
||||
/// index of the DOM node that this patch should apply to. DOM nodes are indexed
|
||||
/// depth first with the root node in the tree having index 0.
|
||||
pub fn node_idx(&self) -> usize {
|
||||
match self {
|
||||
Patch::AppendChildren(node_idx, _) => *node_idx,
|
||||
Patch::TruncateChildren(node_idx, _) => *node_idx,
|
||||
Patch::Replace(node_idx, _) => *node_idx,
|
||||
Patch::AddAttributes(node_idx, _) => *node_idx,
|
||||
Patch::RemoveAttributes(node_idx, _) => *node_idx,
|
||||
Patch::ChangeText(node_idx, _) => *node_idx,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//! Ensure that our DomUpdater maintains Rc's to closures so that they work even
|
||||
//! after dropping virtual dom nodes.
|
||||
//!
|
||||
//! To run all tests in this file:
|
||||
//!
|
||||
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test closures
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::rc::Rc;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
use virtual_dom_rs::DomUpdater;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// TODO: This test current fails in headless browsers but works in non headless browsers
|
||||
// (tested in both geckodriver and chromedriver)
|
||||
// Need to figure out why
|
||||
#[wasm_bindgen_test]
|
||||
fn closure_not_dropped() {
|
||||
let text = Rc::new(RefCell::new("Start Text".to_string()));
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
let mut dom_updater = None;
|
||||
|
||||
{
|
||||
let mut input = make_input_component(Rc::clone(&text));
|
||||
input
|
||||
.as_velement_mut()
|
||||
.expect("Not an element")
|
||||
.attrs
|
||||
.insert("id".into(), "old-input-elem".into());
|
||||
|
||||
let mount = document.create_element("div").unwrap();
|
||||
mount.set_id("mount");
|
||||
document.body().unwrap().append_child(&mount).unwrap();
|
||||
|
||||
dom_updater = Some(DomUpdater::new_replace_mount(input, mount));
|
||||
|
||||
let mut dom_updater = dom_updater.as_mut().unwrap();
|
||||
|
||||
// Input VirtualNode from above gets dropped at the end of this block,
|
||||
// yet that element held Rc's to the Closure's that power the oninput event.
|
||||
//
|
||||
// We're patching the DOM with a new vdom, but since our new vdom doesn't contain any
|
||||
// new elements, `.create_element` won't get called and so no new Closures will be
|
||||
// created.
|
||||
//
|
||||
// So, we're testing that our old Closure's still work. The reason that they work is
|
||||
// that dom_updater maintains Rc's to those Closures.
|
||||
let mut new_node = make_input_component(Rc::clone(&text));
|
||||
new_node
|
||||
.as_velement_mut()
|
||||
.expect("Not an element")
|
||||
.attrs
|
||||
.insert("id".into(), "new-input-elem".into());
|
||||
|
||||
dom_updater.update(new_node);
|
||||
}
|
||||
|
||||
let dom_updater = dom_updater.as_ref().unwrap();
|
||||
|
||||
let input: HtmlInputElement = document
|
||||
.get_element_by_id("new-input-elem")
|
||||
.expect("Input element")
|
||||
.dyn_into()
|
||||
.unwrap();
|
||||
let input_event = InputEvent::new("input").unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "Start Text");
|
||||
|
||||
// After dispatching the oninput event our `text` should have a value of the input elements value.
|
||||
web_sys::EventTarget::from(input)
|
||||
.dispatch_event(&input_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "End Text");
|
||||
|
||||
assert_eq!(
|
||||
dom_updater.active_closures.get(&1).as_ref().unwrap().len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// We're just making sure that things compile - other tests give us confidence that the closure
|
||||
// will work just fine.
|
||||
//
|
||||
// https://github.com/chinedufn/percy/issues/81
|
||||
//
|
||||
//#[wasm_bindgen_test]
|
||||
//fn closure_with_no_params_compiles() {
|
||||
// let _making_sure_this_works = html! {
|
||||
// <div onclick=|| {}></div>
|
||||
// };
|
||||
//}
|
||||
|
||||
fn make_input_component(text_clone: Rc<RefCell<String>>) -> VirtualNode {
|
||||
html! {
|
||||
<input
|
||||
// On input we'll set our Rc<RefCell<String>> value to the input elements value
|
||||
oninput=move |event: Event| {
|
||||
let input_elem = event.target().unwrap();
|
||||
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
|
||||
*text_clone.borrow_mut() = input_elem.value();
|
||||
}
|
||||
value="End Text"
|
||||
>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Tests that ensure that we create the right DOM element from a VirtualNode
|
||||
//!
|
||||
//! To run all tests in this file:
|
||||
//!
|
||||
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate wasm_bindgen_test;
|
||||
extern crate web_sys;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::{Element, Event, EventTarget, MouseEvent};
|
||||
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element nested_divs
|
||||
#[wasm_bindgen_test]
|
||||
fn nested_divs() {
|
||||
let vdiv = html! { <div> <div> <div></div> </div> </div> };
|
||||
let div: Element = vdiv.create_dom_node().node.unchecked_into();
|
||||
|
||||
assert_eq!(&div.inner_html(), "<div><div></div></div>");
|
||||
}
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element svg_element
|
||||
/// TODO: Temporarily disabled until we figure out why it's failing in CI but not failing locally
|
||||
// #[wasm_bindgen_test]
|
||||
// fn svg_element() {
|
||||
// let vdiv = html! { <div><svg xmlns="http://www.w3.org/2000/svg">
|
||||
// <circle cx="50" cy="50" r="50"/>
|
||||
// </svg></div> };
|
||||
// let div: Element = vdiv.create_dom_node().node.unchecked_into();
|
||||
|
||||
// assert_eq!(
|
||||
// &div.inner_html(),
|
||||
// r#"<svg xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="50"></circle></svg>"#
|
||||
// );
|
||||
// }
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element div_with_attributes
|
||||
#[wasm_bindgen_test]
|
||||
fn div_with_attributes() {
|
||||
let vdiv = html! { <div id="id-here" class="two classes"></div> };
|
||||
let div: Element = vdiv.create_dom_node().node.unchecked_into();
|
||||
|
||||
assert_eq!(&div.id(), "id-here");
|
||||
|
||||
assert!(div.class_list().contains("two"));
|
||||
assert!(div.class_list().contains("classes"));
|
||||
|
||||
assert_eq!(div.class_list().length(), 2);
|
||||
}
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element click_event
|
||||
#[wasm_bindgen_test]
|
||||
fn click_event() {
|
||||
let clicked = Rc::new(Cell::new(false));
|
||||
let clicked_clone = Rc::clone(&clicked);
|
||||
|
||||
let div = html! {
|
||||
<div
|
||||
onclick=move |_ev: MouseEvent| {
|
||||
clicked_clone.set(true);
|
||||
}
|
||||
>
|
||||
</div>
|
||||
};
|
||||
|
||||
let click_event = Event::new("click").unwrap();
|
||||
|
||||
let div = div.create_dom_node().node;
|
||||
|
||||
(EventTarget::from(div))
|
||||
.dispatch_event(&click_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*clicked, Cell::new(true));
|
||||
}
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element inner_html
|
||||
/// @book start inner-html
|
||||
#[wasm_bindgen_test]
|
||||
fn inner_html() {
|
||||
let div = html! {
|
||||
<div
|
||||
unsafe_inner_html="<span>hi</span>"
|
||||
>
|
||||
</div>
|
||||
};
|
||||
let div: Element = div.create_dom_node().node.unchecked_into();
|
||||
|
||||
assert_eq!(div.inner_html(), "<span>hi</span>");
|
||||
}
|
||||
// @book end inner-html
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element on_create_elem
|
||||
/// @book start on-create-elem
|
||||
#[wasm_bindgen_test]
|
||||
fn on_create_elem() {
|
||||
let div = html! {
|
||||
<div
|
||||
on_create_elem=|elem: web_sys::Element| {
|
||||
elem.set_inner_html("Hello world");
|
||||
}
|
||||
>
|
||||
<span>This span should get replaced</span>
|
||||
</div>
|
||||
};
|
||||
let div: Element = div.create_dom_node().node.unchecked_into();
|
||||
|
||||
assert_eq!(div.inner_html(), "Hello world");
|
||||
}
|
||||
// @book end on-create-elem
|
||||
@@ -0,0 +1,197 @@
|
||||
//! Tests that ensure that diffing and patching work properly in a real browser.
|
||||
//!
|
||||
//! To run all tests in this file:
|
||||
//!
|
||||
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test diff_patch
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate wasm_bindgen_test;
|
||||
extern crate web_sys;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
mod diff_patch_test_case;
|
||||
use self::diff_patch_test_case::DiffPatchTest;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn replace_child() {
|
||||
DiffPatchTest {
|
||||
desc: "Replace a root node attribute attribute and a child text node",
|
||||
old: html! {
|
||||
<div>
|
||||
Original element
|
||||
</div>
|
||||
},
|
||||
new: html! { <div> Patched element</div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn truncate_children() {
|
||||
DiffPatchTest {
|
||||
desc: "Truncates extra children",
|
||||
old: html! {
|
||||
<div>
|
||||
<div> <div> <b></b> <em></em> </div> </div>
|
||||
</div>
|
||||
},
|
||||
new: html! {
|
||||
<div>
|
||||
<div> <div> <b></b> </div> </div>
|
||||
</div>
|
||||
},
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
|
||||
DiffPatchTest {
|
||||
desc: "https://github.com/chinedufn/percy/issues/48",
|
||||
old: html! {
|
||||
<div>
|
||||
ab <p></p> c
|
||||
</div>
|
||||
},
|
||||
new: html! {
|
||||
<div>
|
||||
ab <p></p>
|
||||
</div>
|
||||
},
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn remove_attributes() {
|
||||
DiffPatchTest {
|
||||
desc: "Removes attributes",
|
||||
old: html! { <div style=""> </div>
|
||||
},
|
||||
new: html! { <div></div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn append_children() {
|
||||
DiffPatchTest {
|
||||
desc: "Append a child node",
|
||||
old: html! { <div> </div>
|
||||
},
|
||||
new: html! { <div> <span></span> </div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn text_node_siblings() {
|
||||
// NOTE: Since there are two text nodes next to eachother we expect a `<!--ptns-->` separator in
|
||||
// between them.
|
||||
// @see virtual_node/mod.rs -> create_dom_node() for more information
|
||||
// TODO: A little more spacing than there should be in between the text nodes ... but doesn't
|
||||
// impact the user experience so we can look into that later..
|
||||
let override_expected = Some(
|
||||
r#"<div id="after"><span> The button has been clicked: <!--ptns--> world </span></div>"#,
|
||||
);
|
||||
|
||||
let old1 = VirtualNode::text("The button has been clicked: ");
|
||||
let old2 = VirtualNode::text("hello");
|
||||
|
||||
let new1 = VirtualNode::text("The button has been clicked: ");
|
||||
let new2 = VirtualNode::text("world");
|
||||
|
||||
DiffPatchTest {
|
||||
desc: "Diff patch on text node siblings",
|
||||
old: html! {
|
||||
<div id="before">
|
||||
<span> { {old1} {old2} } </span>
|
||||
</div>
|
||||
},
|
||||
new: html! {
|
||||
<div id="after">
|
||||
<span> { {new1} {new2} } </span>
|
||||
</div>
|
||||
},
|
||||
override_expected,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn append_text_node() {
|
||||
DiffPatchTest {
|
||||
desc: "Append text node",
|
||||
old: html! { <div> </div> },
|
||||
new: html! { <div> Hello </div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn append_sibling_text_nodes() {
|
||||
let text1 = VirtualNode::text("Hello");
|
||||
let text2 = VirtualNode::text("World");
|
||||
|
||||
DiffPatchTest {
|
||||
desc: "Append sibling text nodes",
|
||||
old: html! { <div> </div> },
|
||||
new: html! { <div> {text1} {text2} </div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn replace_with_children() {
|
||||
DiffPatchTest {
|
||||
desc: "Replace node that has children",
|
||||
old: html! { <table><tr><th>0</th></tr><tr><td>1</td></tr></table> },
|
||||
new: html! { <table><tr><td>2</td></tr><tr><th>3</th></tr></table> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
// https://github.com/chinedufn/percy/issues/62
|
||||
#[wasm_bindgen_test]
|
||||
fn replace_element_with_text_node() {
|
||||
DiffPatchTest {
|
||||
desc: "#62: Replace element with text node",
|
||||
old: html! { <span> <br> </span> },
|
||||
new: html! { <span> a </span> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
// https://github.com/chinedufn/percy/issues/68
|
||||
#[wasm_bindgen_test]
|
||||
fn text_root_node() {
|
||||
DiffPatchTest {
|
||||
desc: "Patching of text root node works",
|
||||
old: html! { Old text },
|
||||
new: html! { New text },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn replace_text_with_element() {
|
||||
DiffPatchTest {
|
||||
desc: "Replacing a text node with an element works",
|
||||
old: html! { <div>a</div> },
|
||||
new: html! { <div><br></div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Kept in its own file to more easily import into the book
|
||||
|
||||
use console_error_panic_hook;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// A test case that both diffing and patching are working in a real browser
|
||||
pub struct DiffPatchTest<'a> {
|
||||
/// Description of the test case.
|
||||
pub desc: &'static str,
|
||||
/// The old virtual node.
|
||||
pub old: VirtualNode,
|
||||
/// The new virtual node.
|
||||
pub new: VirtualNode,
|
||||
/// By default we generate the expected based on `new.to_string()`. You can
|
||||
/// use this field to override the expected HTML after patching.
|
||||
pub override_expected: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> DiffPatchTest<'a> {
|
||||
pub fn test(&mut self) {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
// Create a DOM node of the virtual root node
|
||||
let root_node: Node = self.old.create_dom_node().node;
|
||||
|
||||
// Clone since virtual_dom_rs::patch takes ownership of the root node.
|
||||
let patched_root_node: Node = root_node.clone();
|
||||
|
||||
// Generate patches
|
||||
let patches = virtual_dom_rs::diff(&self.old, &self.new);
|
||||
|
||||
// Patch our root node. It should now look like `self.new`
|
||||
virtual_dom_rs::patch(root_node, &patches);
|
||||
|
||||
// Determine the expected outer HTML
|
||||
let expected_outer_html = match self.override_expected {
|
||||
Some(ref expected) => expected.to_string(),
|
||||
None => self.new.to_string(),
|
||||
};
|
||||
|
||||
let actual_outer_html = match patched_root_node.node_type() {
|
||||
Node::ELEMENT_NODE => patched_root_node.unchecked_into::<Element>().outer_html(),
|
||||
Node::TEXT_NODE => patched_root_node.text_content().unwrap_or("".into()),
|
||||
_ => panic!("Unhandled node type"),
|
||||
};
|
||||
|
||||
assert_eq!(&actual_outer_html, &expected_outer_html, "{}", self.desc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Ensure that our DomUpdater maintains Rc's to closures so that they work even
|
||||
//! after dropping virtual dom nodes.
|
||||
//!
|
||||
//! To run all tests in this file:
|
||||
//!
|
||||
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test dom_updater
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
use console_error_panic_hook;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
use virtual_dom_rs::DomUpdater;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// Verify that our DomUpdater's patch method works.
|
||||
// We test a simple case here, since diff_patch.rs is responsible for testing more complex
|
||||
// diffing and patching.
|
||||
#[wasm_bindgen_test]
|
||||
fn patches_dom() {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
let vdom = html! { <div></div> };
|
||||
|
||||
let mut dom_updater = DomUpdater::new(vdom);
|
||||
|
||||
let new_vdom = html! { <div id="patched"></div> };
|
||||
dom_updater.update(new_vdom);
|
||||
|
||||
document
|
||||
.body()
|
||||
.unwrap()
|
||||
.append_child(&dom_updater.root_node());
|
||||
assert_eq!(document.query_selector("#patched").unwrap().is_some(), true);
|
||||
}
|
||||
|
||||
// When you replace a DOM node with another DOM node we need to make sure that the closures
|
||||
// from the new DOM node are stored by the DomUpdater otherwise they'll get dropped and
|
||||
// won't work.
|
||||
#[wasm_bindgen_test]
|
||||
fn updates_active_closure_on_replace() {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let body = document.body().unwrap();
|
||||
|
||||
let old = html! { <div> </div> };
|
||||
let mut dom_updater = DomUpdater::new_append_to_mount(old, &body);
|
||||
|
||||
let text = Rc::new(RefCell::new("Start Text".to_string()));
|
||||
let text_clone = Rc::clone(&text);
|
||||
|
||||
let id = "update-active-closures-on-replace";
|
||||
|
||||
{
|
||||
let replace_node = html! {
|
||||
<input
|
||||
id=id
|
||||
oninput=move |event: Event| {
|
||||
let input_elem = event.target().unwrap();
|
||||
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
|
||||
*text_clone.borrow_mut() = input_elem.value();
|
||||
}
|
||||
value="End Text"
|
||||
>
|
||||
};
|
||||
|
||||
// New node replaces old node.
|
||||
// We are testing that we've stored this new node's closures even though `new` will be dropped
|
||||
// at the end of this block.
|
||||
dom_updater.update(replace_node);
|
||||
}
|
||||
|
||||
let input_event = InputEvent::new("input").unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "Start Text");
|
||||
|
||||
// After dispatching the oninput event our `text` should have a value of the input elements value.
|
||||
let input = document.get_element_by_id(&id).unwrap();
|
||||
web_sys::EventTarget::from(input)
|
||||
.dispatch_event(&input_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "End Text");
|
||||
}
|
||||
|
||||
// When you replace a DOM node with another DOM node we need to make sure that the closures
|
||||
// from the new DOM node are stored by the DomUpdater otherwise they'll get dropped and
|
||||
// won't work.
|
||||
#[wasm_bindgen_test]
|
||||
fn updates_active_closures_on_append() {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let body = document.body().unwrap();
|
||||
|
||||
let old = html! { <div> </div> };
|
||||
let mut dom_updater = DomUpdater::new_append_to_mount(old, &body);
|
||||
|
||||
let text = Rc::new(RefCell::new("Start Text".to_string()));
|
||||
let text_clone = Rc::clone(&text);
|
||||
|
||||
let id = "update-active-closures-on-append";
|
||||
|
||||
{
|
||||
let append_node = html! {
|
||||
<div>
|
||||
<input
|
||||
id=id
|
||||
oninput=move |event: Event| {
|
||||
let input_elem = event.target().unwrap();
|
||||
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
|
||||
*text_clone.borrow_mut() = input_elem.value();
|
||||
}
|
||||
value="End Text"
|
||||
>
|
||||
</div>
|
||||
};
|
||||
|
||||
// New node gets appended into the DOM.
|
||||
// We are testing that we've stored this new node's closures even though `new` will be dropped
|
||||
// at the end of this block.
|
||||
dom_updater.update(append_node);
|
||||
}
|
||||
|
||||
let input_event = InputEvent::new("input").unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "Start Text");
|
||||
|
||||
// After dispatching the oninput event our `text` should have a value of the input elements value.
|
||||
let input = document.get_element_by_id(id).unwrap();
|
||||
web_sys::EventTarget::from(input)
|
||||
.dispatch_event(&input_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "End Text");
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate wasm_bindgen_test;
|
||||
extern crate web_sys;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::*;
|
||||
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// Make sure that we successfully attach an event listener and see it work.
|
||||
#[wasm_bindgen_test]
|
||||
fn on_input() {
|
||||
let text = Rc::new(RefCell::new("Start Text".to_string()));
|
||||
let text_clone = Rc::clone(&text);
|
||||
|
||||
let input = html! {
|
||||
<input
|
||||
// On input we'll set our Rc<RefCell<String>> value to the input elements value
|
||||
oninput=move |event: Event| {
|
||||
let input_elem = event.target().unwrap();
|
||||
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
|
||||
*text_clone.borrow_mut() = input_elem.value();
|
||||
}
|
||||
value="End Text"
|
||||
>
|
||||
};
|
||||
|
||||
let input_event = InputEvent::new("input").unwrap();
|
||||
let input = input.create_dom_node().node;
|
||||
|
||||
assert_eq!(&*text.borrow(), "Start Text");
|
||||
|
||||
// After dispatching the oninput event our `text` should have a value of the input elements value.
|
||||
web_sys::EventTarget::from(input)
|
||||
.dispatch_event(&input_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "End Text");
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "virtual-node"
|
||||
version = "0.2.7"
|
||||
description = "A standalone Virtual DOM"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/chinedufn/percy"
|
||||
documentation = "https://chinedufn.github.io/percy/api/virtual_node/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "0.2.33"
|
||||
html-validation = {path = "../html-validation", version = "0.1.1"}
|
||||
lazy_static = "1.0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Comment",
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlElement",
|
||||
"EventTarget",
|
||||
"HtmlCollection",
|
||||
"Node",
|
||||
"NodeList",
|
||||
"Text",
|
||||
"Window",
|
||||
"Event",
|
||||
"MouseEvent",
|
||||
"InputEvent",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,647 @@
|
||||
//! The virtual_node module exposes the `VirtualNode` struct and methods that power our
|
||||
//! virtual dom.
|
||||
|
||||
// TODO: A few of these dependencies (including js_sys) are used to power events.. yet events
|
||||
// only work on wasm32 targest. So we should start sprinkling some
|
||||
//
|
||||
// #[cfg(target_arch = "wasm32")]
|
||||
// #[cfg(not(target_arch = "wasm32"))]
|
||||
//
|
||||
// Around in order to get rid of dependencies that we don't need in non wasm32 targets
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub mod virtual_node_test_utils;
|
||||
|
||||
use web_sys::{self, Element, EventTarget, Node, Text};
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// 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.
|
||||
use lazy_static::lazy_static;
|
||||
lazy_static! {
|
||||
static ref ELEM_UNIQUE_ID: Mutex<u32> = Mutex::new(0);
|
||||
}
|
||||
|
||||
/// When building your views you'll typically use the `html!` macro to generate
|
||||
/// `VirtualNode`'s.
|
||||
///
|
||||
/// `html! { <div> <span></span> </div> }` really generates a `VirtualNode` with
|
||||
/// one child (span).
|
||||
///
|
||||
/// Later, on the client side, you'll use the `diff` and `patch` modules to
|
||||
/// update the real DOM with your latest tree of virtual nodes (virtual dom).
|
||||
///
|
||||
/// Or on the server side you'll just call `.to_string()` on your root virtual node
|
||||
/// in order to recursively render the node and all of its children.
|
||||
///
|
||||
/// TODO: Make all of these fields private and create accessor methods
|
||||
/// TODO: Create a builder to create instances of VirtualNode::Element with
|
||||
/// attrs and children without having to explicitly create a VElement
|
||||
#[derive(PartialEq)]
|
||||
pub enum VirtualNode {
|
||||
/// An element node (node type `ELEMENT_NODE`).
|
||||
Element(VElement),
|
||||
/// A text node (node type `TEXT_NODE`).
|
||||
///
|
||||
/// Note: This wraps a `VText` instead of a plain `String` in
|
||||
/// order to enable custom methods like `create_text_node()` on the
|
||||
/// wrapped type.
|
||||
Text(VText),
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct VElement {
|
||||
/// The HTML tag, such as "div"
|
||||
pub tag: String,
|
||||
/// HTML attributes such as id, class, style, etc
|
||||
pub attrs: HashMap<String, String>,
|
||||
/// Events that will get added to your real DOM element via `.addEventListener`
|
||||
pub events: Events,
|
||||
/// The children of this `VirtualNode`. So a <div> <em></em> </div> structure would
|
||||
/// have a parent div and one child, em.
|
||||
pub children: Vec<VirtualNode>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct VText {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl VirtualNode {
|
||||
/// Create a new virtual element node with a given tag.
|
||||
///
|
||||
/// These get patched into the DOM using `document.createElement`
|
||||
///
|
||||
/// ```ignore
|
||||
/// use virtual_dom_rs::VirtualNode;
|
||||
///
|
||||
/// let div = VirtualNode::element("div");
|
||||
/// ```
|
||||
pub fn element<S>(tag: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VirtualNode::Element(VElement::new(tag))
|
||||
}
|
||||
|
||||
/// Create a new virtual text node with the given text.
|
||||
///
|
||||
/// These get patched into the DOM using `document.createTextNode`
|
||||
///
|
||||
/// ```ignore
|
||||
/// use virtual_dom_rs::VirtualNode;
|
||||
///
|
||||
/// let div = VirtualNode::text("div");
|
||||
/// ```
|
||||
pub fn text<S>(text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VirtualNode::Text(VText::new(text.into()))
|
||||
}
|
||||
|
||||
/// Return a [`VElement`] reference, if this is an [`Element`] variant.
|
||||
///
|
||||
/// [`VElement`]: struct.VElement.html
|
||||
/// [`Element`]: enum.VirtualNode.html#variant.Element
|
||||
pub fn as_velement_ref(&self) -> Option<&VElement> {
|
||||
match self {
|
||||
VirtualNode::Element(ref element_node) => Some(element_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mutable [`VElement`] reference, if this is an [`Element`] variant.
|
||||
///
|
||||
/// [`VElement`]: struct.VElement.html
|
||||
/// [`Element`]: enum.VirtualNode.html#variant.Element
|
||||
pub fn as_velement_mut(&mut self) -> Option<&mut VElement> {
|
||||
match self {
|
||||
VirtualNode::Element(ref mut element_node) => Some(element_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [`VText`] reference, if this is an [`Text`] variant.
|
||||
///
|
||||
/// [`VText`]: struct.VText.html
|
||||
/// [`Text`]: enum.VirtualNode.html#variant.Text
|
||||
pub fn as_vtext_ref(&self) -> Option<&VText> {
|
||||
match self {
|
||||
VirtualNode::Text(ref text_node) => Some(text_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a mutable [`VText`] reference, if this is an [`Text`] variant.
|
||||
///
|
||||
/// [`VText`]: struct.VText.html
|
||||
/// [`Text`]: enum.VirtualNode.html#variant.Text
|
||||
pub fn as_vtext_mut(&mut self) -> Option<&mut VText> {
|
||||
match self {
|
||||
VirtualNode::Text(ref mut text_node) => Some(text_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create and return a `CreatedNode` instance (containing a DOM `Node`
|
||||
/// together with potentially related closures) for this virtual node.
|
||||
pub fn create_dom_node(&self) -> CreatedNode<Node> {
|
||||
match self {
|
||||
VirtualNode::Text(text_node) => {
|
||||
CreatedNode::without_closures(text_node.create_text_node())
|
||||
}
|
||||
VirtualNode::Element(element_node) => element_node.create_element_node().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by html-macro to insert space before text that is inside of a block that came after
|
||||
/// an open tag.
|
||||
///
|
||||
/// html! { <div> {world}</div> }
|
||||
///
|
||||
/// So that we end up with <div> world</div> when we're finished parsing.
|
||||
pub fn insert_space_before_text(&mut self) {
|
||||
match self {
|
||||
VirtualNode::Text(text_node) => {
|
||||
text_node.text = " ".to_string() + &text_node.text;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by html-macro to insert space after braced text if we know that the next block is
|
||||
/// another block or a closing tag.
|
||||
///
|
||||
/// html! { <div>{Hello} {world}</div> } -> <div>Hello world</div>
|
||||
/// html! { <div>{Hello} </div> } -> <div>Hello </div>
|
||||
///
|
||||
/// So that we end up with <div>Hello world</div> when we're finished parsing.
|
||||
pub fn insert_space_after_text(&mut self) {
|
||||
match self {
|
||||
VirtualNode::Text(text_node) => {
|
||||
text_node.text += " ";
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VElement {
|
||||
pub fn new<S>(tag: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VElement {
|
||||
tag: tag.into(),
|
||||
attrs: HashMap::new(),
|
||||
events: Events(HashMap::new()),
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a DOM element by recursively creating DOM nodes for this element and it's
|
||||
/// children, it's children's children, etc.
|
||||
pub fn create_element_node(&self) -> CreatedNode<Element> {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
let element = if html_validation::is_svg_namespace(&self.tag) {
|
||||
document
|
||||
.create_element_ns(Some("http://www.w3.org/2000/svg"), &self.tag)
|
||||
.unwrap()
|
||||
} else {
|
||||
document.create_element(&self.tag).unwrap()
|
||||
};
|
||||
|
||||
let mut closures = HashMap::new();
|
||||
|
||||
self.attrs.iter().for_each(|(name, value)| {
|
||||
if name == "unsafe_inner_html" {
|
||||
element.set_inner_html(value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
element
|
||||
.set_attribute(name, value)
|
||||
.expect("Set element attribute in create element");
|
||||
});
|
||||
|
||||
if self.events.0.len() > 0 {
|
||||
let unique_id = create_unique_identifier();
|
||||
|
||||
element
|
||||
.set_attribute("data-vdom-id".into(), &unique_id.to_string())
|
||||
.expect("Could not set attribute on element");
|
||||
|
||||
closures.insert(unique_id, vec![]);
|
||||
|
||||
self.events.0.iter().for_each(|(onevent, callback)| {
|
||||
// onclick -> click
|
||||
let event = &onevent[2..];
|
||||
|
||||
let current_elem: &EventTarget = element.dyn_ref().unwrap();
|
||||
|
||||
current_elem
|
||||
.add_event_listener_with_callback(
|
||||
event,
|
||||
callback.as_ref().as_ref().unchecked_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
closures
|
||||
.get_mut(&unique_id)
|
||||
.unwrap()
|
||||
.push(Rc::clone(callback));
|
||||
});
|
||||
}
|
||||
|
||||
let mut previous_node_was_text = false;
|
||||
|
||||
self.children.iter().for_each(|child| {
|
||||
match child {
|
||||
VirtualNode::Text(text_node) => {
|
||||
let current_node = element.as_ref() as &web_sys::Node;
|
||||
|
||||
// We ensure that the text siblings are patched by preventing the browser from merging
|
||||
// neighboring text nodes. Originally inspired by some of React's work from 2016.
|
||||
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
|
||||
// -> https://github.com/facebook/react/pull/5753
|
||||
//
|
||||
// `ptns` = Percy text node separator
|
||||
if previous_node_was_text {
|
||||
let separator = document.create_comment("ptns");
|
||||
current_node
|
||||
.append_child(separator.as_ref() as &web_sys::Node)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
current_node
|
||||
.append_child(&text_node.create_text_node())
|
||||
.unwrap();
|
||||
|
||||
previous_node_was_text = true;
|
||||
}
|
||||
VirtualNode::Element(element_node) => {
|
||||
previous_node_was_text = false;
|
||||
|
||||
let child = element_node.create_element_node();
|
||||
let child_elem: Element = child.node;
|
||||
|
||||
closures.extend(child.closures);
|
||||
|
||||
element.append_child(&child_elem).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(on_create_elem) = self.events.0.get("on_create_elem") {
|
||||
let on_create_elem: &js_sys::Function =
|
||||
on_create_elem.as_ref().as_ref().unchecked_ref();
|
||||
on_create_elem
|
||||
.call1(&wasm_bindgen::JsValue::NULL, &element)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
CreatedNode {
|
||||
node: element,
|
||||
closures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VText {
|
||||
/// Create an new `VText` instance with the specified text.
|
||||
pub fn new<S>(text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VText { text: text.into() }
|
||||
}
|
||||
|
||||
/// Return a `Text` element from a `VirtualNode`, typically right before adding it
|
||||
/// into the DOM.
|
||||
pub fn create_text_node(&self) -> Text {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
document.create_text_node(&self.text)
|
||||
}
|
||||
}
|
||||
|
||||
/// A node along with all of the closures that were created for that
|
||||
/// node's events and all of it's child node's events.
|
||||
pub struct CreatedNode<T> {
|
||||
/// A `Node` or `Element` that was created from a `VirtualNode`
|
||||
pub node: T,
|
||||
/// A map of a node's unique identifier along with all of the Closures for that node.
|
||||
///
|
||||
/// The DomUpdater uses this to look up nodes and see if they're still in the page. If not
|
||||
/// the reference that we maintain to their closure will be dropped, thus freeing the Closure's
|
||||
/// memory.
|
||||
pub closures: HashMap<u32, Vec<DynClosure>>,
|
||||
}
|
||||
|
||||
impl<T> CreatedNode<T> {
|
||||
pub fn without_closures<N: Into<T>>(node: N) -> Self {
|
||||
CreatedNode {
|
||||
node: node.into(),
|
||||
closures: HashMap::with_capacity(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for CreatedNode<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.node
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreatedNode<Element>> for CreatedNode<Node> {
|
||||
fn from(other: CreatedNode<Element>) -> CreatedNode<Node> {
|
||||
CreatedNode {
|
||||
node: other.node.into(),
|
||||
closures: other.closures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_unique_identifier() -> u32 {
|
||||
let mut elem_unique_id = ELEM_UNIQUE_ID.lock().unwrap();
|
||||
|
||||
*elem_unique_id += 1;
|
||||
|
||||
*elem_unique_id
|
||||
}
|
||||
|
||||
/// A trait with common functionality for rendering front-end views.
|
||||
pub trait View {
|
||||
/// Render a VirtualNode, or any IntoIter<VirtualNode>
|
||||
fn render(&self) -> VirtualNode;
|
||||
}
|
||||
|
||||
impl<V> From<&V> for VirtualNode
|
||||
where
|
||||
V: View,
|
||||
{
|
||||
fn from(v: &V) -> Self {
|
||||
v.render()
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by the html! macro for all braced child nodes so that we can use any type
|
||||
/// that implements Into<IterableNodes>
|
||||
///
|
||||
/// html! { <div> { nodes } </div> }
|
||||
///
|
||||
/// nodes can be a String .. VirtualNode .. Vec<VirtualNode> ... etc
|
||||
pub struct IterableNodes(Vec<VirtualNode>);
|
||||
|
||||
impl IterableNodes {
|
||||
/// Retrieve the first node mutably
|
||||
pub fn first(&mut self) -> &mut VirtualNode {
|
||||
self.0.first_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Retrieve the last node mutably
|
||||
pub fn last(&mut self) -> &mut VirtualNode {
|
||||
self.0.last_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for IterableNodes {
|
||||
type Item = VirtualNode;
|
||||
// TODO: Is this possible with an array [VirtualNode] instead of a vec?
|
||||
type IntoIter = ::std::vec::IntoIter<VirtualNode>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VirtualNode> for IterableNodes {
|
||||
fn from(other: VirtualNode) -> Self {
|
||||
IterableNodes(vec![other])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for IterableNodes {
|
||||
fn from(other: &str) -> Self {
|
||||
IterableNodes(vec![VirtualNode::text(other)])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for IterableNodes {
|
||||
fn from(other: String) -> Self {
|
||||
IterableNodes(vec![VirtualNode::text(other.as_str())])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<VirtualNode>> for IterableNodes {
|
||||
fn from(other: Vec<VirtualNode>) -> Self {
|
||||
IterableNodes(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> From<Vec<V>> for IterableNodes {
|
||||
fn from(other: Vec<V>) -> Self {
|
||||
IterableNodes(other.into_iter().map(|it| it.render()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> From<&Vec<V>> for IterableNodes {
|
||||
fn from(other: &Vec<V>) -> Self {
|
||||
IterableNodes(other.iter().map(|it| it.render()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View> From<&[V]> for IterableNodes {
|
||||
fn from(other: &[V]) -> Self {
|
||||
IterableNodes(other.iter().map(|it| it.render()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VText> for VirtualNode {
|
||||
fn from(other: VText) -> Self {
|
||||
VirtualNode::Text(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VElement> for VirtualNode {
|
||||
fn from(other: VElement) -> Self {
|
||||
VirtualNode::Element(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for VirtualNode {
|
||||
fn from(other: &str) -> Self {
|
||||
VirtualNode::text(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for VirtualNode {
|
||||
fn from(other: String) -> Self {
|
||||
VirtualNode::text(other.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for VText {
|
||||
fn from(text: &str) -> Self {
|
||||
VText {
|
||||
text: text.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for VText {
|
||||
fn from(text: String) -> Self {
|
||||
VText { text }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for VirtualNode {
|
||||
type Item = VirtualNode;
|
||||
// TODO: Is this possible with an array [VirtualNode] instead of a vec?
|
||||
type IntoIter = ::std::vec::IntoIter<VirtualNode>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
vec![self].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<::std::vec::IntoIter<VirtualNode>> for VirtualNode {
|
||||
fn into(self) -> ::std::vec::IntoIter<VirtualNode> {
|
||||
self.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VirtualNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
VirtualNode::Element(e) => write!(f, "Node::{:?}", e),
|
||||
VirtualNode::Text(t) => write!(f, "Node::{:?}", t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VElement {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Element(<{}>, attrs: {:?}, children: {:?})",
|
||||
self.tag, self.attrs, self.children,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VText {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Text({})", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VElement {
|
||||
// Turn a VElement and all of it's children (recursively) into an HTML string
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "<{}", self.tag).unwrap();
|
||||
|
||||
for (attr, value) in self.attrs.iter() {
|
||||
write!(f, r#" {}="{}""#, attr, value)?;
|
||||
}
|
||||
|
||||
write!(f, ">")?;
|
||||
|
||||
for child in self.children.iter() {
|
||||
write!(f, "{}", child.to_string())?;
|
||||
}
|
||||
|
||||
if !html_validation::is_self_closing(&self.tag) {
|
||||
write!(f, "</{}>", self.tag)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a VText into an HTML string
|
||||
impl fmt::Display for VText {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a VirtualNode into an HTML string (delegate impl to variants)
|
||||
impl fmt::Display for VirtualNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
VirtualNode::Element(element) => write!(f, "{}", element),
|
||||
VirtualNode::Text(text) => write!(f, "{}", text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Box<dyn AsRef<JsValue>>> is our js_sys::Closure. Stored this way to allow us to store
|
||||
/// any Closure regardless of the arguments.
|
||||
pub type DynClosure = Rc<dyn AsRef<JsValue>>;
|
||||
|
||||
/// We need a custom implementation of fmt::Debug since JsValue doesn't
|
||||
/// implement debug.
|
||||
pub struct Events(pub HashMap<String, DynClosure>);
|
||||
|
||||
impl PartialEq for Events {
|
||||
// TODO: What should happen here..? And why?
|
||||
fn eq(&self, _rhs: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Events {
|
||||
// Print out all of the event names for this VirtualNode
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let events: String = self.0.keys().map(|key| " ".to_string() + key).collect();
|
||||
write!(f, "{}", events)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn self_closing_tag_to_string() {
|
||||
let node = VirtualNode::element("br");
|
||||
|
||||
// No </br> since self closing tag
|
||||
assert_eq!(&node.to_string(), "<br>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let mut node = VirtualNode::Element(VElement::new("div"));
|
||||
node.as_velement_mut()
|
||||
.unwrap()
|
||||
.attrs
|
||||
.insert("id".into(), "some-id".into());
|
||||
|
||||
let mut child = VirtualNode::Element(VElement::new("span"));
|
||||
|
||||
let mut text = VirtualNode::Text(VText::new("Hello world"));
|
||||
|
||||
child.as_velement_mut().unwrap().children.push(text);
|
||||
|
||||
node.as_velement_mut().unwrap().children.push(child);
|
||||
|
||||
let expected = r#"<div id="some-id"><span>Hello world</span></div>"#;
|
||||
|
||||
assert_eq!(node.to_string(), expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
//! A collection of functions that are useful for unit testing your html! views.
|
||||
|
||||
use crate::VirtualNode;
|
||||
|
||||
impl VirtualNode {
|
||||
/// Get a vector of all of the VirtualNode children / grandchildren / etc of
|
||||
/// your virtual_node that have a label that matches your filter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # #[macro_use] extern crate virtual_dom_rs; fn main() {
|
||||
///
|
||||
/// let component = html! {<div>
|
||||
/// <span label="hello",> {"Hi!"} </span>
|
||||
/// <em label="world",> {"There!!"} </em>
|
||||
/// <em label="hello",></em>
|
||||
/// </div> };
|
||||
///
|
||||
/// let hello_nodes = component.filter_label(|label| {
|
||||
/// label.contains("hello")
|
||||
/// });
|
||||
///
|
||||
/// assert_eq!(hello_nodes.len(), 2);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn filter_label<'a, F>(&'a self, filter: F) -> Vec<&'a VirtualNode>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
// Get descendants recursively
|
||||
let mut descendants: Vec<&'a VirtualNode> = vec![];
|
||||
match self {
|
||||
VirtualNode::Text(_) => { /* nothing to do */ }
|
||||
VirtualNode::Element(element_node) => {
|
||||
for child in element_node.children.iter() {
|
||||
get_descendants(&mut descendants, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter descendants
|
||||
descendants
|
||||
.into_iter()
|
||||
.filter(|vn: &&'a VirtualNode| match vn {
|
||||
VirtualNode::Text(_) => false,
|
||||
VirtualNode::Element(element_node) => match element_node.attrs.get("label") {
|
||||
Some(label) => filter(label),
|
||||
None => false,
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a vector of all of the descendants of this VirtualNode
|
||||
/// that have the provided `filter`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # #[macro_use] extern crate virtual_dom_rs; fn main() {
|
||||
///
|
||||
/// let component = html! {<div>
|
||||
/// <span label="hello",> {"Hi!"} </span>
|
||||
/// <em label="world",> {"There!!"} </em>
|
||||
/// <em label="hello",></em>
|
||||
/// </div> };
|
||||
///
|
||||
/// let hello_nodes = component.filter_label_equals("hello");
|
||||
///
|
||||
/// assert_eq!(hello_nodes.len(), 2);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn filter_label_equals<'a>(&'a self, label: &str) -> Vec<&'a VirtualNode> {
|
||||
self.filter_label(|node_label| node_label == label)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_descendants<'a>(descendants: &mut Vec<&'a VirtualNode>, node: &'a VirtualNode) {
|
||||
descendants.push(node);
|
||||
match node {
|
||||
VirtualNode::Text(_) => { /* nothing to do */ }
|
||||
VirtualNode::Element(element_node) => {
|
||||
for child in element_node.children.iter() {
|
||||
get_descendants(descendants, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::VElement;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// TODO: Move this test somewhere that we can use the `html!` macro
|
||||
// #[test]
|
||||
// fn filter_label() {
|
||||
// let html = html! {
|
||||
// // Should not pick up labels on the root node
|
||||
// <div label="hello0",>
|
||||
// // This node gets picked up
|
||||
// <span label="hello1",>
|
||||
// </span>
|
||||
// // This node gets picked up
|
||||
// <em label="hello2",>
|
||||
// { "hello there :)!" }
|
||||
// </em>
|
||||
// <div label="world",></div>
|
||||
// </div>
|
||||
// };
|
||||
//
|
||||
// let hello_nodes = html.filter_label(|label| label.contains("hello"));
|
||||
//
|
||||
// assert_eq!(
|
||||
// hello_nodes.len(),
|
||||
// 2,
|
||||
// "2 elements with label containing 'hello'"
|
||||
// );
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn label_equals() {
|
||||
let span = VirtualNode::element("span");
|
||||
|
||||
let mut attrs = HashMap::new();
|
||||
attrs.insert("label".to_string(), "hello".to_string());
|
||||
let mut em = VElement::new("em");
|
||||
em.attrs = attrs;
|
||||
|
||||
let mut html = VElement::new("div");
|
||||
html.children.push(span);
|
||||
html.children.push(em.into());
|
||||
|
||||
let html_node = VirtualNode::Element(html);
|
||||
let hello_nodes = html_node.filter_label_equals("hello");
|
||||
|
||||
assert_eq!(hello_nodes.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user