Feat: Clean up repo a bit

This commit is contained in:
Jonathan Kelley 2021-05-16 02:55:16 -04:00
parent c28697e1fe
commit a99147c85b
55 changed files with 76 additions and 6609 deletions

View file

@ -5,10 +5,10 @@ members = [
"packages/core",
"packages/web",
"packages/dioxus",
"packages/recoil",
"packages/docsite",
] # "packages/webview",
# "packages/docsite",
# "packages/ssr",
# "packages/cli",
# "packages/webview",

View file

@ -77,17 +77,16 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su
- [x] keys on components
- [x] Allow paths for components
- [x] todo mvc
- [ ] Make events lazy (use traits + Box<dyn>) - not sure what this means anymore
- [ ] Attributes on elements should implement format_args instead of string fmt
- [ ] Beef up the dioxus CLI tool to report build progress
- [x] Tweak macro parsing for better errors
- [x] dirty tagging, compression
- [x] code health
- [ ] Make events lazy (use traits + Box<dyn>) - not sure what this means anymore
- [ ] Beef up the dioxus CLI tool to report build progress
- [ ] Extract arena logic out for better safety guarantees
- [ ] Extract BumpFrame logic out for better safety guarantees
- [ ] make SSR follow HTML spec
- [x] dirty tagging, compression
- [ ] fix keys on elements
- [ ] MIRI tests
- [ ] code health
- [ ] all synthetic events filled out
- [ ] double check event targets and stuff
- [ ] Documentation overhaul
@ -95,6 +94,7 @@ Welcome to the first iteration of the Dioxus Virtual DOM! This release brings su
- [ ] controlled components
lower priority features
- [ ] Attributes on elements should implement format_args instead of string fmt
- [ ] fragments
- [ ] node refs (postpone for future release?)
- [ ] styling built-in (future release?)

View file

@ -1,9 +0,0 @@
[package]
name = "dioxus3d"
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

@ -1,25 +0,0 @@
# Dioxus3d: declarative framework for using the `Canvas`
Declarative wrapper over wgpu for creating interactive gpu-enabled visualizations in the browser with Dioxus. This crate's analog is ThreeJS and react-three-fiber. Here, we expose a set of hooks for using the `Canvas` imperatively, and then provide a set of components that interact with this canvas context. From there, declaring scenes is as easy as:
```rust
use dioxus3d::{Canvas};
use dioxus::prelude::*;
static HelloWorld = |ctx, props| {
ctx.render(rsx! {
Canvas {
Text {
"Hello world"
rel_pos: (0,0,1)
size: (1,1,1)
}
Cube {
size: (1,1,1)
}
}
})
};
```
// dioxus bevy: wrap a bevy instance with reactive controls.

View file

@ -14,7 +14,7 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
dioxus-core-macro = { path = "../core-macro", version = "0.1.1" }
# Backs scopes and graphs between parent and children
generational-arena = { version = "0.2.8", features = ["serde"] }
generational-arena = { version = "0.2.8" }
# Bumpalo backs the VNode creation
bumpalo = { version = "3.6.0", features = ["collections", "boxed"] }
@ -36,3 +36,4 @@ serde = { version = "1", features = ["derive"], optional = true }
[features]
default = []
serialize = ["serde", "generational-arena/serde"]

View file

@ -0,0 +1,48 @@
use std::{cell::UnsafeCell, collections::HashMap};
use generational_arena::Arena;
use crate::innerlude::*;
pub struct ScopeArena {
pub(crate) arena: UnsafeCell<Arena<Scope>>,
locks: HashMap<ScopeIdx, MutStatus>,
}
enum MutStatus {
Immut,
Mut,
}
impl ScopeArena {
pub fn new(arena: Arena<Scope>) -> Self {
Self {
arena: UnsafeCell::new(arena),
locks: Default::default(),
}
}
pub fn get(&self, idx: ScopeIdx) -> Result<&Scope> {
todo!()
}
pub fn get_mut(&self, idx: ScopeIdx) -> Result<&Scope> {
todo!()
}
fn inner(&self) -> &Arena<Scope> {
todo!()
}
fn inner_mut(&mut self) -> &mut Arena<Scope> {
todo!()
}
pub fn with<T>(&mut self, f: impl FnOnce(&mut Arena<Scope>) -> T) -> T {
todo!()
}
unsafe fn inner_unchecked<'s>() -> &'s mut Arena<Scope> {
todo!()
}
}

View file

@ -120,4 +120,4 @@ impl<'a> crate::virtual_dom::Context<'a> {
///
/// - Dioxus reducer is built on the safe API and provides a useful but slightly limited API.
/// - Dioxus Dataflow is built on the unsafe API and provides an even snazzier API than Dioxus Reducer.
fn blah() {}
fn _blah() {}

View file

@ -1,3 +1,7 @@
//! Internal error handling for Dioxus
//!
//!
use thiserror::Error as ThisError;
pub type Result<T, E = Error> = std::result::Result<T, E>;

View file

@ -65,6 +65,7 @@
//! - dioxus-liveview (SSR + StringRenderer)
//!
pub mod arena;
pub mod component; // Logic for extending FC
pub mod context; // Logic for providing hook + context functionality to user components
pub mod debug_renderer;

View file

@ -19,16 +19,11 @@
//!
//! # Known Issues
//! ----
//! - stack machine approach does not work when 3rd party extensions inject elements (breaking our view of the dom)
use std::cell::Ref;
//! - stack machine approach does not work when 3rd party extensions inject elements (breaking our view of the dom) - solvable by the renderer
use crate::innerlude::ScopeIdx;
pub type EditList<'src> = Vec<Edit<'src>>;
// pub struct EditList<'src> {
// edits: Vec<Edit<'src>>,
// }
/// The `Edit` represents a single modifcation of the renderer tree.
/// todo @jon, go through and make certain fields static. tag names should be known at compile time

View file

@ -2,7 +2,7 @@
Generate the liveview site powering diouxslabs.com.
We use the dioxus SSR crate and the use_router hook to generate a
We use the dioxus SSR crate and the use_router hook to generate all the routes

View file

@ -1,105 +1,11 @@
use dioxus_ssr::{
prelude::{builder::IntoDomTree, dioxus::events::on::MouseEvent, *},
prelude::*,
prelude::{builder::IntoDomTree, dioxus::events::on::MouseEvent},
TextRenderer,
};
mod components {
mod app;
}
fn main() {
TextRenderer::new(App);
}
#[derive(Debug, PartialEq)]
enum Route {
Homepage,
Examples,
}
#[derive(Debug, PartialEq, Props)]
struct AppProps {
route: Route,
}
static App: FC<AppProps> = |ctx, props| {
let body = match props.route {
Route::Homepage => ctx.render(rsx! {
div {
"Some Content"
}
}),
Route::Examples => ctx.render(rsx! {
div {
"Other Content"
}
}),
};
ctx.render(rsx!(
div {
Header {}
{body}
ul {
{(0..10).map(|f| rsx!{
li {
"this is list item {f}"
}
})}
}
}
))
};
static Header: FC<()> = |ctx, _| {
ctx.render(rsx! {
div {
}
})
};
mod example {
use super::*;
static ExampleUsage: FC<()> = |ctx, props| {
// direct rsx!
let body = rsx! {
div {}
};
// rendered rsx!
let top = ctx.render(rsx! {
div {
"ack!"
}
});
// listy rsx
let list2 = (0..10).map(|f| {
rsx! {
div {}
}
});
// rendered list rsx
let list = (0..10).map(|f| {
ctx.render(rsx! {
div {}
})
});
ctx.render(rsx!(
div {
Header {}
{body}
{top}
{list}
{list2}
// inline rsx
{rsx!{
div { "hello" }
}}
}
))
};
}
fn App(ctx: Context, props: &()) -> DomTree {}

View file

@ -1,7 +0,0 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View file

@ -1,9 +0,0 @@
[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]

View file

@ -1,72 +0,0 @@
pub struct Store<T> {
user_data: T,
}
/// Select just a small bit of the
fn use_selector() {}
/*
// borrow a closure so we can copy the reference
let dispatch = use_dispatch::<UserData>(ctx);
dispatch(|| UserData::Logout)
dispatch()
*/
fn use_dispatch() {}
mod tests {
struct UserData {}
// static SelectLoggedIn: FC<T> = |_| {};
/*
// Merge the various stores into a single context
// This auto generates the appropriate selectors, reducing the need to wrap the app in excess providers
let all = combine_stores(vec![store1, store2]);
<Redux store={all}>
</Redux>
*/
}
struct Context {
data: String,
logged_in: bool,
}
// "static" selectors automatically get memoized
static SelectUserName: Selector<&str> = |root: Context| root.data.as_str();
static SelectUserName: Selector<bool> = |root: Context| root.data.logged_in;
fn main() {
/*
use_context is very unsafe! It essentially exposes your data in an unsafecell where &mut T and &T can exist at the same time. It's up to *you* the library implmenetor to make this safe.
We provide a redux-style system and a recoil-style system that are both saf
*/
// connect itsy bits of state together
let context = use_create_context(
ctx,
ContextBuilder::new()
.with_root(|| Context {})
.with_root(|| Context {})
.with_root(|| Context {})
.with_root(|| Context {})
.with_root(|| Context {})
.with_root(|| Context {})
.with_root(|| Context {})
.with_root(|| Context {})
.build(),
);
let g: HashMap<TypeId, Box<dyn Any>> = HashMap::new();
}

View file

@ -1,58 +0,0 @@
# 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)

View file

@ -1,47 +0,0 @@
[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",
]

View file

@ -1,3 +0,0 @@
# virtual-dom-rs
> A standalone Virtual DOM creation, diffing and patching implementation

View file

@ -1,26 +0,0 @@
//! 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);
}
}

View file

@ -1,351 +0,0 @@
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",
// })
// }
}

View file

@ -1,103 +0,0 @@
//! 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()
}
}

View file

@ -1,44 +0,0 @@
//! 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;
}

View file

@ -1,217 +0,0 @@
use crate::dom_updater::ActiveClosures;
use crate::patch::Patch;
use std::cmp::min;
use std::collections::HashMap;
use std::collections::HashSet;
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(())
}

View file

@ -1,75 +0,0 @@
//! 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,
}
}
}

View file

@ -1,116 +0,0 @@
//! 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"
>
}
}

View file

@ -1,118 +0,0 @@
//! 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

View file

@ -1,197 +0,0 @@
//! 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();
}

View file

@ -1,53 +0,0 @@
//! 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);
}
}

View file

@ -1,147 +0,0 @@
//! 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");
}

View file

@ -1,47 +0,0 @@
// #![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");
}

View file

@ -11,9 +11,10 @@ impl Display for SsrRenderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let node = self
.dom
.components
.get(self.dom.base_scope)
.unwrap()
.base_scope()
// .components
// .get(self.dom.base_scope)
// .unwrap()
.frames
.current_head_node();

View file

@ -1,4 +0,0 @@
out
node_modules
client/server
.vscode-test

View file

@ -1,44 +0,0 @@
// A launch configuration that compiles the extension and then opens it inside a new window
{
"version": "0.2.0",
"configurations": [
{
"type": "extensionHost",
"request": "launch",
"name": "Launch Client",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
"outFiles": ["${workspaceRoot}/client/out/**/*.js"],
"preLaunchTask": {
"type": "npm",
"script": "watch"
}
},
{
"type": "node",
"request": "attach",
"name": "Attach to Server",
"port": 6009,
"restart": true,
"outFiles": ["${workspaceRoot}/server/out/**/*.js"]
},
{
"name": "Language Server E2E Test",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}",
"--extensionTestsPath=${workspaceRoot}/client/out/test",
"${workspaceRoot}/client/testFixture"
],
"outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
}
],
"compounds": [
{
"name": "Client + Server",
"configurations": ["Launch Client", "Attach to Server"]
}
]
}

View file

@ -1,33 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"group": "build",
"presentation": {
"panel": "dedicated",
"reveal": "never"
},
"problemMatcher": [
"$tsc"
]
},
{
"type": "npm",
"script": "watch",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"panel": "dedicated",
"reveal": "never"
},
"problemMatcher": [
"$tsc-watch"
]
}
]
}

View file

@ -1,14 +0,0 @@
.vscode/**
**/*.ts
**/*.map
.gitignore
**/tsconfig.json
**/tsconfig.base.json
contributing.md
.travis.yml
client/node_modules/**
!client/node_modules/vscode-jsonrpc/**
!client/node_modules/vscode-languageclient/**
!client/node_modules/vscode-languageserver-protocol/**
!client/node_modules/vscode-languageserver-types/**
!client/node_modules/semver/**

View file

@ -1,14 +0,0 @@
[package]
name = "syntax-gen"
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]
[[bin]]
path = "./rust/main.rs"
name = "main"

View file

@ -1,17 +0,0 @@
# VSCode support for Dioxus html macro
This macro provides syntax highlighting for the html! macro used in Dioxus projects. Users should feel at home writing html and css alongside the custom attributes used by Dioxus.
## How it works
This extension works by:
- Creating a custom HTML ruleset for dioxus html! nodes
- Request forwarding content
## Resources
Request forwarding is performed intelligently by the extension.
Requests within the html! tag are forwarded to the html language service. It's simple and doesn't
https://code.visualstudio.com/api/language-extensions/embedded-languages#language-services

View file

@ -1,81 +0,0 @@
{
"name": "lsp-sample-client",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/vscode": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.43.0.tgz",
"integrity": "sha512-kIaR9qzd80rJOxePKpCB/mdy00mz8Apt2QA5Y6rdrKFn13QNFNeP3Hzmsf37Bwh/3cS7QjtAeGSK7wSqAU0sYQ==",
"dev": true
},
"vscode-html-languageservice": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-3.0.3.tgz",
"integrity": "sha512-U+upM3gHp3HaF3wXAnUduA6IDKcz6frWS/dTAju3cZVIyZwOLBBFElQVlLH0ycHyMzqUFrjvdv+kEyPAEWfQ/g==",
"requires": {
"vscode-languageserver-types": "^3.15.0-next.2",
"vscode-nls": "^4.1.1",
"vscode-uri": "^2.0.3"
},
"dependencies": {
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
}
}
},
"vscode-jsonrpc": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
},
"vscode-languageclient": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-6.1.3.tgz",
"integrity": "sha512-YciJxk08iU5LmWu7j5dUt9/1OLjokKET6rME3cI4BRpiF6HZlusm2ZwPt0MYJ0lV5y43sZsQHhyon2xBg4ZJVA==",
"requires": {
"semver": "^6.3.0",
"vscode-languageserver-protocol": "^3.15.3"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"vscode-languageserver-protocol": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
"requires": {
"vscode-jsonrpc": "^5.0.1",
"vscode-languageserver-types": "3.15.1"
}
},
"vscode-languageserver-textdocument": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
"integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
},
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
},
"vscode-nls": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.2.tgz",
"integrity": "sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw=="
},
"vscode-uri": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.1.tgz",
"integrity": "sha512-eY9jmGoEnVf8VE8xr5znSah7Qt1P/xsCdErz+g8HYZtJ7bZqKH5E3d+6oVNm1AC/c6IHUDokbmVXKOi4qPAC9A=="
}
}
}

View file

@ -1,23 +0,0 @@
{
"name": "lsp-sample-client",
"description": "VSCode part of a language server",
"author": "Microsoft Corporation",
"license": "MIT",
"version": "0.0.1",
"publisher": "vscode",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/vscode-extension-samples"
},
"engines": {
"vscode": "^1.43.0"
},
"dependencies": {
"vscode-html-languageservice": "^3.0.3",
"vscode-languageclient": "^6.1.3",
"vscode-languageserver-textdocument": "^1.0.1"
},
"devDependencies": {
"@types/vscode": "^1.43.0"
}
}

View file

@ -1,449 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from "vscode-languageserver-textdocument";
import { Position, Range } from "vscode-languageclient";
import { LanguageService, TokenType } from "vscode-html-languageservice";
export interface LanguageRange extends Range {
languageId: string | undefined;
attributeValue?: boolean;
}
export interface HTMLDocumentRegions {
getEmbeddedDocument(
languageId: string,
ignoreAttributeValues?: boolean
): TextDocument;
getLanguageRanges(range: Range): LanguageRange[];
getLanguageAtPosition(position: Position): string | undefined;
getLanguagesInDocument(): string[];
getImportedScripts(): string[];
}
export const CSS_STYLE_RULE = "__";
interface EmbeddedRegion {
languageId: string | undefined;
start: number;
end: number;
attributeValue?: boolean;
}
// Check if the request is coming from inside a special region
export function isInsideStyleRegion(
languageService: LanguageService,
documentText: string,
offset: number
) {
let scanner = languageService.createScanner(documentText);
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.Styles:
if (
offset >= scanner.getTokenOffset() &&
offset <= scanner.getTokenEnd()
) {
return true;
}
}
token = scanner.scan();
}
return false;
}
export function getCSSVirtualContent(
languageService: LanguageService,
documentText: string
): string {
let regions: EmbeddedRegion[] = [];
let scanner = languageService.createScanner(documentText);
let lastTagName: string = "";
let lastAttributeName: string | null = null;
let languageIdFromType: string | undefined = undefined;
let importedScripts: string[] = [];
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
lastTagName = scanner.getTokenText();
lastAttributeName = null;
languageIdFromType = "javascript";
break;
case TokenType.Styles:
regions.push({
languageId: "css",
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd(),
});
break;
case TokenType.Script:
regions.push({
languageId: languageIdFromType,
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd(),
});
break;
case TokenType.AttributeName:
lastAttributeName = scanner.getTokenText();
break;
case TokenType.AttributeValue:
if (
lastAttributeName === "src" &&
lastTagName.toLowerCase() === "script"
) {
let value = scanner.getTokenText();
if (value[0] === "'" || value[0] === '"') {
value = value.substr(1, value.length - 1);
}
importedScripts.push(value);
} else if (
lastAttributeName === "type" &&
lastTagName.toLowerCase() === "script"
) {
if (
/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
scanner.getTokenText()
)
) {
languageIdFromType = "javascript";
} else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
languageIdFromType = "typescript";
} else {
languageIdFromType = undefined;
}
} else {
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
if (attributeLanguageId) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
let firstChar = documentText[start];
if (firstChar === "'" || firstChar === '"') {
start++;
end--;
}
regions.push({
languageId: attributeLanguageId,
start,
end,
attributeValue: true,
});
}
}
lastAttributeName = null;
break;
}
token = scanner.scan();
}
let content = documentText
.split("\n")
.map((line) => {
return " ".repeat(line.length);
})
.join("\n");
regions.forEach((r) => {
if (r.languageId === "css") {
content =
content.slice(0, r.start) +
documentText.slice(r.start, r.end) +
content.slice(r.end);
}
});
return content;
}
export function getDocumentRegions(
languageService: LanguageService,
document: TextDocument
): HTMLDocumentRegions {
let regions: EmbeddedRegion[] = [];
let scanner = languageService.createScanner(document.getText());
let lastTagName: string = "";
let lastAttributeName: string | null = null;
let languageIdFromType: string | undefined = undefined;
let importedScripts: string[] = [];
let token = scanner.scan();
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.StartTag:
lastTagName = scanner.getTokenText();
lastAttributeName = null;
languageIdFromType = "javascript";
break;
case TokenType.Styles:
regions.push({
languageId: "css",
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd(),
});
break;
case TokenType.Script:
regions.push({
languageId: languageIdFromType,
start: scanner.getTokenOffset(),
end: scanner.getTokenEnd(),
});
break;
case TokenType.AttributeName:
lastAttributeName = scanner.getTokenText();
break;
case TokenType.AttributeValue:
if (
lastAttributeName === "src" &&
lastTagName.toLowerCase() === "script"
) {
let value = scanner.getTokenText();
if (value[0] === "'" || value[0] === '"') {
value = value.substr(1, value.length - 1);
}
importedScripts.push(value);
} else if (
lastAttributeName === "type" &&
lastTagName.toLowerCase() === "script"
) {
if (
/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(
scanner.getTokenText()
)
) {
languageIdFromType = "javascript";
} else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
languageIdFromType = "typescript";
} else {
languageIdFromType = undefined;
}
} else {
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
if (attributeLanguageId) {
let start = scanner.getTokenOffset();
let end = scanner.getTokenEnd();
let firstChar = document.getText()[start];
if (firstChar === "'" || firstChar === '"') {
start++;
end--;
}
regions.push({
languageId: attributeLanguageId,
start,
end,
attributeValue: true,
});
}
}
lastAttributeName = null;
break;
}
token = scanner.scan();
}
return {
getLanguageRanges: (range: Range) =>
getLanguageRanges(document, regions, range),
getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) =>
getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues),
getLanguageAtPosition: (position: Position) =>
getLanguageAtPosition(document, regions, position),
getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
getImportedScripts: () => importedScripts,
};
}
function getLanguageRanges(
document: TextDocument,
regions: EmbeddedRegion[],
range: Range
): LanguageRange[] {
let result: LanguageRange[] = [];
let currentPos = range ? range.start : Position.create(0, 0);
let currentOffset = range ? document.offsetAt(range.start) : 0;
let endOffset = range
? document.offsetAt(range.end)
: document.getText().length;
for (let region of regions) {
if (region.end > currentOffset && region.start < endOffset) {
let start = Math.max(region.start, currentOffset);
let startPos = document.positionAt(start);
if (currentOffset < region.start) {
result.push({
start: currentPos,
end: startPos,
languageId: "html",
});
}
let end = Math.min(region.end, endOffset);
let endPos = document.positionAt(end);
if (end > region.start) {
result.push({
start: startPos,
end: endPos,
languageId: region.languageId,
attributeValue: region.attributeValue,
});
}
currentOffset = end;
currentPos = endPos;
}
}
if (currentOffset < endOffset) {
let endPos = range ? range.end : document.positionAt(endOffset);
result.push({
start: currentPos,
end: endPos,
languageId: "html",
});
}
return result;
}
function getLanguagesInDocument(
_document: TextDocument,
regions: EmbeddedRegion[]
): string[] {
let result = [];
for (let region of regions) {
if (region.languageId && result.indexOf(region.languageId) === -1) {
result.push(region.languageId);
if (result.length === 3) {
return result;
}
}
}
result.push("html");
return result;
}
function getLanguageAtPosition(
document: TextDocument,
regions: EmbeddedRegion[],
position: Position
): string | undefined {
let offset = document.offsetAt(position);
for (let region of regions) {
if (region.start <= offset) {
if (offset <= region.end) {
return region.languageId;
}
} else {
break;
}
}
return "html";
}
function getEmbeddedDocument(
document: TextDocument,
contents: EmbeddedRegion[],
languageId: string,
ignoreAttributeValues: boolean
): TextDocument {
let currentPos = 0;
let oldContent = document.getText();
let result = "";
let lastSuffix = "";
for (let c of contents) {
if (
c.languageId === languageId &&
(!ignoreAttributeValues || !c.attributeValue)
) {
result = substituteWithWhitespace(
result,
currentPos,
c.start,
oldContent,
lastSuffix,
getPrefix(c)
);
result += oldContent.substring(c.start, c.end);
currentPos = c.end;
lastSuffix = getSuffix(c);
}
}
result = substituteWithWhitespace(
result,
currentPos,
oldContent.length,
oldContent,
lastSuffix,
""
);
return TextDocument.create(
document.uri,
languageId,
document.version,
result
);
}
function getPrefix(c: EmbeddedRegion) {
if (c.attributeValue) {
switch (c.languageId) {
case "css":
return CSS_STYLE_RULE + "{";
}
}
return "";
}
function getSuffix(c: EmbeddedRegion) {
if (c.attributeValue) {
switch (c.languageId) {
case "css":
return "}";
case "javascript":
return ";";
}
}
return "";
}
function substituteWithWhitespace(
result: string,
start: number,
end: number,
oldContent: string,
before: string,
after: string
) {
let accumulatedWS = 0;
result += before;
for (let i = start + before.length; i < end; i++) {
let ch = oldContent[i];
if (ch === "\n" || ch === "\r") {
// only write new lines, skip the whitespace
accumulatedWS = 0;
result += ch;
} else {
accumulatedWS++;
}
}
result = append(result, " ", accumulatedWS - after.length);
result += after;
return result;
}
function append(result: string, str: string, n: number): string {
while (n > 0) {
if (n & 1) {
result += str;
}
n >>= 1;
str += str;
}
return result;
}
function getAttributeLanguage(attributeName: string): string | null {
let match = attributeName.match(/^(style)$|^(on\w+)$/i);
if (!match) {
return null;
}
return match[1] ? "css" : "javascript";
}

View file

@ -1,147 +0,0 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as path from "path";
import {
commands,
CompletionList,
ExtensionContext,
Uri,
workspace,
} from "vscode";
import { getLanguageService } from "vscode-html-languageservice";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind,
} from "vscode-languageclient";
import { isInsideHtmlMacro } from "./rustSupport";
// import { getCSSVirtualContent, isInsideStyleRegion } from "./embeddedSupport";
let client: LanguageClient;
const htmlLanguageService = getLanguageService();
export function activate(context: ExtensionContext) {
// The server is implemented in node
let serverModule = context.asAbsolutePath(
path.join("server", "out", "server.js")
);
// The debug options for the server
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
let debugOptions = { execArgv: ["--nolazy", "--inspect=6009"] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
let serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions,
},
};
const virtualDocumentContents = new Map<string, string>();
workspace.registerTextDocumentContentProvider("embedded-content", {
provideTextDocumentContent: (uri) => {
const originalUri = uri.path.slice(1).slice(0, -4);
console.error(originalUri);
const decodedUri = decodeURIComponent(originalUri);
return virtualDocumentContents.get(decodedUri);
},
});
let clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "rust" }],
middleware: {
provideCompletionItem: async (
document,
position,
context,
token,
next
) => {
/*
1: Find the occurences of the html! macro using regex
2: Check if any of the occurences match the cursor offset
3: If so, direct the captured block to the html to the rsx language service
*/
const docSrc = document.getText();
const offset = document.offsetAt(position);
const matches = docSrc.matchAll(macroRegex);
// Lazily loop through matches, abort early if the cursor is after the match
// let start = 0;
// let end = 0;
let matchBody: string | undefined = undefined;
for (const match of matches) {
// // Check if the cursor is inbetween the previous end and the new start
// // This means the cursor is between html! invocations and we should bail early
// if (offset > end && offset < match.index) {
// // Ensure the match
// // defer to the "next?" symbol
// return await next(document, position, context, token);
// }
// Otherwise, move the counters forward
const start = match.index;
const end = start + match.length;
// Ensure the cursor is within the match
// Break if so
if (offset >= start && offset <= end) {
matchBody = match[1];
break;
}
}
// If we looped through all the matches and the match wasn't defined, then bail
if (matchBody === undefined) {
return await next(document, position, context, token);
}
// If we're inside the style region, then provide CSS completions with the CSS provider
const originalUri = document.uri.toString();
virtualDocumentContents.set(originalUri, matchBody);
// getCSSVirtualContent(htmlLanguageService, document.getText())
const vdocUriString = `embedded-content://html/${encodeURIComponent(
originalUri
)}.html`;
const vdocUri = Uri.parse(vdocUriString);
return await commands.executeCommand<CompletionList>(
"vscode.executeCompletionItemProvider",
vdocUri,
position,
context.triggerCharacter
);
},
},
};
// Create the language client and start the client.
client = new LanguageClient(
"languageServerExample",
"Language Server Example",
serverOptions,
clientOptions
);
// Start the client. This will also launch the server
client.start();
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}
const macroRegex = /html! {([\s\S]*?)}/g;

View file

@ -1,8 +0,0 @@
const macroRegex = /html! {([\s\S]*?)}/g;
export function isInsideHtmlMacro(
match: RegExpMatchArray,
cursor: number
): boolean {
return false;
}

View file

@ -1,12 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ESNext",
"lib": ["ESNext"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
}

File diff suppressed because it is too large Load diff

View file

@ -1,37 +0,0 @@
{
"name": "dioxus-rsx-plugin",
"description": "Get HTML completion inside the html! macro for dioxus projects",
"author": "Jonathan Kelley",
"license": "MIT",
"version": "0.1.0",
"repository": {
"type": "git",
"url": "https://github.com/jkelleyrtp/dioxus"
},
"publisher": "jkelleyrtp",
"categories": [],
"keywords": [],
"engines": {
"vscode": "^1.43.0",
"node": "*"
},
"activationEvents": [
"onLanguage:rust"
],
"main": "./client/out/extension",
"scripts": {
"vscode:prepublish": "cd client && npm install && cd .. && npm run compile",
"compile": "tsc -b",
"watch": "tsc -b -w",
"// postinstall": "cd client && npm install && cd ../server && npm install && cd ..",
"test": "sh ./scripts/e2e.sh"
},
"devDependencies": {
"@types/mocha": "^5.2.7",
"@types/node": "^12.12.0",
"@typescript-eslint/eslint-plugin": "^3.0.2",
"@typescript-eslint/parser": "^3.0.2",
"eslint": "^7.1.0",
"typescript": "^4.0.2"
}
}

View file

@ -1,3 +0,0 @@
//! Generate the syntax for the RSX language
fn main() {}

View file

@ -1,67 +0,0 @@
{
"name": "lsp-sample-server",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"vscode-html-languageservice": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-3.0.3.tgz",
"integrity": "sha512-U+upM3gHp3HaF3wXAnUduA6IDKcz6frWS/dTAju3cZVIyZwOLBBFElQVlLH0ycHyMzqUFrjvdv+kEyPAEWfQ/g==",
"requires": {
"vscode-languageserver-types": "^3.15.0-next.2",
"vscode-nls": "^4.1.1",
"vscode-uri": "^2.0.3"
},
"dependencies": {
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
},
"vscode-uri": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.1.tgz",
"integrity": "sha512-eY9jmGoEnVf8VE8xr5znSah7Qt1P/xsCdErz+g8HYZtJ7bZqKH5E3d+6oVNm1AC/c6IHUDokbmVXKOi4qPAC9A=="
}
}
},
"vscode-jsonrpc": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
},
"vscode-languageserver": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz",
"integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==",
"requires": {
"vscode-languageserver-protocol": "^3.15.3"
}
},
"vscode-languageserver-protocol": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
"requires": {
"vscode-jsonrpc": "^5.0.1",
"vscode-languageserver-types": "3.15.1"
}
},
"vscode-languageserver-textdocument": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
"integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
},
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
},
"vscode-nls": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-4.1.2.tgz",
"integrity": "sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw=="
}
}
}

View file

@ -1,20 +0,0 @@
{
"name": "lsp-sample-server",
"description": "Example implementation of a language server in node.",
"version": "1.0.0",
"author": "Microsoft Corporation",
"license": "MIT",
"engines": {
"node": "*"
},
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/vscode-extension-samples"
},
"dependencies": {
"vscode-html-languageservice": "^3.0.3",
"vscode-languageserver": "^6.1.1",
"vscode-languageserver-textdocument": "^1.0.1"
},
"scripts": {}
}

View file

@ -1,54 +0,0 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { getLanguageService } from "vscode-html-languageservice";
import {
createConnection,
InitializeParams,
ProposedFeatures,
TextDocuments,
TextDocumentSyncKind,
} from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
// Create a connection for the server. The connection uses Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);
// Create a simple text document manager. The text document manager
// supports full document sync only
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
const htmlLanguageService = getLanguageService();
connection.onInitialize((_params: InitializeParams) => {
return {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Full,
// Tell the client that the server supports code completion
completionProvider: {
resolveProvider: false,
},
},
};
});
connection.onInitialized(() => {});
connection.onCompletion(async (textDocumentPosition, token) => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (!document) {
return null;
}
return htmlLanguageService.doComplete(
document,
textDocumentPosition.position,
htmlLanguageService.parseHTMLDocument(document)
);
});
documents.listen(connection);
connection.listen();

View file

@ -1,14 +0,0 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext"],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"outDir": "out",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
}

File diff suppressed because one or more lines are too long

View file

@ -1,27 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ESNext",
"lib": [
"ESNext"
],
"outDir": "out",
"rootDir": "src",
"sourceMap": true
},
"include": [
"src"
],
"exclude": [
"node_modules",
".vscode-test"
],
"references": [
{
"path": "./client"
},
{
"path": "./server"
}
]
}

View file

@ -11,10 +11,8 @@ license = "MIT/Apache-2.0"
dioxus-core = { path = "../core", version = "0.1.2" }
js-sys = "0.3"
wasm-bindgen = "0.2.71"
# wasm-bindgen = "0.2.70"
lazy_static = "1.4.0"
wasm-bindgen-futures = "0.4.20"
# futures = "0.3.12"
wasm-logger = "0.2.0"
log = "0.4.14"
fxhash = "0.2.1"
@ -23,6 +21,9 @@ console_error_panic_hook = "0.1.6"
generational-arena = "0.2.8"
wasm-bindgen-test = "0.3.21"
once_cell = "1.7.2"
# wasm-bindgen = "0.2.70"
# futures = "0.3.12"
# html-validation = { path = "../html-validation", version = "0.1.1" }
async-channel = "1.6.1"
@ -57,11 +58,7 @@ features = [
"DocumentType",
"CharacterData",
"HtmlOptionElement",
] # "FormEvent",
# "UIEvent",
# "ToggleEvent",
# "MediaEvent",
# "SelectionEvent",
]
[profile.release]
debug = true
@ -69,7 +66,6 @@ debug = true
[lib]
crate-type = ["cdylib", "rlib"]
[dev-dependencies]
uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] }