Feat: docs, code frm percy

This commit is contained in:
Jonathan Kelley committed 2021-01-14 02:56:41 -05:00
commit 2b9c8d09d9
72 files changed
+5130

No files matched your search

+11
View File
@@ -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"
+22
View File
@@ -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:
+1
View File
@@ -0,0 +1 @@
+243
View File
@@ -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);
}
}
}
+9
View File
@@ -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]
+7
View File
@@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
+11
View File
@@ -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"
+3
View File
@@ -0,0 +1,3 @@
# html-macro-test
Unit tests for the `html!` macro
+13
View File
@@ -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;
+3
View File
@@ -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();
}
+171
View File
@@ -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>"
)
}
+7
View File
@@ -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>
| ^^^^^^
+19
View File
@@ -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"}
+20
View File
@@ -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);
}
```
+55
View File
@@ -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 })
}
}
+102
View File
@@ -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);
}
}
}
+270
View File
@@ -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)
}
+152
View File
@@ -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));
}
}
}
+96
View File
@@ -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
}
}
}
+310
View File
@@ -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,
})
}
+12
View File
@@ -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"
+42
View File
@@ -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)
}
+9
View File
@@ -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]
View File
Whitespace-only changes.
+9
View File
@@ -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]
+7
View File
@@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
+9
View File
@@ -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]
+7
View File
@@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
+58
View File
@@ -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)
+47
View File
@@ -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",
]
+3
View File
@@ -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);
}
}
+351
View File
@@ -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",
// })
// }
}
+103
View File
@@ -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()
}
}
+44
View File
@@ -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(())
}
+76
View File
@@ -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,
}
}
}
+116
View File
@@ -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
+197
View File
@@ -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");
}
+47
View File
@@ -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");
}
+35
View File
@@ -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",
]
+647
View File
@@ -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);
}
}