commit 2b9c8d09d926ff6b5ad8a7e7b7b0b6f93bb8eb36 Author: Jonathan Kelley Date: Thu Jan 14 02:56:41 2021 -0500 Feat: docs, code frm percy diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..16d56367c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.DS_Store diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..cbec6c247 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = [ + # Built-n + "packages/core", + "packages/macro", + "packages/hooks", + "packages/recoil", + "packages/redux", + # Pulled from percy + "packages/html-macro", + "packages/html-macro-test", + "packages/virtual-dom-rs", + "packages/virtual-node", +] diff --git a/README.md b/README.md new file mode 100644 index 000000000..ce02496cf --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Dioxus: A concurrent, functional, arena-allocated VDOM implementation for creating UIs in Rust + +Dioxus is a new approach for creating performant cross platform user experiences in Rust. In Dioxus, the UI is represented by a tree of Virtual Nodes not bound to any renderer framework. Instead, external renderers can leverage Dioxus' virtual DOM and event system as a source of truth for rendering to a medium of their choice. Developers used to crafting react-based experiences should feel comfortable with Dioxus. + +## Hello World +Dioxus should look and feel just like writing functional React components. In Dioxus, there are no class components with lifecycles. All state management is done via hooks. This encourages logic resusability and lessens the burden on Dioxus to maintain a non-breaking lifecycle API. + +```rust +#[derive(Properties, PartialEq)] +struct MyProps { + name: String +} + +fn Example(ctx: Context) -> VNode { + html! {
"Hello {ctx.props().name}!"
} +} +``` + + + + + + + + + + +To build user interfaces, you must provide a way of creating VNodes. We provide a macro `dioxus-rsx` which makes it easy to drop in html templates and event listeners to make interactive user experiences. + +Inspired by React's Concurrent Mode, components in Dioxus are asynchronous by default. When components need to load asynchronous data, their rendering will be halted until ready, leading to fewer DOM updates and greater performance. External crates can tap into this system using futures to craft useful transition-based hooks. + +Rules of Dioxus: +- Every component is asynchronous +- Components will queued when completed +- + +Dioxus supports: +- Hooks +- Concurrent rendering +- Context subscriptions + + diff --git a/docs/1-hello-world.md b/docs/1-hello-world.md new file mode 100644 index 000000000..f265f63f5 --- /dev/null +++ b/docs/1-hello-world.md @@ -0,0 +1,16 @@ +# Hello, World! + +Dioxus should look and feel just like writing functional React components. In Dioxus, there are no class components with lifecycles. All state management is done via hooks. This encourages logic resusability and lessens the burden on Dioxus to maintain a non-breaking lifecycle API. + +```rust +#[derive(Properties, PartialEq)] +struct MyProps { + name: String +} + +fn Example(ctx: Context) -> VNode { + html! {
"Hello {ctx.props().name}!"
} +} +``` + +For functions to be valid components, they must take the `Context` object which is generic over some properties. The properties parameter must implement the `Properties` trait, which can be automatically derived. Whenever the input properties of a component changes, the function component will be re-ran and a new set of VNodes will be generated. diff --git a/docs/10-concurrent-mode.md b/docs/10-concurrent-mode.md new file mode 100644 index 000000000..5154f04d8 --- /dev/null +++ b/docs/10-concurrent-mode.md @@ -0,0 +1,84 @@ +# Concurrent mode + +Concurrent mode provides a mechanism for building efficient asynchronous components. With this feature, components don't need to render immediately, and instead can schedule a future render by returning a future. + +To make a component asynchronous, simply change its function signature to async. + +```rust +fn Example(ctx: Context<()>) -> Vnode { + rsx!{
"Hello world!"
} +} +``` +becomes + +```rust +async fn Example(ctx: Context<()>) -> Vnode { + rsx!{
"Hello world!"
} +} +``` + +Now, logic in components can be awaited to delay updates of the component and its children. Like so: + +```rust +async fn Example(ctx: Context<()>) -> Vnode { + let name = fetch_name().await; + rsx!{
"Hello {name}"
} +} + +async fetch_name() -> String { + // ... +} +``` + +This component will only schedule its render once the fetch is complete. However, we *don't* recommend using async/await directly in your components. + +Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, and cause confusion. + +Instead, we suggest using hooks and future combinators that can safely utilize the safe guards of the component's Context when interacting with async tasks. + +As part of our Dioxus hooks crate, we provide a data loader hook which pauses a component until its async dependencies are ready. This caches requests, reruns the fetch if dependencies have changed, and provides the option to render something else while the component is loading. + +```rust +async fn ExampleLoader(ctx: Context<()>) -> Vnode { + /* + Fetch, pause the component from rendering at all. + + The component is locked while waiting for the request to complete + While waiting, an alternate component is scheduled in its place. + + This API stores the result on the Context object, so the loaded data is taken as reference. + */ + let name: &Result = use_fetch_data("http://example.com/json", ()) + .place_holder(|ctx| rsx!{
"loading..."
}) + .delayed_place_holder(1000, |ctx| rsx!{
"still loading..."
}) + .await; + + match name { + Ok(name) => rsx! {
"Hello {something}"
}, + Err(e) => rsx! {
"An error occured :("
} + } +} +``` + + + + +```rust +async fn Example(ctx: Context<()>) -> VNode { + // Diff this set between the last set + // Check if we have any outstanding tasks? + // + // Eventually, render the component into the VDOM when the future completes +
+ +
+ + // Render a div, queue a component + // Render the placeholder first, then when the component is ready, then render the component +
+ "Loading"
}}> + + + +} +``` diff --git a/docs/11-arena-memo.md b/docs/11-arena-memo.md new file mode 100644 index 000000000..bb953ef3d --- /dev/null +++ b/docs/11-arena-memo.md @@ -0,0 +1,56 @@ +# Memoization and the arena allocator + +Dioxus differs slightly from other UI virtual doms in some subtle ways due to its memory allocator. + +One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to any and allocated on the heap - not in the arena with the components. + +With this system, we try to be more efficient when leaving the component arena and entering the heap. By default, props are memoized between renders using COW and context. This makes props comparisons fast - done via ptr comparisons on the cow pointer. Because memoization is done by default, parent re-renders will *not* cascade to children if the child's props did not change. + +https://dmitripavlutin.com/use-react-memo-wisely/ + +This behavior is defined as an implicit attribute to user components. When in React land you might wrap a component is `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization. + +```rust +fn test() -> VNode { + html! { + <> + + // same as + + + } +} + +static TestComponent: FC<()> = |ctx| html!{
"Hello world"
}; + +static TestComponent: FC<()> = |ctx| { + let g = "BLAH"; + html! { +
"Hello world"
+ } +}; + +#[functional_component] +static TestComponent: FC<{ name: String }> = |ctx| html! {
"Hello {name}"
}; +``` + +## Why this behavior? + +"This is different than React, why differ?". + +Take a component likes this: + +```rust +fn test(ctx: Context<()>) -> VNode { + let Bundle { alpha, beta, gamma } = use_context::(ctx); + html! { +
+ + + +
+ } +} +``` + +While the contents of the destructued bundle might change, not every child component will need to be re-rendered. diff --git a/docs/2-utilites.md b/docs/2-utilites.md new file mode 100644 index 000000000..39baf3d4c --- /dev/null +++ b/docs/2-utilites.md @@ -0,0 +1,58 @@ +# Utilities + +There are a few macros and utility functions that make life easier when writing Dioxus components. + + +## The `functional_component` procedural macro +The `functional_component` proc macro allows you to inline props into the generic parameter of the component's context. This is useful When writing "pure" components, or when you don't want the extra clutter of structs, derives, and burden of naming things. + +This macro allows allows a classic struct definition to be embedded directly into the function arguments. The props are automatically pulled from the context and destructured into the function's body, saving an extra step. + +```rust +// Inlines and destructure props *automatically* +#[functional_component] +fn Example(ctx: &mut Context<{ + name: String + pending: bool + count: i32 +}>) -> VNode { + html! { +
+

"Hello, {name}!"

+

"Status: {pending}!"

+

"Count {count}!"

+
+ } +} +``` + +becomes this: + +```rust +#[derive(Debug, Properties, PartialEq)] +struct ExampleProps { + name: String + pending: bool + count: i32 +}; + +fn Example(ctx: &mut Context) -> VNode { + let ExampleProps { + name, pending, count + } = ctx.props(); + + rsx! { +
+

"Hello, {name}!"

+

"Status: {pending}!"

+

"Count {count}!"

+
+ } +} +``` + + +## The rsx! macro + +The rsx! macacro is similar to the html! macro in other libraries, but with a few add-ons to make it fun and easy to work with. We'll cover the rsx macro more in depth in the [vnode-macro](3-vnode-macros.md) chapter. + diff --git a/docs/3-vnode-macros.md b/docs/3-vnode-macros.md new file mode 100644 index 000000000..1e0ca6a5f --- /dev/null +++ b/docs/3-vnode-macros.md @@ -0,0 +1 @@ +# diff --git a/docs/4-hooks.md b/docs/4-hooks.md new file mode 100644 index 000000000..b2cd76f6e --- /dev/null +++ b/docs/4-hooks.md @@ -0,0 +1,17 @@ + +```rust +fn Example(ctx: &mut Context<()>) -> VNode { + let service = use_combubulator(ctx); + let Status { name, pending, count } = service.info(); + html! { +
+

"Hello, {name}!"

+

"Status: {pending}!"

+

"Count {count}!"

+ +
+ } +} +``` diff --git a/docs/5-context-api.md b/docs/5-context-api.md new file mode 100644 index 000000000..a34b47f96 --- /dev/null +++ b/docs/5-context-api.md @@ -0,0 +1,47 @@ +# Context API + + + +```rust +// Create contexts available to children +// Only one context can be associated with any given component +// This is known as "exposed state". Children can access this context, +// but will not be automatically subscribed. +fn ContextCreate(ctx: &mut Context<()>) -> VNode { + let context = ctx.set_context(|| CustomContext::new()); + html! { <> {ctx.children()} } +} + +fn ContextRead(ctx: &mut Context<()>) -> VNode { + // Panics if context is not available + let some_ctx = ctx.get_context::(); + let text = some_ctx.select("some_selector"); + html! {
"{text}"
} +} + +fn Subscription(ctx: &mut Context<()>) -> VNode { + // Open a "port" on the component for actions to trigger a re-evaluation + let subscription = ctx.new_subscription(); + + // A looping timer - the effect is re-called on every re-evaluation + use_async_effect(ctx, move || async { + timer::new(2000).await; + subscription.call(); + }, None); + + // A one-shot timer, the deps don't change so the effect only happens once + use_async_effect_deps(ctx, move || async { + timer::new(2000).await; + subscription.call(); + }, ()); +} + +// Mix subscriptions and context to make a simple Redux +fn use_global_state(ctx: &mut Context<()>) -> T { + let some_ctx = ctx.get_context::(); + let component_subscription = ctx.new_subscription(); + some_ctx.subscribe_component(component_subscription); + some_ctx +} + +``` diff --git a/docs/6-subscription-api.md b/docs/6-subscription-api.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/7-state-management.md b/docs/7-state-management.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/8-custom-renderer.md b/docs/8-custom-renderer.md new file mode 100644 index 000000000..577b4f5d3 --- /dev/null +++ b/docs/8-custom-renderer.md @@ -0,0 +1 @@ +# Custom Renderer diff --git a/docs/9-interactivity.md b/docs/9-interactivity.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml new file mode 100644 index 000000000..7d4e7c976 --- /dev/null +++ b/packages/core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dioxus-core" +version = "0.1.0" +authors = ["Jonathan Kelley "] +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" diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 000000000..aa34e371e --- /dev/null +++ b/packages/core/README.md @@ -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: + + diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/core/src/lib.rs @@ -0,0 +1 @@ + diff --git a/packages/core/src/old.rs b/packages/core/src/old.rs new file mode 100644 index 000000000..adecc96b9 --- /dev/null +++ b/packages/core/src/old.rs @@ -0,0 +1,243 @@ +mod old { + + // #![feature(type_alias_impl_trait)] + // + use std::future::Future; + + trait Props {} + struct Context { + _props: std::marker::PhantomData, + } + struct VNode {} + + // type FC = fn(&mut Context) -> VNode; + // type FC = fn(&mut Context) -> Box>; + + impl Props for () {} + + // async fn some_component(g: &mut Context<()>) -> VNode { + // rsx! { + //
+ + //
+ // } + // } + // Absolve ourselves of any type data about the context itself + trait ContextApplier { + fn use_hook( + &mut self, + initializer: impl FnOnce() -> H, + runner: impl Fn(&mut H) -> O, + tear_down: impl Fn(&mut H), + ) -> O; + } + impl ContextApplier for Context { + fn use_hook( + &mut self, + initializer: impl FnOnce() -> H, + runner: impl Fn(&mut H) -> O, + tear_down: impl Fn(&mut H), + ) -> O { + todo!() + } + } + + fn use_state(c: &mut impl ContextApplier, g: impl Fn() -> T) -> T { + c.use_hook(|| {}, |_| {}, |_| {}); + g() + } + + enum SomeComponent { + Imperative, + Async, + } + + // impl From for SomeComponent + // where + // F: Fn() -> G, + // G: Future, + // { + // fn from(_: F) -> Self { + // SomeComponent::Async + // } + // } + + // impl From ()> for SomeComponent { + // fn from(_: F) -> Self { + // SomeComponent::Async + // } + // } + // impl Into for fn() -> F + // where + // F: Future, + // { + // 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! { + //
+ // "hello world!" + //
+ // } + // } + + // fn use_state(c: &mut Context) {} + + // 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! { + // "Loading..."}> + //
+ // "hello {name}!" + //
+ // + // } + // } + + /* + + Rationale + Today, you can do use_async and do some async operations, + + + + + + + + */ + // type FC = fn(&mut Context

) -> VNode; + + // static Example: FC<()> = |_| async { + // // some async work + // }; + + // type FC2 = fn() -> impl Future; + // struct fc(fn(&mut Context

) -> G); + // fn blah>(a: fn(&mut Context

) -> 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> = fn(i32) -> R; + + // async fn my_component() + // static MyThing: FA> = |_| async { 10 }; + + // type SomeFn = fn() -> (); + + // static MyFn: SomeFn = || {}; +} + +mod old2 { + mod vdom { + //! Virtual DOM implementation + use super::*; + + pub struct VDom { + patches: Vec, + } + + 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, + + /// Direct children, non arena-allocated + children: Vec, + } + + 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); + } + } +} diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml new file mode 100644 index 000000000..ad42c35f4 --- /dev/null +++ b/packages/hooks/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dioxus-hooks" +version = "0.0.0" +authors = ["Jonathan Kelley "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] \ No newline at end of file diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs new file mode 100644 index 000000000..31e1bb209 --- /dev/null +++ b/packages/hooks/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/packages/html-macro-test/Cargo.toml b/packages/html-macro-test/Cargo.toml new file mode 100644 index 000000000..318551fff --- /dev/null +++ b/packages/html-macro-test/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "html-macro-test" +version = "0.1.0" +authors = ["Chinedu Francis Nwafili "] +edition = "2018" + +[dependencies] +html-macro = {path = "../html-macro"} +virtual-dom-rs = { path = "../virtual-dom-rs" } +virtual-node = {path = "../virtual-node"} +trybuild = "1.0" diff --git a/packages/html-macro-test/README.md b/packages/html-macro-test/README.md new file mode 100644 index 000000000..f153294c8 --- /dev/null +++ b/packages/html-macro-test/README.md @@ -0,0 +1,3 @@ +# html-macro-test + +Unit tests for the `html!` macro diff --git a/packages/html-macro-test/src/lib.rs b/packages/html-macro-test/src/lib.rs new file mode 100644 index 000000000..45369ca9b --- /dev/null +++ b/packages/html-macro-test/src/lib.rs @@ -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; diff --git a/packages/html-macro-test/src/tests.rs b/packages/html-macro-test/src/tests.rs new file mode 100644 index 000000000..bc0747c59 --- /dev/null +++ b/packages/html-macro-test/src/tests.rs @@ -0,0 +1,3 @@ +mod all_tests; +mod text; +mod ui; diff --git a/packages/html-macro-test/src/tests/all_tests.rs b/packages/html-macro-test/src/tests/all_tests.rs new file mode 100644 index 000000000..21d2104ce --- /dev/null +++ b/packages/html-macro-test/src/tests/all_tests.rs @@ -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! {

}, + 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! {
}, + expected: expected.into(), + } + .test(); +} + +/// Events are ignored in non wasm-32 targets +#[test] +fn ignore_events_on_non_wasm32_targets() { + HtmlMacroTest { + generated: html! { +
+ }, + expected: html! {
}, + } + .test(); +} + +#[test] +fn child_node() { + let mut expected = VElement::new("div"); + expected.children = vec![VirtualNode::element("span")]; + + HtmlMacroTest { + generated: html! {
}, + 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! {
}, + 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! {
}, + 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! {
This is a text node
}, + expected: expected.into(), + } + .test(); +} + +#[test] +fn nested_macro() { + let child_2 = html! { }; + + let mut expected = VElement::new("div"); + expected.children = vec![VirtualNode::element("span"), VirtualNode::element("b")]; + + HtmlMacroTest { + generated: html! { +
+ { html! { } } + { child_2 } +
+ }, + 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! { }; + + 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! {
    }; + + let mut expected = VElement::new("div"); + expected.children = vec![ + VirtualNode::text(" A bit of text "), + VirtualNode::element("ul"), + ]; + + HtmlMacroTest { + generated: html! { +
    + A bit of text + { child } +
    + }, + 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! {
    }, html! { }]; + + let mut expected = VElement::new("div"); + expected.children = vec![VirtualNode::element("div"), VirtualNode::element("strong")]; + + HtmlMacroTest { + generated: html! {
    { children }
    }, + expected: expected.into(), + } + .test(); +} + +/// Just make sure that this compiles since async, for, loop, and type are keywords +#[test] +fn keyword_attribute() { + html! {