on master help sign in
75 files changed+6247−2436
since splitsnapshots
unifiedsplit
.gitignore+1 .travis.yml+19−9 Cargo.lock+647−322 Cargo.toml+51−14 README.md+13−6 benches/benches.rs+118 build.sh+2−2 crates/js-api/Cargo.toml+6−7 crates/js-api/src/lib.rs+30−38 examples/counter/Cargo.toml+5−5 examples/counter/README.md+1−1 examples/counter/index.html+3−3 examples/counter/src/lib.rs+10−13 examples/game-of-life/Cargo.toml+5−6 examples/game-of-life/README.md+1−1 examples/game-of-life/index.html+3−6 examples/game-of-life/src/lib.rs+25−23 examples/hello-world/Cargo.toml+3−3 examples/hello-world/README.md+1−1 examples/hello-world/index.html+3−3 examples/hello-world/src/lib.rs+12−14 examples/input-form/Cargo.toml+3−3 examples/input-form/README.md+1−1 examples/input-form/index.html+3−3 examples/input-form/src/lib.rs+10−11 examples/js-component/Cargo.toml+3−3 examples/js-component/README.md+1−1 examples/js-component/index.html+3−3 examples/js-component/src/lib.rs+6−10 examples/moire/Cargo.toml+8−9 examples/moire/README.md+1−1 examples/moire/index.html+3−3 examples/moire/src/lib.rs+38−64 examples/sierpinski-triangle/Cargo.toml+28 examples/sierpinski-triangle/README.md+24 examples/sierpinski-triangle/ferris.png examples/sierpinski-triangle/index.html+69 examples/sierpinski-triangle/src/lib.rs+157 examples/todomvc/Cargo.toml+11−12 examples/todomvc/README.md+1−1 examples/todomvc/index.html+3−3 examples/todomvc/src/router.rs+23−21 examples/todomvc/src/todo.rs+86−32 examples/todomvc/src/todos.rs+40−56 js/change-list-interpreter.js+377 js/change-list.js−268 js/package-lock.json−5 js/package.json−11 js/strace.js+80 publish.sh+11 src/builder.rs+161−6 src/cached.rs+139−73 src/cached_set.rs+186 src/change_list.rs−426 src/change_list/emitter.rs+292 src/change_list/js.rs+40 src/change_list/mod.rs+353 src/change_list/strings.rs+66 src/change_list/traversal.rs+344 src/diff.rs+1125 src/events.rs+123−83 src/lib.rs+64−11 src/node.rs+150−77 src/render.rs+65−50 src/render_context.rs+104 src/strace.rs+22 src/vdom.rs+182−338 tests/web/cached.js+11 tests/web/cached.rs+239−51 tests/web/events.rs+58−156 tests/web/js_api.rs+37−47 tests/web/keyed.js+25 tests/web/keyed.rs+269 tests/web/main.rs+60−37 tests/web/render.rs+183−83
.gitignore+1
@@ -1,3 +1,4 @@
11/target
22**/*.rs.bk
33wasm-pack.log
4+/.vscode
.travis.yml+19−9
@@ -25,20 +25,30 @@ matrix:
2525 - name: "build (beta)"
2626 rust: beta
2727 script: INSTALL=1 ./build.sh
28− - name: "build (nightly)"
29− rust: nightly
30− script: INSTALL=1 ./build.sh
3128 - name: "headless browser tests (firefox)"
3229 rust: stable
3330 addons:
3431 firefox: latest
35− script: wasm-pack test --firefox --headless
36− - name: "headless browser tests (chrome)"
37− rust: stable
38− addons:
39− chrome: stable
40− script: wasm-pack test --chrome --headless
32+ script: wasm-pack test --firefox --headless -- --features xxx-unstable-internal-use-only
4133 - name: "native tests"
4234 rust: stable
4335 install: echo "no install"
4436 script: cargo test --all
37+
38+ # TODO: A bug in `rustc` is making `js-sys` broken on nightly:
39+ # https://github.com/rust-lang/rust/issues/69315
40+ # - name: "build (nightly)"
41+ # rust: nightly
42+ # script: INSTALL=1 ./build.sh
43+ # - name: "check native benches"
44+ # rust: nightly
45+ # install: echo "no install"
46+ # script: cargo check --benches --features xxx-unstable-internal-use-only
47+
48+ # TODO: `chromedriver` is complaining about ports or something. Temporarily
49+ # disable in CI until we figure it out.
50+ # - name: "headless browser tests (chrome)"
51+ # rust: stable
52+ # addons:
53+ # chrome: stable
54+ # script: wasm-pack test --chrome --headless -- --features xxx-unstable-internal-use-only
Cargo.lock+647−322
@@ -1,104 +1,204 @@
11# This file is automatically @generated by Cargo.
22# It is not intended for manual editing.
33[[package]]
4−name = "aho-corasick"
5−version = "0.6.10"
4+name = "anyhow"
5+version = "1.0.25"
66source = "registry+https://github.com/rust-lang/crates.io-index"
7−dependencies = [
8− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
9−]
107
118[[package]]
129name = "atty"
13−version = "0.2.11"
10+version = "0.2.13"
1411source = "registry+https://github.com/rust-lang/crates.io-index"
1512dependencies = [
16− "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
17− "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
18− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
13+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
14+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
1915]
2016
2117[[package]]
2218name = "autocfg"
23−version = "0.1.2"
19+version = "0.1.7"
2420source = "registry+https://github.com/rust-lang/crates.io-index"
2521
2622[[package]]
27−name = "backtrace"
28−version = "0.3.14"
23+name = "bitflags"
24+version = "1.2.1"
2925source = "registry+https://github.com/rust-lang/crates.io-index"
30−dependencies = [
31− "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
32− "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
33− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
34− "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
35− "rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
36− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
37−]
3826
3927[[package]]
40−name = "backtrace-sys"
41−version = "0.1.28"
28+name = "bstr"
29+version = "0.2.8"
4230source = "registry+https://github.com/rust-lang/crates.io-index"
4331dependencies = [
44− "cc 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)",
45− "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
32+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
33+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
34+ "regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
35+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
4636]
4737
4838[[package]]
4939name = "bumpalo"
50−version = "2.1.0"
40+version = "2.6.0"
5141source = "registry+https://github.com/rust-lang/crates.io-index"
5242
5343[[package]]
5444name = "byteorder"
55−version = "1.3.1"
45+version = "1.3.2"
5646source = "registry+https://github.com/rust-lang/crates.io-index"
5747
5848[[package]]
59−name = "cc"
60−version = "1.0.30"
49+name = "cast"
50+version = "0.2.3"
6151source = "registry+https://github.com/rust-lang/crates.io-index"
52+dependencies = [
53+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
54+]
6255
6356[[package]]
6457name = "cfg-if"
65−version = "0.1.7"
58+version = "0.1.10"
6659source = "registry+https://github.com/rust-lang/crates.io-index"
6760
61+[[package]]
62+name = "clap"
63+version = "2.33.0"
64+source = "registry+https://github.com/rust-lang/crates.io-index"
65+dependencies = [
66+ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
67+ "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
68+ "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
69+]
70+
6871[[package]]
6972name = "console_error_panic_hook"
7073version = "0.1.6"
7174source = "registry+https://github.com/rust-lang/crates.io-index"
7275dependencies = [
73− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
74− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
76+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
77+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
7578]
7679
7780[[package]]
7881name = "console_log"
79−version = "0.1.2"
82+version = "0.2.0"
83+source = "registry+https://github.com/rust-lang/crates.io-index"
84+dependencies = [
85+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
86+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
87+]
88+
89+[[package]]
90+name = "criterion"
91+version = "0.3.3"
92+source = "registry+https://github.com/rust-lang/crates.io-index"
93+dependencies = [
94+ "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
95+ "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
96+ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
97+ "criterion-plot 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
98+ "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
99+ "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
100+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
101+ "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
102+ "oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
103+ "plotters 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
104+ "rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
105+ "regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
106+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
107+ "serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
108+ "serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
109+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
110+ "tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
111+ "walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
112+]
113+
114+[[package]]
115+name = "criterion-plot"
116+version = "0.4.3"
117+source = "registry+https://github.com/rust-lang/crates.io-index"
118+dependencies = [
119+ "cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
120+ "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
121+]
122+
123+[[package]]
124+name = "crossbeam-deque"
125+version = "0.7.2"
126+source = "registry+https://github.com/rust-lang/crates.io-index"
127+dependencies = [
128+ "crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
129+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
130+]
131+
132+[[package]]
133+name = "crossbeam-epoch"
134+version = "0.8.0"
135+source = "registry+https://github.com/rust-lang/crates.io-index"
136+dependencies = [
137+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
138+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
139+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
140+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
141+ "memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
142+ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
143+]
144+
145+[[package]]
146+name = "crossbeam-queue"
147+version = "0.2.0"
148+source = "registry+https://github.com/rust-lang/crates.io-index"
149+dependencies = [
150+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
151+]
152+
153+[[package]]
154+name = "crossbeam-utils"
155+version = "0.7.0"
156+source = "registry+https://github.com/rust-lang/crates.io-index"
157+dependencies = [
158+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
159+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
160+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
161+]
162+
163+[[package]]
164+name = "csv"
165+version = "1.1.1"
166+source = "registry+https://github.com/rust-lang/crates.io-index"
167+dependencies = [
168+ "bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
169+ "csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
170+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
171+ "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
172+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
173+]
174+
175+[[package]]
176+name = "csv-core"
177+version = "0.1.6"
80178source = "registry+https://github.com/rust-lang/crates.io-index"
81179dependencies = [
82− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
83− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
180+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
84181]
85182
86183[[package]]
87184name = "dodrio"
88−version = "0.1.0"
185+version = "0.2.0"
89186dependencies = [
90− "bumpalo 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
91− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
92− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
93− "dodrio-js-api 0.1.0",
94− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
187+ "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
188+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
189+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
190+ "criterion 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
191+ "dodrio-js-api 0.2.0",
192+ "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
95193 "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
96− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
97− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
98− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
99− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
100− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
101− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
194+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
195+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
196+ "longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
197+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
198+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
199+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
200+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
201+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
102202]
103203
104204[[package]]
@@ -106,12 +206,12 @@ name = "dodrio-counter"
106206version = "0.1.0"
107207dependencies = [
108208 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
109− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
110− "dodrio 0.1.0",
111− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
112− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
113− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
114− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
209+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
210+ "dodrio 0.2.0",
211+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
212+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
213+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
214+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
115215]
116216
117217[[package]]
@@ -119,13 +219,12 @@ name = "dodrio-game-of-life"
119219version = "0.1.0"
120220dependencies = [
121221 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
122− "dodrio 0.1.0",
123− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
124− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
125− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
126− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
127− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
128− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
222+ "dodrio 0.2.0",
223+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
224+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
225+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
226+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
227+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
129228]
130229
131230[[package]]
@@ -133,10 +232,10 @@ name = "dodrio-hello-world"
133232version = "0.1.0"
134233dependencies = [
135234 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
136− "dodrio 0.1.0",
137− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
138− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
139− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
235+ "dodrio 0.2.0",
236+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
237+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
238+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
140239]
141240
142241[[package]]
@@ -144,22 +243,21 @@ name = "dodrio-input-form"
144243version = "0.1.0"
145244dependencies = [
146245 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
147− "dodrio 0.1.0",
148− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
149− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
150− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
246+ "dodrio 0.2.0",
247+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
248+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
249+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
151250]
152251
153252[[package]]
154253name = "dodrio-js-api"
155−version = "0.1.0"
254+version = "0.2.0"
156255dependencies = [
157− "dodrio 0.1.0",
158− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
159− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
160− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
161− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
162− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
256+ "dodrio 0.2.0",
257+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
258+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
259+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
260+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
163261]
164262
165263[[package]]
@@ -167,322 +265,515 @@ name = "dodrio-js-component"
167265version = "0.1.0"
168266dependencies = [
169267 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
170− "dodrio 0.1.0",
171− "dodrio-js-api 0.1.0",
172− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
173− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
174− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
268+ "dodrio 0.2.0",
269+ "dodrio-js-api 0.2.0",
270+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
271+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
272+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
175273]
176274
177275[[package]]
178276name = "dodrio-moire"
179277version = "0.1.0"
180278dependencies = [
181− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
279+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
182280 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
183− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
184− "dodrio 0.1.0",
185− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
186− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
187− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
188− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
189− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
190− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
191− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
281+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
282+ "dodrio 0.2.0",
283+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
284+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
285+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
286+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
287+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
288+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
289+]
290+
291+[[package]]
292+name = "dodrio-sierpinski"
293+version = "0.1.0"
294+dependencies = [
295+ "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
296+ "dodrio 0.2.0",
297+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
298+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
299+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
300+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
301+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
192302]
193303
194304[[package]]
195305name = "dodrio-todomvc"
196306version = "0.1.0"
197307dependencies = [
198− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
308+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
199309 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
200− "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
201− "dodrio 0.1.0",
202− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
203− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
204− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
205− "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
206− "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
207− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
208− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
209− "wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
210− "web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
310+ "console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
311+ "dodrio 0.2.0",
312+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
313+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
314+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
315+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
316+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
317+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
318+ "wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
319+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
211320]
212321
213322[[package]]
214−name = "env_logger"
215−version = "0.6.1"
323+name = "either"
324+version = "1.5.3"
325+source = "registry+https://github.com/rust-lang/crates.io-index"
326+
327+[[package]]
328+name = "futures"
329+version = "0.3.5"
216330source = "registry+https://github.com/rust-lang/crates.io-index"
217331dependencies = [
218− "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
219− "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
220− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
221− "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
222− "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
332+ "futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
333+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
334+ "futures-executor 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
335+ "futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
336+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
337+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
338+ "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
223339]
224340
225341[[package]]
226−name = "failure"
227−version = "0.1.5"
342+name = "futures-channel"
343+version = "0.3.5"
228344source = "registry+https://github.com/rust-lang/crates.io-index"
229345dependencies = [
230− "backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
231− "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
346+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
347+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
232348]
233349
234350[[package]]
235−name = "failure_derive"
236−version = "0.1.5"
351+name = "futures-core"
352+version = "0.3.5"
353+source = "registry+https://github.com/rust-lang/crates.io-index"
354+
355+[[package]]
356+name = "futures-executor"
357+version = "0.3.5"
237358source = "registry+https://github.com/rust-lang/crates.io-index"
238359dependencies = [
239− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
240− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
241− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
242− "synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
360+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
361+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
362+ "futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
243363]
244364
245365[[package]]
246−name = "futures"
247−version = "0.1.25"
366+name = "futures-io"
367+version = "0.3.5"
368+source = "registry+https://github.com/rust-lang/crates.io-index"
369+
370+[[package]]
371+name = "futures-macro"
372+version = "0.3.5"
373+source = "registry+https://github.com/rust-lang/crates.io-index"
374+dependencies = [
375+ "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
376+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
377+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
378+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
379+]
380+
381+[[package]]
382+name = "futures-sink"
383+version = "0.3.5"
248384source = "registry+https://github.com/rust-lang/crates.io-index"
249385
386+[[package]]
387+name = "futures-task"
388+version = "0.3.5"
389+source = "registry+https://github.com/rust-lang/crates.io-index"
390+dependencies = [
391+ "once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
392+]
393+
394+[[package]]
395+name = "futures-util"
396+version = "0.3.5"
397+source = "registry+https://github.com/rust-lang/crates.io-index"
398+dependencies = [
399+ "futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
400+ "futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
401+ "futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
402+ "futures-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
403+ "futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
404+ "futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
405+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
406+ "pin-project 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)",
407+ "pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
408+ "proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
409+ "proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
410+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
411+]
412+
250413[[package]]
251414name = "fxhash"
252415version = "0.2.1"
253416source = "registry+https://github.com/rust-lang/crates.io-index"
254417dependencies = [
255− "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
418+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
256419]
257420
421+[[package]]
422+name = "half"
423+version = "1.6.0"
424+source = "registry+https://github.com/rust-lang/crates.io-index"
425+
258426[[package]]
259427name = "heck"
260428version = "0.3.1"
261429source = "registry+https://github.com/rust-lang/crates.io-index"
262430dependencies = [
263− "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
431+ "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
432+]
433+
434+[[package]]
435+name = "hermit-abi"
436+version = "0.1.3"
437+source = "registry+https://github.com/rust-lang/crates.io-index"
438+dependencies = [
439+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
264440]
265441
266442[[package]]
267−name = "humantime"
268−version = "1.2.0"
443+name = "itertools"
444+version = "0.9.0"
269445source = "registry+https://github.com/rust-lang/crates.io-index"
270446dependencies = [
271− "quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
447+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
272448]
273449
274450[[package]]
275451name = "itoa"
276−version = "0.4.3"
452+version = "0.4.4"
277453source = "registry+https://github.com/rust-lang/crates.io-index"
278454
279455[[package]]
280456name = "js-sys"
281−version = "0.3.15"
457+version = "0.3.32"
282458source = "registry+https://github.com/rust-lang/crates.io-index"
283459dependencies = [
284− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
460+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
285461]
286462
287463[[package]]
288464name = "lazy_static"
289−version = "1.3.0"
465+version = "1.4.0"
290466source = "registry+https://github.com/rust-lang/crates.io-index"
291467
292468[[package]]
293469name = "libc"
294−version = "0.2.50"
470+version = "0.2.66"
295471source = "registry+https://github.com/rust-lang/crates.io-index"
296472
297473[[package]]
298474name = "log"
299−version = "0.4.6"
475+version = "0.4.11"
300476source = "registry+https://github.com/rust-lang/crates.io-index"
301477dependencies = [
302− "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
478+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
303479]
304480
481+[[package]]
482+name = "longest-increasing-subsequence"
483+version = "0.1.0"
484+source = "registry+https://github.com/rust-lang/crates.io-index"
485+
305486[[package]]
306487name = "memchr"
307−version = "2.2.0"
488+version = "2.2.1"
308489source = "registry+https://github.com/rust-lang/crates.io-index"
490+dependencies = [
491+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
492+]
493+
494+[[package]]
495+name = "memoffset"
496+version = "0.5.3"
497+source = "registry+https://github.com/rust-lang/crates.io-index"
498+dependencies = [
499+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
500+]
309501
310502[[package]]
311503name = "nom"
312−version = "4.2.2"
504+version = "4.2.3"
313505source = "registry+https://github.com/rust-lang/crates.io-index"
314506dependencies = [
315− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
507+ "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
316508 "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
317509]
318510
319511[[package]]
320−name = "proc-macro2"
321−version = "0.4.27"
512+name = "num-traits"
513+version = "0.2.10"
322514source = "registry+https://github.com/rust-lang/crates.io-index"
323515dependencies = [
324− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
516+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
325517]
326518
327519[[package]]
328−name = "quick-error"
329−version = "1.2.2"
520+name = "num_cpus"
521+version = "1.11.1"
330522source = "registry+https://github.com/rust-lang/crates.io-index"
523+dependencies = [
524+ "hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
525+ "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
526+]
527+
528+[[package]]
529+name = "once_cell"
530+version = "1.3.1"
531+source = "registry+https://github.com/rust-lang/crates.io-index"
532+
533+[[package]]
534+name = "oorandom"
535+version = "11.1.0"
536+source = "registry+https://github.com/rust-lang/crates.io-index"
537+
538+[[package]]
539+name = "pin-project"
540+version = "0.4.16"
541+source = "registry+https://github.com/rust-lang/crates.io-index"
542+dependencies = [
543+ "pin-project-internal 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)",
544+]
545+
546+[[package]]
547+name = "pin-project-internal"
548+version = "0.4.16"
549+source = "registry+https://github.com/rust-lang/crates.io-index"
550+dependencies = [
551+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
552+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
553+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
554+]
555+
556+[[package]]
557+name = "pin-utils"
558+version = "0.1.0"
559+source = "registry+https://github.com/rust-lang/crates.io-index"
560+
561+[[package]]
562+name = "plotters"
563+version = "0.2.12"
564+source = "registry+https://github.com/rust-lang/crates.io-index"
565+dependencies = [
566+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
567+ "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
568+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
569+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
570+]
571+
572+[[package]]
573+name = "proc-macro-hack"
574+version = "0.5.11"
575+source = "registry+https://github.com/rust-lang/crates.io-index"
576+dependencies = [
577+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
578+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
579+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
580+]
581+
582+[[package]]
583+name = "proc-macro-nested"
584+version = "0.1.3"
585+source = "registry+https://github.com/rust-lang/crates.io-index"
586+
587+[[package]]
588+name = "proc-macro2"
589+version = "1.0.18"
590+source = "registry+https://github.com/rust-lang/crates.io-index"
591+dependencies = [
592+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
593+]
331594
332595[[package]]
333596name = "quote"
334−version = "0.6.11"
597+version = "1.0.2"
335598source = "registry+https://github.com/rust-lang/crates.io-index"
336599dependencies = [
337− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
600+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
338601]
339602
340603[[package]]
341−name = "redox_syscall"
342−version = "0.1.51"
604+name = "rayon"
605+version = "1.3.0"
343606source = "registry+https://github.com/rust-lang/crates.io-index"
607+dependencies = [
608+ "crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
609+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
610+ "rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
611+]
344612
345613[[package]]
346−name = "redox_termios"
347−version = "0.1.1"
614+name = "rayon-core"
615+version = "1.7.0"
348616source = "registry+https://github.com/rust-lang/crates.io-index"
349617dependencies = [
350− "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
618+ "crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
619+ "crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
620+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
621+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
622+ "num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
351623]
352624
353625[[package]]
354626name = "regex"
355−version = "1.1.2"
627+version = "1.3.3"
356628source = "registry+https://github.com/rust-lang/crates.io-index"
357629dependencies = [
358− "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
359− "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
360− "regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
361− "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
362− "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
630+ "regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
363631]
364632
365633[[package]]
366−name = "regex-syntax"
367−version = "0.6.5"
634+name = "regex-automata"
635+version = "0.1.8"
368636source = "registry+https://github.com/rust-lang/crates.io-index"
369637dependencies = [
370− "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
638+ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
371639]
372640
373641[[package]]
374−name = "rustc-demangle"
375−version = "0.1.13"
642+name = "regex-syntax"
643+version = "0.6.13"
376644source = "registry+https://github.com/rust-lang/crates.io-index"
377645
646+[[package]]
647+name = "rustc_version"
648+version = "0.2.3"
649+source = "registry+https://github.com/rust-lang/crates.io-index"
650+dependencies = [
651+ "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
652+]
653+
378654[[package]]
379655name = "ryu"
380−version = "0.2.7"
656+version = "1.0.2"
381657source = "registry+https://github.com/rust-lang/crates.io-index"
382658
659+[[package]]
660+name = "same-file"
661+version = "1.0.5"
662+source = "registry+https://github.com/rust-lang/crates.io-index"
663+dependencies = [
664+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
665+]
666+
383667[[package]]
384668name = "scoped-tls"
385669version = "1.0.0"
386670source = "registry+https://github.com/rust-lang/crates.io-index"
387671
388672[[package]]
389−name = "serde"
390−version = "1.0.89"
673+name = "scopeguard"
674+version = "1.0.0"
391675source = "registry+https://github.com/rust-lang/crates.io-index"
392−dependencies = [
393− "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
394−]
395676
396677[[package]]
397−name = "serde_derive"
398−version = "1.0.89"
678+name = "semver"
679+version = "0.9.0"
399680source = "registry+https://github.com/rust-lang/crates.io-index"
400681dependencies = [
401− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
402− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
403− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
682+ "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
404683]
405684
406685[[package]]
407−name = "serde_json"
408−version = "1.0.39"
686+name = "semver-parser"
687+version = "0.7.0"
409688source = "registry+https://github.com/rust-lang/crates.io-index"
410−dependencies = [
411− "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
412− "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
413− "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)",
414−]
415689
416690[[package]]
417−name = "sourcefile"
418−version = "0.1.4"
691+name = "serde"
692+version = "1.0.114"
419693source = "registry+https://github.com/rust-lang/crates.io-index"
694+dependencies = [
695+ "serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
696+]
420697
421698[[package]]
422−name = "syn"
423−version = "0.15.29"
699+name = "serde_cbor"
700+version = "0.11.1"
424701source = "registry+https://github.com/rust-lang/crates.io-index"
425702dependencies = [
426− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
427− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
428− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
703+ "half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
704+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
429705]
430706
431707[[package]]
432−name = "synstructure"
433−version = "0.10.1"
708+name = "serde_derive"
709+version = "1.0.114"
434710source = "registry+https://github.com/rust-lang/crates.io-index"
435711dependencies = [
436− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
437− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
438− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
439− "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
712+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
713+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
714+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
440715]
441716
442717[[package]]
443−name = "termcolor"
444−version = "1.0.4"
718+name = "serde_json"
719+version = "1.0.57"
445720source = "registry+https://github.com/rust-lang/crates.io-index"
446721dependencies = [
447− "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
722+ "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
723+ "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
724+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
448725]
449726
450727[[package]]
451−name = "termion"
452−version = "1.5.1"
728+name = "slab"
729+version = "0.4.2"
730+source = "registry+https://github.com/rust-lang/crates.io-index"
731+
732+[[package]]
733+name = "sourcefile"
734+version = "0.1.4"
735+source = "registry+https://github.com/rust-lang/crates.io-index"
736+
737+[[package]]
738+name = "syn"
739+version = "1.0.33"
453740source = "registry+https://github.com/rust-lang/crates.io-index"
454741dependencies = [
455− "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)",
456− "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)",
457− "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
742+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
743+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
744+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
458745]
459746
460747[[package]]
461−name = "thread_local"
462−version = "0.3.6"
748+name = "textwrap"
749+version = "0.11.0"
463750source = "registry+https://github.com/rust-lang/crates.io-index"
464751dependencies = [
465− "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
752+ "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
466753]
467754
468755[[package]]
469−name = "ucd-util"
470−version = "0.1.3"
756+name = "tinytemplate"
757+version = "1.1.0"
471758source = "registry+https://github.com/rust-lang/crates.io-index"
759+dependencies = [
760+ "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
761+ "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)",
762+]
472763
473764[[package]]
474765name = "unicode-segmentation"
475−version = "1.2.1"
766+version = "1.6.0"
476767source = "registry+https://github.com/rust-lang/crates.io-index"
477768
478769[[package]]
479−name = "unicode-xid"
480−version = "0.1.0"
770+name = "unicode-width"
771+version = "0.1.6"
481772source = "registry+https://github.com/rust-lang/crates.io-index"
482773
483774[[package]]
484−name = "utf8-ranges"
485−version = "1.0.2"
775+name = "unicode-xid"
776+version = "0.2.0"
486777source = "registry+https://github.com/rust-lang/crates.io-index"
487778
488779[[package]]
@@ -490,125 +781,136 @@ name = "version_check"
490781version = "0.1.5"
491782source = "registry+https://github.com/rust-lang/crates.io-index"
492783
784+[[package]]
785+name = "walkdir"
786+version = "2.3.1"
787+source = "registry+https://github.com/rust-lang/crates.io-index"
788+dependencies = [
789+ "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
790+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
791+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
792+]
793+
493794[[package]]
494795name = "wasm-bindgen"
495−version = "0.2.38"
796+version = "0.2.55"
496797source = "registry+https://github.com/rust-lang/crates.io-index"
497798dependencies = [
498− "wasm-bindgen-macro 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
799+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
800+ "wasm-bindgen-macro 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
499801]
500802
501803[[package]]
502804name = "wasm-bindgen-backend"
503−version = "0.2.38"
805+version = "0.2.55"
504806source = "registry+https://github.com/rust-lang/crates.io-index"
505807dependencies = [
506− "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
507− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
508− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
509− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
510− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
511− "wasm-bindgen-shared 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
808+ "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
809+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
810+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
811+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
812+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
813+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
814+ "wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
512815]
513816
514817[[package]]
515818name = "wasm-bindgen-futures"
516−version = "0.3.15"
819+version = "0.4.5"
517820source = "registry+https://github.com/rust-lang/crates.io-index"
518821dependencies = [
519− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
520− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
521− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
822+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
823+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
824+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
825+ "web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
522826]
523827
524828[[package]]
525829name = "wasm-bindgen-macro"
526−version = "0.2.38"
830+version = "0.2.55"
527831source = "registry+https://github.com/rust-lang/crates.io-index"
528832dependencies = [
529− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
530− "wasm-bindgen-macro-support 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
833+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
834+ "wasm-bindgen-macro-support 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
531835]
532836
533837[[package]]
534838name = "wasm-bindgen-macro-support"
535−version = "0.2.38"
839+version = "0.2.55"
536840source = "registry+https://github.com/rust-lang/crates.io-index"
537841dependencies = [
538− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
539− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
540− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
541− "wasm-bindgen-backend 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
542− "wasm-bindgen-shared 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
842+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
843+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
844+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
845+ "wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
846+ "wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
543847]
544848
545849[[package]]
546850name = "wasm-bindgen-shared"
547−version = "0.2.38"
851+version = "0.2.55"
548852source = "registry+https://github.com/rust-lang/crates.io-index"
549853
550854[[package]]
551855name = "wasm-bindgen-test"
552−version = "0.2.38"
856+version = "0.3.5"
553857source = "registry+https://github.com/rust-lang/crates.io-index"
554858dependencies = [
555859 "console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
556− "futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)",
557− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
860+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
558861 "scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
559− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
560− "wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
561− "wasm-bindgen-test-macro 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
862+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
863+ "wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
864+ "wasm-bindgen-test-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
562865]
563866
564867[[package]]
565868name = "wasm-bindgen-test-macro"
566−version = "0.2.38"
869+version = "0.3.5"
567870source = "registry+https://github.com/rust-lang/crates.io-index"
568871dependencies = [
569− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
570− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
872+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
873+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
571874]
572875
573876[[package]]
574877name = "wasm-bindgen-webidl"
575−version = "0.2.32"
878+version = "0.2.55"
576879source = "registry+https://github.com/rust-lang/crates.io-index"
577880dependencies = [
578− "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
881+ "anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
579882 "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
580− "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
581− "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)",
582− "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)",
583− "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)",
584− "wasm-bindgen-backend 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
585− "weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
883+ "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
884+ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
885+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
886+ "syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
887+ "wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
888+ "weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
586889]
587890
588891[[package]]
589892name = "web-sys"
590−version = "0.3.15"
893+version = "0.3.32"
591894source = "registry+https://github.com/rust-lang/crates.io-index"
592895dependencies = [
593− "env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
594− "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
595− "js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
896+ "anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
897+ "js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)",
596898 "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
597− "wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
598− "wasm-bindgen-webidl 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)",
899+ "wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
900+ "wasm-bindgen-webidl 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)",
599901]
600902
601903[[package]]
602904name = "weedle"
603−version = "0.8.0"
905+version = "0.10.0"
604906source = "registry+https://github.com/rust-lang/crates.io-index"
605907dependencies = [
606− "nom 4.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
908+ "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
607909]
608910
609911[[package]]
610912name = "winapi"
611−version = "0.3.6"
913+version = "0.3.8"
612914source = "registry+https://github.com/rust-lang/crates.io-index"
613915dependencies = [
614916 "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -625,7 +927,7 @@ name = "winapi-util"
625927version = "0.1.2"
626928source = "registry+https://github.com/rust-lang/crates.io-index"
627929dependencies = [
628− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
930+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
629931]
630932
631933[[package]]
@@ -633,78 +935,101 @@ name = "winapi-x86_64-pc-windows-gnu"
633935version = "0.4.0"
634936source = "registry+https://github.com/rust-lang/crates.io-index"
635937
636−[[package]]
637−name = "wincolor"
638−version = "1.0.1"
639−source = "registry+https://github.com/rust-lang/crates.io-index"
640−dependencies = [
641− "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
642− "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
643−]
644−
645938[metadata]
646−"checksum aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
647−"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
648−"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
649−"checksum backtrace 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "cd5a90e2b463010cd0e0ce9a11d4a9d5d58d9f41d4a6ba3dcaf9e68b466e88b4"
650−"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6"
651−"checksum bumpalo 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "21ef2109240a377370f55ea3ef0b486b46d7b5c0f7455ab0ec676d73f875d58a"
652−"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
653−"checksum cc 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)" = "d01c69d08ff207f231f07196e30f84c70f1c815b04f980f8b7b01ff01f05eb92"
654−"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4"
939+"checksum anyhow 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9267dff192e68f3399525901e709a48c1d3982c9c072fa32f2127a0cb0babf14"
940+"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
941+"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
942+"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
943+"checksum bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6c2c5b58ab920a4f5aeaaca34b4488074e8cc7596af94e6f8c6ff247c60245"
944+"checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708"
945+"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
946+"checksum cast 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
947+"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
948+"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
655949"checksum console_error_panic_hook 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211"
656−"checksum console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e7871d2947441b0fdd8e2bd1ce2a2f75304f896582c0d572162d48290683c48"
657−"checksum env_logger 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b61fa891024a945da30a9581546e8cfaf5602c7b3f4c137a2805cf388f92075a"
658−"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
659−"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"
660−"checksum futures 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "49e7653e374fe0d0c12de4250f0bdb60680b8c80eed558c5c7538eec9c89e21b"
950+"checksum console_log 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494"
951+"checksum criterion 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "70daa7ceec6cf143990669a04c7df13391d55fb27bd4079d252fca774ba244d8"
952+"checksum criterion-plot 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
953+"checksum crossbeam-deque 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca"
954+"checksum crossbeam-epoch 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac"
955+"checksum crossbeam-queue 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfd6515864a82d2f877b42813d4553292c6659498c9a2aa31bab5a15243c2700"
956+"checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4"
957+"checksum csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "37519ccdfd73a75821cac9319d4fce15a81b9fcf75f951df5b9988aa3a0af87d"
958+"checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c"
959+"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
960+"checksum futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
961+"checksum futures-channel 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
962+"checksum futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
963+"checksum futures-executor 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
964+"checksum futures-io 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
965+"checksum futures-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
966+"checksum futures-sink 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
967+"checksum futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626"
968+"checksum futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
661969"checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
970+"checksum half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177"
662971"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
663−"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
664−"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b"
665−"checksum js-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "09636f30233c2623a9b4bb05506e224733550e2a13b32ab560dc1744fd5103ca"
666−"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
667−"checksum libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "aab692d7759f5cd8c859e169db98ae5b52c924add2af5fbbca11d12fefb567c1"
668−"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
669−"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
670−"checksum nom 4.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "22293d25d3f33a8567cc8a1dc20f40c7eeb761ce83d0fcca059858580790cac3"
671−"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915"
672−"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
673−"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1"
674−"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85"
675−"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
676−"checksum regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53ee8cfdddb2e0291adfb9f13d31d3bbe0a03c9a402c01b1e24188d86c35b24f"
677−"checksum regex-syntax 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8c2f35eedad5295fdf00a63d7d4b238135723f92b434ec06774dad15c7ab0861"
678−"checksum rustc-demangle 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "adacaae16d02b6ec37fdc7acfcddf365978de76d1983d3ee22afc260e1ca9619"
679−"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7"
972+"checksum hermit-abi 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "307c3c9f937f38e3534b1d6447ecf090cafcc9744e4a6360e8b037b2cf5af120"
973+"checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
974+"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
975+"checksum js-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)" = "1c840fdb2167497b0bd0db43d6dfe61e91637fa72f9d061f8bd17ddc44ba6414"
976+"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
977+"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
978+"checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
979+"checksum longest-increasing-subsequence 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86"
980+"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
981+"checksum memoffset 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9"
982+"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
983+"checksum num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4"
984+"checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
985+"checksum once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1c601810575c99596d4afc46f78a678c80105117c379eb3650cf99b8a21ce5b"
986+"checksum oorandom 11.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ebcec7c9c2a95cacc7cd0ecb89d8a8454eca13906f6deb55258ffff0adeb9405"
987+"checksum pin-project 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)" = "81d480cb4e89522ccda96d0eed9af94180b7a5f93fb28f66e1fd7d68431663d1"
988+"checksum pin-project-internal 0.4.16 (registry+https://github.com/rust-lang/crates.io-index)" = "a82996f11efccb19b685b14b5df818de31c1edcee3daa256ab5775dd98e72feb"
989+"checksum pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
990+"checksum plotters 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "4e3bb8da247d27ae212529352020f3e5ee16e83c0c258061d27b08ab92675eeb"
991+"checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
992+"checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e"
993+"checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
994+"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
995+"checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098"
996+"checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9"
997+"checksum regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87"
998+"checksum regex-automata 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "92b73c2a1770c255c240eaa4ee600df1704a38dc3feaa6e949e7fcd4f8dc09f9"
999+"checksum regex-syntax 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90"
1000+"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
1001+"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
1002+"checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421"
6801003"checksum scoped-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
681−"checksum serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "92514fb95f900c9b5126e32d020f5c6d40564c27a5ea6d1d7d9f157a96623560"
682−"checksum serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6eabf4b5914e88e24eea240bb7c9f9a2cbc1bbbe8d961d381975ec3c6b806c"
683−"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d"
1004+"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
1005+"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
1006+"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
1007+"checksum serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)" = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
1008+"checksum serde_cbor 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622"
1009+"checksum serde_derive 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)" = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
1010+"checksum serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)" = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
1011+"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
6841012"checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3"
685−"checksum syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1825685f977249735d510a242a6727b46efe914bb67e38d30c071b1b72b1d5c2"
686−"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
687−"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f"
688−"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
689−"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
690−"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
691−"checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1"
692−"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
693−"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
1013+"checksum syn 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
1014+"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
1015+"checksum tinytemplate 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f"
1016+"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
1017+"checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20"
1018+"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
6941019"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
695−"checksum wasm-bindgen 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "f7148f7446b911dd8e8d490a4fd44bf7fcd6caa21ba2fed9aef7bf34b9a974d4"
696−"checksum wasm-bindgen-backend 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "07b7b7e0a5c244e02469d7ad0f071dca0a01a7be217db41e4de5e05d4a1ab2ac"
697−"checksum wasm-bindgen-futures 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "ad790bdb3818ba6fba63d3a3fd65ab52ca7f601cad1c6c1feda66199b4e9f161"
698−"checksum wasm-bindgen-macro 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "e51818f0ab6cdef85125d2c2f1a89096370286fe229d24de89504617918d76af"
699−"checksum wasm-bindgen-macro-support 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "4770fd638bed46ce4669cb59aba4657e264839c24c568cc03d772849bfaed7eb"
700−"checksum wasm-bindgen-shared 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "6f52047198d1887ffd4e508264bffbf92382a97c8b09b74781dbb11ee055ccc5"
701−"checksum wasm-bindgen-test 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "3f551eb8ad779620e0bb424f6936c87aaea8d93db72fc6d6292c179306773b6f"
702−"checksum wasm-bindgen-test-macro 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "5d7bf67f27d40d702d1c85778abaa354142b77ef00528b72127bb392b8db39e2"
703−"checksum wasm-bindgen-webidl 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)" = "264e2ff77974c11640a2c6a91435f645843d2bd7e12e3ce58ac74209e6d5f76d"
704−"checksum web-sys 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a9c8ff79e13b74f47f438cd48de2ed808c419cd52ec4780dfc6d1de4a37be19"
705−"checksum weedle 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26a4c67f132386d965390b8a734d5d10adbcd30eb5cc74bd9229af8b83f10044"
706−"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
1020+"checksum walkdir 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
1021+"checksum wasm-bindgen 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "29ae32af33bacd663a9a28241abecf01f2be64e6a185c6139b04f18b6385c5f2"
1022+"checksum wasm-bindgen-backend 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "1845584bd3593442dc0de6e6d9f84454a59a057722f36f005e44665d6ab19d85"
1023+"checksum wasm-bindgen-futures 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1458706aa1b8fe6898d19433c9f110d93a05d1f22ae6adf55810409a94df34b4"
1024+"checksum wasm-bindgen-macro 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "87fcc747e6b73c93d22c947a6334644d22cfec5abd8b66238484dc2b0aeb9fe4"
1025+"checksum wasm-bindgen-macro-support 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "3dc4b3f2c4078c8c4a5f363b92fcf62604c5913cbd16c6ff5aaf0f74ec03f570"
1026+"checksum wasm-bindgen-shared 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "ca0b78d6d3be8589b95d1d49cdc0794728ca734adf36d7c9f07e6459508bb53d"
1027+"checksum wasm-bindgen-test 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ed5833682054e941db0b7e2b4877b61bbaafd680608e40d268e070d34558b305"
1028+"checksum wasm-bindgen-test-macro 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9c5dcf70d21d0ff0ea5cdb97dba9a9b56ddad4a9bd10c8912eedc55618be022f"
1029+"checksum wasm-bindgen-webidl 0.2.55 (registry+https://github.com/rust-lang/crates.io-index)" = "3126356474ceb717c8fb5549ae387c9fbf4872818454f4d87708bee997214bb5"
1030+"checksum web-sys 0.3.32 (registry+https://github.com/rust-lang/crates.io-index)" = "98405c0a2e722ed3db341b4c5b70eb9fe0021621f7350bab76df93b09b649bbf"
1031+"checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
1032+"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
7071033"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
7081034"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
7091035"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
710−"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
Cargo.toml+51−14
@@ -8,23 +8,52 @@ license = "MPL-2.0"
88name = "dodrio"
99readme = "./README.md"
1010repository = "https://github.com/fitzgen/dodrio"
11−version = "0.1.0"
11+version = "0.2.0"
1212
1313[badges]
1414travis-ci = { repository = "fitzgen/dodrio" }
1515
16+[lib]
17+path = "src/lib.rs"
18+bench = false
19+
20+[[test]]
21+name = "web"
22+path = "tests/web/main.rs"
23+required-features = ["xxx-unstable-internal-use-only"]
24+
25+[[bench]]
26+name = "benches"
27+path = "benches/benches.rs"
28+harness = false
29+required-features = ["xxx-unstable-internal-use-only"]
30+
31+[features]
32+# Don't rely on this feature! Its behavior will change and it could go away at
33+# any time. This enables monkey-patching DOM methods so that we wrap them and
34+# trace what DOM methods are used and how many times they are invoked when
35+# applying our emitted change lists to the physical DOM. This is useful for
36+# evaluating the differences between how Dodrio applies changes from virtual
37+# nodes to the physical DOM versus how other virtual DOM libraries do the same.
38+xxx-unstable-strace = []
39+
40+# Don't use this feature! It only exists for exposing otherwise-internal APIs
41+# for testing and benchmarks.
42+xxx-unstable-internal-use-only = []
43+
1644[dependencies]
17−bumpalo = "2.1.0"
18−cfg-if = "0.1.7"
19−futures = "0.1.25"
20−wasm-bindgen = "0.2.38"
21−wasm-bindgen-futures = "0.3.15"
22−js-sys = "0.3.15"
45+bumpalo = "2.6.0"
46+cfg-if = "0.1.10"
2347fxhash = "0.2.1"
24−log = { version = "0.4.6", optional = true }
48+js-sys = "0.3.32"
49+log = { version = "0.4.11", optional = true }
50+longest-increasing-subsequence = "0.1.0"
51+serde = { version = "1.0.114", optional = true }
52+wasm-bindgen = "0.2.55"
53+wasm-bindgen-futures = "0.4.5"
2554
2655[dependencies.web-sys]
27−version = "0.3.15"
56+version = "0.3.32"
2857features = [
2958 "console",
3059 "Document",
@@ -35,13 +64,14 @@ features = [
3564]
3665
3766[dev-dependencies]
38−console_log = "0.1.2"
39−dodrio-js-api = { version = "=0.1.0", path = "./crates/js-api" }
40−log = "0.4.6"
41−wasm-bindgen-test = "0.2.38"
67+console_log = "0.2.0"
68+dodrio-js-api = { version = "=0.2.0", path = "./crates/js-api" }
69+futures = "0.3.5"
70+log = "0.4.11"
71+wasm-bindgen-test = "0.3.5"
4272
4373[dev-dependencies.web-sys]
44−version = "0.3.15"
74+version = "0.3.32"
4575features = [
4676 "Attr",
4777 "EventTarget",
@@ -50,6 +80,12 @@ features = [
5080 "NodeList",
5181]
5282
83+[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
84+criterion = "0.3.3"
85+
86+[profile.bench]
87+debug = true
88+
5389[profile.release]
5490incremental = false
5591lto = true
@@ -64,5 +100,6 @@ members = [
64100 "./examples/input-form",
65101 "./examples/js-component",
66102 "./examples/moire",
103+ "./examples/sierpinski-triangle",
67104 "./examples/todomvc",
68105]
README.md+13−6
@@ -31,12 +31,10 @@ struct Hello {
3131}
3232
3333impl Render for Hello {
34− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
35− where
36− 'a: 'bump,
37− {
38− span(bump)
39− .children([text("Hello, "), text(&self.who), text("!")])
34+ fn render<'a>(&self, cx: &mut RenderContext<a>) -> Node<'a> {
35+ let who = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
36+ div(cx)
37+ .children([text(who.into_bump_str())])
4038 .finish()
4139 }
4240}
@@ -59,6 +57,15 @@ directory](https://github.com/fitzgen/dodrio/tree/master/examples), including:
5957* [`js-component`](https://github.com/fitzgen/dodrio/tree/master/examples/js-component):
6058 Defines a rendering component in JavaScript with the `dodrio-js-api` crate.
6159
60+## Cargo Features
61+
62+* `log` &mdash; enable debugging-oriented log messages with the `log` crate's
63+ facade. You still have to initialize a logger for the messages to go anywhere,
64+ such as [`console_log`](https://github.com/iamcodemaker/console_log).
65+
66+* `serde` &mdash; enable `serde::{Serialize, Deserialize}` implementations for
67+ `Cached<R>` where `R` is serializable and deserializable.
68+
6269## Design
6370
6471### Bump Allocation
benches/benches.rsnew file+118
@@ -0,0 +1,118 @@
1+#![cfg(all(
2+ feature = "xxx-unstable-internal-use-only",
3+ not(target_arch = "wasm32")
4+))]
5+
6+use criterion::{
7+ black_box, criterion_group, criterion_main, Benchmark, Criterion, ParameterizedBenchmark,
8+ Throughput,
9+};
10+use dodrio::{
11+ builder::*,
12+ bumpalo::{self, Bump},
13+ CachedSet, Node, Render, RenderContext, Vdom,
14+};
15+use std::cell::RefCell;
16+use std::convert::TryInto;
17+
18+/// The simplest thing we can render: `<div/>`.
19+struct Empty;
20+impl<'a> Render<'a> for Empty {
21+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
22+ div(&cx).finish()
23+ }
24+}
25+
26+/// Render a list that is `self.0` items long, has attributes and listeners.
27+struct SimpleList(usize);
28+impl<'a> Render<'a> for SimpleList {
29+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
30+ let mut children = bumpalo::collections::Vec::with_capacity_in(self.0, cx.bump);
31+ children.extend((0..self.0).map(|_| {
32+ li(&cx)
33+ .attr("class", "my-list-item")
34+ .on("click", |_root, _vdom, _event| {
35+ panic!("no one should call this")
36+ })
37+ .children([text("a list item")])
38+ .finish()
39+ }));
40+ ol(&cx).attr("id", "my-list").children(children).finish()
41+ }
42+}
43+
44+fn criterion_benchmark(c: &mut Criterion) {
45+ c.bench(
46+ "render",
47+ Benchmark::new("empty", {
48+ let mut bump = Bump::new();
49+ let cached_set = RefCell::new(CachedSet::default());
50+ let mut templates = Default::default();
51+ move |b| {
52+ bump.reset();
53+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
54+ b.iter(|| {
55+ black_box(Empty.render(&mut cx));
56+ })
57+ }
58+ }),
59+ );
60+
61+ c.bench(
62+ "render",
63+ ParameterizedBenchmark::new(
64+ "list",
65+ {
66+ let mut bump = Bump::new();
67+ let cached_set = RefCell::new(CachedSet::default());
68+ let mut templates = Default::default();
69+ move |b, n| {
70+ bump.reset();
71+ let mut cx = RenderContext::new(&bump, &cached_set, &mut templates);
72+ b.iter(|| {
73+ black_box(SimpleList(*n).render(&mut cx));
74+ })
75+ }
76+ },
77+ vec![100, 1_000, 10_000],
78+ )
79+ .throughput(|n| Throughput::Elements((*n).try_into().unwrap())),
80+ );
81+
82+ c.bench(
83+ "render-and-diff",
84+ ParameterizedBenchmark::new(
85+ "same-list",
86+ |b, &n| {
87+ let vdom = Vdom::new(&(), SimpleList(n));
88+ b.iter(|| {
89+ vdom.immediately_render_and_diff(SimpleList(n));
90+ black_box(&vdom);
91+ })
92+ },
93+ vec![100, 1_000, 10_000],
94+ )
95+ .with_function("empty-to-full-list-to-empty", |b, &n| {
96+ b.iter(|| {
97+ let vdom = Vdom::new(&(), Empty);
98+ vdom.immediately_render_and_diff(SimpleList(n));
99+ black_box(&vdom);
100+ vdom.immediately_render_and_diff(Empty);
101+ black_box(&vdom);
102+ });
103+ })
104+ .with_function("append-one-and-remove-one", |b, &n| {
105+ let vdom = Vdom::new(&(), SimpleList(n));
106+ b.iter(|| {
107+ vdom.immediately_render_and_diff(SimpleList(n + 1));
108+ black_box(&vdom);
109+ vdom.immediately_render_and_diff(SimpleList(n));
110+ black_box(&vdom);
111+ })
112+ })
113+ .throughput(|n| Throughput::Elements((*n).try_into().unwrap())),
114+ );
115+}
116+
117+criterion_group!(benches, criterion_benchmark);
118+criterion_main!(benches);
build.sh+2−2
@@ -4,8 +4,8 @@ cd $(dirname $0)
44set -eux
55
66cargo fmt --all || true
7−cargo check --all --target wasm32-unknown-unknown
7+cargo build --all --target wasm32-unknown-unknown
88
99for x in ./examples/*; do
10− wasm-pack build --target no-modules "$x"
10+ wasm-pack build --target web "$x"
1111done
crates/js-api/Cargo.toml+6−7
@@ -7,17 +7,16 @@ keywords = ["javascript", "js", "vdom", "virtual-dom"]
77license = "MPL-2.0"
88name = "dodrio-js-api"
99repository = "https://github.com/fitzgen/dodrio"
10−version = "0.1.0"
10+version = "0.2.0"
1111
1212[badges]
1313travis-ci = { repository = "fitzgen/dodrio" }
1414
1515[dependencies]
16−dodrio = { version = "=0.1.0", path = "../.." }
17−wasm-bindgen = "0.2.38"
18−js-sys = "0.3.15"
19−wasm-bindgen-futures = "0.3.15"
20−futures = "0.1.25"
16+dodrio = { version = "=0.2.0", path = "../.." }
17+wasm-bindgen = "0.2.55"
18+js-sys = "0.3.32"
19+wasm-bindgen-futures = "0.4.5"
2120
2221[dev-dependencies]
23−wasm-bindgen-test = "0.2.38"
22+wasm-bindgen-test = "0.3.5"
crates/js-api/src/lib.rs+30−38
@@ -66,8 +66,7 @@ And here is a Rust rendering component that internally uses the JS rendering
6666component:
6767
6868```rust,no_run
69−use dodrio::bumpalo::Bump;
70−use dodrio::{Node, Render, Vdom};
69+use dodrio::{Node, Render, RenderContext, Vdom};
7170use dodrio_js_api::JsRender;
7271use js_sys::Object;
7372use wasm_bindgen::prelude::*;
@@ -100,16 +99,13 @@ impl GreetingViaJs {
10099
101100/// And finally the `Render` implementation! This adds a `<p>` element and some
102101/// text around whatever the inner JS `Greeting` component renders.
103−impl Render for GreetingViaJs {
104− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
105− where
106− 'a: 'bump,
107− {
102+impl<'a> Render<'a> for GreetingViaJs {
103+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
108104 use dodrio::builder::*;
109− p(bump)
105+ p(&cx)
110106 .children([
111107 text("JavaScript says: "),
112− self.js.render(bump),
108+ self.js.render(cx),
113109 ])
114110 .finish()
115111 }
@@ -119,12 +115,7 @@ impl Render for GreetingViaJs {
119115 */
120116#![deny(missing_docs, missing_debug_implementations)]
121117
122−use dodrio::{
123− builder,
124− bumpalo::{self, Bump},
125− Node, Render,
126−};
127−use futures::prelude::*;
118+use dodrio::{builder, bumpalo, Node, Render, RenderContext};
128119use js_sys::{Object, Promise, Reflect};
129120use wasm_bindgen::prelude::*;
130121use wasm_bindgen::JsCast;
@@ -193,12 +184,16 @@ impl VdomWeak {
193184 /// Schedule re-rendering of the virtual DOM. A promise is returned that is
194185 /// resolved after the rendering has happened.
195186 pub fn render(&self) -> Promise {
196− wasm_bindgen_futures::future_to_promise(
197− self.inner.render().map(|_| JsValue::null()).map_err(|e| {
187+ let future = self.inner.render();
188+
189+ wasm_bindgen_futures::future_to_promise(async move {
190+ if let Err(e) = future.await {
198191 let msg = e.to_string();
199− js_sys::Error::new(&msg).into()
200− }),
201− )
192+ Err(js_sys::Error::new(&msg).into())
193+ } else {
194+ Ok(JsValue::null())
195+ }
196+ })
202197 }
203198}
204199
@@ -221,12 +216,9 @@ impl JsRender {
221216 }
222217}
223218
224−impl Render for JsRender {
225− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
226− where
227− 'a: 'bump,
228− {
229− create(bump, self.render())
219+impl<'a> Render<'a> for JsRender {
220+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
221+ create(cx, self.render())
230222 }
231223}
232224
@@ -234,9 +226,9 @@ fn has_property(obj: &Object, property: &str) -> bool {
234226 Reflect::has(obj, &property.into()).unwrap_or_default()
235227}
236228
237−fn create(bump: &Bump, val: JsValue) -> Node {
229+fn create<'a>(cx: &mut RenderContext<'a>, val: JsValue) -> Node<'a> {
238230 if let Some(txt) = val.as_string() {
239− let text = bumpalo::collections::String::from_str_in(&txt, bump);
231+ let text = bumpalo::collections::String::from_str_in(&txt, cx.bump);
240232 return builder::text(text.into_bump_str());
241233 }
242234
@@ -251,12 +243,12 @@ fn create(bump: &Bump, val: JsValue) -> Node {
251243 );
252244
253245 let tag_name = elem.tag_name();
254− let tag_name = bumpalo::collections::String::from_str_in(&tag_name, bump);
246+ let tag_name = bumpalo::collections::String::from_str_in(&tag_name, cx.bump);
255247
256− builder::ElementBuilder::new(bump, tag_name.into_bump_str())
248+ builder::ElementBuilder::new(cx.bump, tag_name.into_bump_str())
257249 .listeners({
258250 let mut listeners =
259− bumpalo::collections::Vec::new_in(bump);
251+ bumpalo::collections::Vec::new_in(cx.bump);
260252 if has_property(&elem, "listeners") {
261253 let js_listeners = elem.listeners();
262254 listeners.reserve(js_listeners.length() as usize);
@@ -275,10 +267,10 @@ fn create(bump: &Bump, val: JsValue) -> Node {
275267 "listener objects returned by JS render methods must have an `callback` property"
276268 );
277269 let on = listener.on();
278− let on = bumpalo::collections::String::from_str_in(&on, bump);
270+ let on = bumpalo::collections::String::from_str_in(&on, cx.bump);
279271 let callback = listener.callback();
280272 let elem = elem.clone();
281− listeners.push(builder::on(bump, on.into_bump_str(), move |_root, vdom, event| {
273+ listeners.push(builder::on(cx.bump, on.into_bump_str(), move |_root, vdom, event| {
282274 let vdom = VdomWeak::new(vdom);
283275 let vdom: JsValue = vdom.into();
284276 if let Err(e) = callback.call2(&elem, &vdom, &event) {
@@ -290,7 +282,7 @@ fn create(bump: &Bump, val: JsValue) -> Node {
290282 listeners
291283 })
292284 .attributes({
293− let mut attributes = bumpalo::collections::Vec::new_in(bump);
285+ let mut attributes = bumpalo::collections::Vec::new_in(cx.bump);
294286 if has_property(&elem, "attributes") {
295287 let js_attributes = elem.attributes();
296288 attributes.reserve(js_attributes.length() as usize);
@@ -309,21 +301,21 @@ fn create(bump: &Bump, val: JsValue) -> Node {
309301 "attribute objects returned by JS render methods must have a `value` property"
310302 );
311303 let name = attribute.name();
312− let name = bumpalo::collections::String::from_str_in(&name, bump);
304+ let name = bumpalo::collections::String::from_str_in(&name, cx.bump);
313305 let value = attribute.value();
314− let value = bumpalo::collections::String::from_str_in(&value, bump);
306+ let value = bumpalo::collections::String::from_str_in(&value, cx.bump);
315307 attributes.push(builder::attr(name.into_bump_str(), value.into_bump_str()));
316308 });
317309 }
318310 attributes
319311 })
320312 .children({
321− let mut children = bumpalo::collections::Vec::new_in(bump);
313+ let mut children = bumpalo::collections::Vec::new_in(cx.bump);
322314 if has_property(&elem, "children") {
323315 let js_children = elem.children();
324316 children.reserve(js_children.length() as usize);
325317 js_children.for_each(&mut |child, _index, _array| {
326− children.push(create(bump, child));
318+ children.push(create(cx, child));
327319 });
328320 }
329321 children
examples/counter/Cargo.toml+5−5
@@ -11,13 +11,13 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313console_error_panic_hook = "0.1.6"
14−console_log = "0.1.2"
14+console_log = "0.2.0"
1515dodrio = { path = "../.." }
16−log = "0.4.6"
17−wasm-bindgen = "0.2.38"
16+log = "0.4.11"
17+wasm-bindgen = "0.2.55"
1818
1919[dependencies.web-sys]
20−version = "0.3.15"
20+version = "0.3.32"
2121features = [
2222 "console",
2323 "Document",
@@ -30,4 +30,4 @@ features = [
3030]
3131
3232[dev-dependencies]
33−wasm-bindgen-test = "0.2.38"
33+wasm-bindgen-test = "0.3.5"
examples/counter/README.md+1−1
@@ -9,7 +9,7 @@ See `src/lib.rs`.
99## Build
1010
1111```
12−wasm-pack build --target no-modules
12+wasm-pack build --target web
1313```
1414
1515## Serve
examples/counter/index.html+3−3
@@ -5,9 +5,9 @@
55 <title>Counter</title>
66 </head>
77 <body>
8− <script src="pkg/dodrio_counter.js"></script>
9− <script>
10− wasm_bindgen("pkg/dodrio_counter_bg.wasm");
8+ <script type="module">
9+ import init from "./pkg/dodrio_counter.js";
10+ init("./pkg/dodrio_counter_bg.wasm");
1111 </script>
1212 </body>
1313</html>
examples/counter/src/lib.rsmode 100644 → 100755+10−13
@@ -1,9 +1,8 @@
1−use dodrio::bumpalo::{self, Bump};
2−use dodrio::Render;
1+use dodrio::{bumpalo, Node, Render, RenderContext};
32use log::*;
43use wasm_bindgen::prelude::*;
54
6−/// A counter that can be incremented and decrmented!
5+/// A counter that can be incremented and decremented!
76struct Counter {
87 count: isize,
98}
@@ -27,19 +26,16 @@ impl Counter {
2726
2827// The `Render` implementation for `Counter`s displays the current count and has
2928// buttons to increment and decrement the count.
30−impl Render for Counter {
31− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> dodrio::Node<'bump>
32− where
33− 'a: 'bump,
34− {
29+impl<'a> Render<'a> for Counter {
30+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
3531 use dodrio::builder::*;
3632
3733 // Stringify the count as a bump-allocated string.
38− let count = bumpalo::format!(in bump, "{}", self.count);
34+ let count = bumpalo::format!(in cx.bump, "{}", self.count);
3935
40− div(bump)
36+ div(&cx)
4137 .children([
42− button(bump)
38+ button(&cx)
4339 .on("click", |root, vdom, _event| {
4440 // Cast the root render component to a `Counter`, since
4541 // we know that's what it is.
@@ -55,10 +51,11 @@ impl Render for Counter {
5551 .children([text("+")])
5652 .finish(),
5753 text(count.into_bump_str()),
58− button(bump)
54+ button(&cx)
5955 .on("click", |root, vdom, _event| {
6056 // Same as above, but decrementing instead of incrementing.
61− root.unwrap_mut::<Counter>().decrement();
57+ let counter = root.unwrap_mut::<Counter>();
58+ counter.decrement();
6259 vdom.schedule_render();
6360 })
6461 .children([text("-")])
examples/game-of-life/Cargo.toml+5−6
@@ -11,14 +11,13 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.38"
14+wasm-bindgen = "0.2.55"
1515console_error_panic_hook = "0.1.6"
16−js-sys = "0.3.15"
17−wasm-bindgen-futures = "0.3.15"
18−futures = "0.1.25"
16+js-sys = "0.3.32"
17+wasm-bindgen-futures = "0.4.5"
1918
2019[dependencies.web-sys]
21−version = "0.3.15"
20+version = "0.3.32"
2221features = [
2322 "Document",
2423 "HtmlElement",
@@ -27,4 +26,4 @@ features = [
2726]
2827
2928[dev-dependencies]
30−wasm-bindgen-test = "0.2.38"
29+wasm-bindgen-test = "0.3.5"
examples/game-of-life/README.md+1−1
@@ -11,7 +11,7 @@ See `src/lib.rs`.
1111## Build
1212
1313```
14−wasm-pack build --target no-modules
14+wasm-pack build --target web
1515```
1616
1717## Serve
examples/game-of-life/index.htmlmode 100644 → 100755+3−6
@@ -19,9 +19,6 @@
1919 padding: 0;
2020 }
2121
22− .universe {
23− }
24−
2522 body {
2623 width: 100vw;
2724 height: 100vh;
@@ -39,9 +36,9 @@
3936 </style>
4037 </head>
4138 <body>
42− <script src="pkg/dodrio_game_of_life.js"></script>
43− <script>
44− wasm_bindgen("pkg/dodrio_game_of_life_bg.wasm");
39+ <script type="module">
40+ import init from "./pkg/dodrio_game_of_life.js";
41+ init("./pkg/dodrio_game_of_life_bg.wasm");
4542 </script>
4643 </body>
4744</html>
examples/game-of-life/src/lib.rs+25−23
@@ -1,6 +1,4 @@
1−use dodrio::bumpalo::{self, Bump};
2−use dodrio::{Node, Render, Vdom};
3−use futures::prelude::*;
1+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
42use std::cell::RefCell;
53use std::rc::Rc;
64use wasm_bindgen::{prelude::*, JsCast};
@@ -128,21 +126,19 @@ impl Universe {
128126}
129127
130128/// The rendering implementation for our Game of Life.
131−impl Render for Universe {
132− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
133− where
134− 'a: 'bump,
135− {
129+impl<'a> Render<'a> for Universe {
130+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
136131 use dodrio::builder::*;
137132
138− let mut rows = bumpalo::collections::Vec::with_capacity_in(self.height as usize, bump);
133+ let mut rows = bumpalo::collections::Vec::with_capacity_in(self.height as usize, cx.bump);
139134
140135 for row in self.cells.chunks(self.width as usize) {
141− let mut cells = bumpalo::collections::Vec::with_capacity_in(self.width as usize, bump);
136+ let mut cells =
137+ bumpalo::collections::Vec::with_capacity_in(self.width as usize, cx.bump);
142138
143139 for cell in row {
144140 cells.push(
145− span(bump)
141+ span(&cx)
146142 .attr("class", "cell")
147143 .attr(
148144 "style",
@@ -155,10 +151,10 @@ impl Render for Universe {
155151 );
156152 }
157153
158− rows.push(div(bump).attr("class", "row").children(cells).finish());
154+ rows.push(div(&cx).attr("class", "row").children(cells).finish());
159155 }
160156
161− div(bump).attr("class", "universe").children(rows).finish()
157+ div(&cx).attr("class", "universe").children(rows).finish()
162158 }
163159}
164160
@@ -175,26 +171,32 @@ pub fn run() {
175171 // Create a new Game of Life `Universe` render component.
176172 let universe = Universe::new();
177173
178− // Create a virtual DOM and mount it and the `Hello` render component to the
174+ // Create a virtual DOM and mount it and the `Universe` render component to the
179175 // `<body>`.
180176 let vdom = Vdom::new(body.as_ref(), universe);
181177
182178 // Kick off a loop that keeps computing a tick of the universe and then
183179 // re-rendering on every animation frame.
184− let rc: Rc<RefCell<Option<Closure<FnMut()>>>> = Rc::new(RefCell::new(None));
180+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
185181 let rc2 = rc.clone();
186182 let window2 = window.clone();
187183 let weak = vdom.weak();
188184 let f = Closure::wrap(Box::new(move || {
189185 weak.schedule_render();
190186
191− wasm_bindgen_futures::spawn_local(
192− weak.with_component(|root| {
193− let universe = root.unwrap_mut::<Universe>();
194− universe.tick();
195− })
196− .map_err(|_| wasm_bindgen::throw_str("impossible, we always keep the vdom alive")),
197− );
187+ wasm_bindgen_futures::spawn_local({
188+ let weak = weak.clone();
189+ async move {
190+ let fut = weak.with_component(|root| {
191+ let universe = root.unwrap_mut::<Universe>();
192+ universe.tick();
193+ });
194+
195+ if fut.await.is_err() {
196+ wasm_bindgen::throw_str("impossible, we always keep the vdom alive");
197+ }
198+ }
199+ });
198200
199201 window
200202 .request_animation_frame(
@@ -205,7 +207,7 @@ pub fn run() {
205207 .unchecked_ref::<js_sys::Function>(),
206208 )
207209 .unwrap_throw();
208− }) as Box<FnMut()>);
210+ }) as Box<dyn FnMut()>);
209211 window2
210212 .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
211213 .unwrap_throw();
examples/hello-world/Cargo.toml+3−3
@@ -11,11 +11,11 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.38"
14+wasm-bindgen = "0.2.55"
1515console_error_panic_hook = "0.1.6"
1616
1717[dependencies.web-sys]
18−version = "0.3.15"
18+version = "0.3.32"
1919features = [
2020 "Document",
2121 "HtmlElement",
@@ -24,4 +24,4 @@ features = [
2424]
2525
2626[dev-dependencies]
27−wasm-bindgen-test = "0.2.38"
27+wasm-bindgen-test = "0.3.5"
examples/hello-world/README.md+1−1
@@ -9,7 +9,7 @@ See `src/lib.rs`.
99## Build
1010
1111```
12−wasm-pack build --target no-modules
12+wasm-pack build --target web
1313```
1414
1515## Serve
examples/hello-world/index.html+3−3
@@ -5,9 +5,9 @@
55 <title>Hello, World!</title>
66 </head>
77 <body>
8− <script src="pkg/dodrio_hello_world.js"></script>
9− <script>
10− wasm_bindgen("pkg/dodrio_hello_world_bg.wasm");
8+ <script type="module">
9+ import init from "./pkg/dodrio_hello_world.js";
10+ init("./pkg/dodrio_hello_world_bg.wasm");
1111 </script>
1212 </body>
1313</html>
examples/hello-world/src/lib.rs+12−14
@@ -1,24 +1,20 @@
1−use dodrio::bumpalo::Bump;
2−use dodrio::{Node, Render, Vdom};
1+use dodrio::{builder::*, bumpalo};
2+use dodrio::{Node, Render, RenderContext, Vdom};
33use wasm_bindgen::prelude::*;
44
55/// A rendering component that displays a greeting.
6−struct Hello<'who> {
6+struct Hello {
77 /// Who to greet.
8− who: &'who str,
8+ who: String,
99}
1010
1111// The `Render` implementation describes how to render a `Hello` component into
1212// HTML.
13−impl<'who> Render for Hello<'who> {
14− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
15− where
16− 'a: 'bump,
17− {
18− use dodrio::builder::*;
19− p(bump)
20− .children([text("Hello, "), text(self.who), text("!")])
21− .finish()
13+impl<'a> Render<'a> for Hello {
14+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
15+ let msg = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
16+ let msg = msg.into_bump_str();
17+ p(&cx).children([text(msg)]).finish()
2218 }
2319}
2420
@@ -33,7 +29,9 @@ pub fn run() {
3329 let body = document.body().unwrap_throw();
3430
3531 // Create a new `Hello` render component.
36− let component = Hello { who: "World" };
32+ let component = Hello {
33+ who: String::from("World"),
34+ };
3735
3836 // Create a virtual DOM and mount it and the `Hello` render component to the
3937 // `<body>`.
examples/input-form/Cargo.toml+3−3
@@ -11,11 +11,11 @@ crate-type = ["cdylib"]
1111
1212[dependencies]
1313dodrio = { path = "../.." }
14−wasm-bindgen = "0.2.38"
14+wasm-bindgen = "0.2.55"
1515console_error_panic_hook = "0.1.6"
1616
1717[dependencies.web-sys]
18−version = "0.3.15"
18+version = "0.3.32"
1919features = [
2020 "console",
2121 "Document",
@@ -29,4 +29,4 @@ features = [
2929]
3030
3131[dev-dependencies]
32−wasm-bindgen-test = "0.2.38"
32+wasm-bindgen-test = "0.3.5"
examples/input-form/README.md+1−1
@@ -10,7 +10,7 @@ See `src/lib.rs`.
1010## Build
1111
1212```
13−wasm-pack build --target no-modules
13+wasm-pack build --target web
1414```
1515
1616## Serve
examples/input-form/index.html+3−3
@@ -5,9 +5,9 @@
55 <title>Input Form</title>
66 </head>
77 <body>
8− <script src="pkg/dodrio_input_form.js"></script>
9− <script>
10− wasm_bindgen("pkg/dodrio_input_form_bg.wasm");
8+ <script type="module">
9+ import init from "./pkg/dodrio_input_form.js";
10+ init("./pkg/dodrio_input_form_bg.wasm");
1111 </script>
1212 </body>
1313</html>
examples/input-form/src/lib.rs+10−11
@@ -1,5 +1,4 @@
1−use dodrio::bumpalo::{self, Bump};
2−use dodrio::{Node, Render};
1+use dodrio::{bumpalo, Node, Render, RenderContext};
32use wasm_bindgen::prelude::*;
43use wasm_bindgen::JsCast;
54
@@ -24,18 +23,18 @@ impl SayHelloTo {
2423
2524// The `Render` implementation has a text `<input>` and a `<div>` that shows a
2625// greeting to the `<input>`'s value.
27−impl Render for SayHelloTo {
28− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
29− where
30− 'a: 'bump,
31− {
26+impl<'a> Render<'a> for SayHelloTo {
27+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
3228 use dodrio::builder::*;
3329
34− div(bump)
30+ div(&cx)
3531 .children([
36− input(bump)
32+ input(&cx)
3733 .attr("type", "text")
38− .attr("value", &self.who)
34+ .attr(
35+ "value",
36+ bumpalo::format!(in cx.bump, "{}", self.who).into_bump_str(),
37+ )
3938 .on("input", |root, vdom, event| {
4039 // If the event's target is our input...
4140 let input = match event
@@ -55,7 +54,7 @@ impl Render for SayHelloTo {
5554 vdom.schedule_render();
5655 })
5756 .finish(),
58− text(bumpalo::format!(in bump, "Hello, {}!", self.who).into_bump_str()),
57+ text(bumpalo::format!(in cx.bump, "Hello, {}!", self.who).into_bump_str()),
5958 ])
6059 .finish()
6160 }
examples/js-component/Cargo.toml+3−3
@@ -12,12 +12,12 @@ crate-type = ["cdylib"]
1212[dependencies]
1313dodrio = { path = "../.." }
1414dodrio-js-api = { path = "../../crates/js-api" }
15−wasm-bindgen = "0.2.38"
15+wasm-bindgen = "0.2.55"
1616console_error_panic_hook = "0.1.6"
17−js-sys = "0.3.15"
17+js-sys = "0.3.32"
1818
1919[dependencies.web-sys]
20−version = "0.3.15"
20+version = "0.3.32"
2121features = [
2222 "Document",
2323 "HtmlElement",
examples/js-component/README.md+1−1
@@ -11,7 +11,7 @@ See `src/lib.rs` for the Rust code.
1111## Build
1212
1313```
14−wasm-pack build --target no-modules
14+wasm-pack build --target web
1515```
1616
1717## Serve
examples/js-component/index.html+3−3
@@ -6,9 +6,9 @@
66 </head>
77 <body>
88 <script src="greeting.js"></script>
9− <script src="pkg/dodrio_js_component.js"></script>
10− <script>
11− wasm_bindgen("pkg/dodrio_js_component_bg.wasm");
9+ <script type="module">
10+ import init from "./pkg/dodrio_js_component.js";
11+ init("./pkg/dodrio_js_component_bg.wasm");
1212 </script>
1313 </body>
1414</html>
examples/js-component/src/lib.rs+6−10
@@ -1,5 +1,4 @@
1−use dodrio::bumpalo::Bump;
2−use dodrio::{Node, Render, Vdom};
1+use dodrio::{Node, Render, RenderContext, Vdom};
32use dodrio_js_api::JsRender;
43use js_sys::Object;
54use wasm_bindgen::prelude::*;
@@ -32,14 +31,11 @@ impl GreetingViaJs {
3231
3332/// Here's the `Render` implementation! This adds a `<p>` element and some text
3433/// around whatever the inner JS `Greeting` component renders.
35−impl Render for GreetingViaJs {
36− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
37− where
38− 'a: 'bump,
39− {
34+impl<'a> Render<'a> for GreetingViaJs {
35+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
4036 use dodrio::builder::*;
41− p(bump)
42− .children([text("JavaScript says: "), self.js.render(bump)])
37+ p(&cx)
38+ .children([text("JavaScript says: "), self.js.render(cx)])
4339 .finish()
4440 }
4541}
@@ -58,7 +54,7 @@ pub fn run() {
5854 // Create a new `GreetingViaJs` render component.
5955 let component = GreetingViaJs::new("World");
6056
61− // Create a virtual DOM and mount it and the `Hello` render component to the
57+ // Create a virtual DOM and mount it and the `GreetingViaJs` render component to the
6258 // `<body>`.
6359 let vdom = Vdom::new(body.as_ref(), component);
6460
examples/moire/Cargo.toml+8−9
@@ -8,20 +8,19 @@ edition = "2018"
88crate-type = ["cdylib"]
99
1010[dependencies]
11−cfg-if = "0.1.7"
11+cfg-if = "0.1.10"
1212dodrio = { path = "../.." }
13−futures = "0.1.25"
14−log = "0.4.6"
15−wasm-bindgen = "0.2.38"
16−wasm-bindgen-futures = "0.3.15"
13+log = "0.4.11"
14+wasm-bindgen = "0.2.55"
15+wasm-bindgen-futures = "0.4.5"
1716
1817# Optional dependencies for logging.
1918console_error_panic_hook = { optional = true, version = "0.1.6" }
20−console_log = { optional = true, version = "0.1.2" }
21−js-sys = "0.3.15"
19+console_log = { optional = true, version = "0.2.0" }
20+js-sys = "0.3.32"
2221
2322[dependencies.web-sys]
24−version = "0.3.15"
23+version = "0.3.32"
2524features = [
2625 "Document",
2726 "Element",
@@ -35,7 +34,7 @@ features = [
3534]
3635
3736[dev-dependencies]
38−wasm-bindgen-test = "0.2.38"
37+wasm-bindgen-test = "0.3.5"
3938
4039[features]
4140logging = [
examples/moire/README.md+1−1
@@ -14,7 +14,7 @@ conversion and other color utilities.
1414## Build
1515
1616```
17−wasm-pack build --target no-modules
17+wasm-pack build --target web
1818```
1919
2020## Serve
examples/moire/index.html+3−3
@@ -42,9 +42,9 @@
4242 <body>
4343 <code>FPS: <output id="fps"></code></code>
4444 <div id="scene"></div>
45− <script src="pkg/dodrio_moire.js"></script>
46− <script>
47− wasm_bindgen("pkg/dodrio_moire_bg.wasm");
45+ <script type="module">
46+ import init from "./pkg/dodrio_moire.js";
47+ init("./pkg/dodrio_moire_bg.wasm");
4848 </script>
4949 <script>
5050 let lastTime = 0
examples/moire/src/lib.rs+38−64
@@ -1,13 +1,11 @@
11mod colors;
22
33use cfg_if::cfg_if;
4−use dodrio::bumpalo::{self, Bump};
5−use dodrio::{Node, Render, Vdom};
4+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
65use std::cell::RefCell;
76use std::rc::Rc;
87use std::str;
9−use wasm_bindgen::prelude::*;
10−use wasm_bindgen::JsCast;
8+use wasm_bindgen::{prelude::*, JsCast};
119
1210/// This is the main function that is automatically invoked when the wasm module
1311/// is loaded.
@@ -24,12 +22,12 @@ pub fn run() {
2422 // Create a new `Moire` render component.
2523 let component = Moire::new();
2624
27− // Create a virtual DOM and mount it and the `Hello` render component to the
25+ // Create a virtual DOM and mount it and the `Moire` render component to the
2826 // scene.
2927 let vdom = Vdom::new(&scene, component);
3028
3129 // Kick off a loop that keeps re-rendering on every animation frame.
32− let rc: Rc<RefCell<Option<Closure<FnMut()>>>> = Rc::new(RefCell::new(None));
30+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
3331 let rc2 = rc.clone();
3432 let window2 = window.clone();
3533 let weak = vdom.weak();
@@ -44,7 +42,7 @@ pub fn run() {
4442 .unchecked_ref::<js_sys::Function>(),
4543 )
4644 .unwrap_throw();
47− }) as Box<FnMut()>);
45+ }) as Box<dyn FnMut()>);
4846 window2
4947 .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
5048 .unwrap_throw();
@@ -106,11 +104,8 @@ impl Moire {
106104 }
107105}
108106
109−impl Render for Moire {
110− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
111− where
112− 'a: 'bump,
113− {
107+impl<'a> Render<'a> for Moire {
108+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
114109 use dodrio::builder::*;
115110
116111 let elapsed = web_sys::window()
@@ -120,18 +115,18 @@ impl Render for Moire {
120115 .now();
121116 let elapsed = elapsed / 1600.0;
122117
123− main(bump)
124− .attr("style", self.moire_style(bump, elapsed))
118+ main(&cx)
119+ .attr("style", self.moire_style(cx, elapsed))
125120 .children([
126121 // The `<input>` that lets users control how many circles to
127122 // render.
128− input(bump)
123+ input(&cx)
129124 .attr("id", "circle-count")
130125 .attr("type", "range")
131126 .attr("min", "30")
132127 .attr("max", "500")
133128 .attr("value", {
134− let count = bumpalo::format!(in bump, "{}", self.count);
129+ let count = bumpalo::format!(in cx.bump, "{}", self.count);
135130 count.into_bump_str()
136131 })
137132 .on("change", |root, _vdom, event| {
@@ -144,8 +139,8 @@ impl Render for Moire {
144139 })
145140 .finish(),
146141 // And the actual concentric circles that form the Moire patterns.
147− self.orbiting_objects(bump, elapsed),
148− self.lemniscate_objects(bump, elapsed),
142+ self.orbiting_objects(cx, elapsed),
143+ self.lemniscate_objects(cx, elapsed),
149144 ])
150145 .finish()
151146 }
@@ -154,14 +149,11 @@ impl Render for Moire {
154149/// Rendering helper methods.
155150impl Moire {
156151 /// Generate the main Moire element's inline CSS styles.
157− fn moire_style<'a, 'bump>(&'a self, bump: &'bump Bump, elapsed: f64) -> &'bump str
158− where
159− 'a: 'bump,
160− {
152+ fn moire_style<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> &'a str {
161153 let elapsed = elapsed / 3.0;
162154 let color = colors::get_interpolated_color(|(_, bg)| bg, elapsed % 1.0);
163155 let style = bumpalo::format!(
164− in bump,
156+ in cx.bump,
165157 "background-color: rgb({}, {}, {})",
166158 color.r,
167159 color.g,
@@ -171,93 +163,75 @@ impl Moire {
171163 }
172164
173165 /// Generate the orbiting circle objects.
174− fn orbiting_objects<'a, 'bump>(&'a self, bump: &'bump Bump, elapsed: f64) -> Node<'bump>
175− where
176− 'a: 'bump,
177− {
166+ fn orbiting_objects<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> Node<'a> {
178167 const D: f64 = 200.0;
179168 let x = elapsed.sin() * D;
180169 let y = elapsed.cos() * D;
181− self.moving_object(bump, elapsed, x, y)
170+ self.moving_object(cx, elapsed, x, y)
182171 }
183172
184173 /// Generate the lemniscate circle objects that interact with the orbiting
185174 /// circle objects to form Moire patterns.
186− fn lemniscate_objects<'a, 'bump>(&'a self, bump: &'bump Bump, elapsed: f64) -> Node<'bump>
187− where
188− 'a: 'bump,
189− {
175+ fn lemniscate_objects<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> Node<'a> {
190176 const A: f64 = 150.0;
191177 let x = elapsed.sin() * A;
192178 let y = elapsed.sin() * elapsed.cos() * A;
193− self.moving_object(bump, elapsed, x, y)
179+ self.moving_object(cx, elapsed, x, y)
194180 }
195181
196− fn moving_object<'a, 'bump>(
197− &'a self,
198− bump: &'bump Bump,
182+ fn moving_object<'a>(
183+ &self,
184+ cx: &mut RenderContext<'a>,
199185 elapsed: f64,
200186 x: f64,
201187 y: f64,
202− ) -> Node<'bump>
203− where
204− 'a: 'bump,
205− {
188+ ) -> Node<'a> {
206189 use dodrio::builder::*;
207− div(bump)
190+ div(&cx)
208191 .attr("class", "object")
209− .attr("style", self.moving_object_style(bump, x, y))
210− .children([self.circle(bump, elapsed, self.count)])
192+ .attr("style", self.moving_object_style(cx, x, y))
193+ .children([self.circle(cx, elapsed, self.count)])
211194 .finish()
212195 }
213196
214197 /// Generate inline CSS styles for a moving object.
215− fn moving_object_style<'a, 'bump>(&'a self, bump: &'bump Bump, x: f64, y: f64) -> &'bump str
216− where
217− 'a: 'bump,
218− {
219− bumpalo::format!(in bump, "left: {}px; top: {}px;", x, y).into_bump_str()
198+ fn moving_object_style<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64) -> &'a str {
199+ bumpalo::format!(in cx.bump, "left: {}px; top: {}px;", x, y).into_bump_str()
220200 }
221201
222202 /// Recursively generate `self.count` concentric circles.
223− fn circle<'a, 'bump>(&'a self, bump: &'bump Bump, elapsed: f64, n: u32) -> Node<'bump>
224− where
225− 'a: 'bump,
226− {
203+ fn circle<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64, n: u32) -> Node<'a> {
227204 use dodrio::builder::*;
228205
229206 let r = n * 16;
230207
231− let mut circle = div(bump)
208+ let mut circle = div(&cx)
232209 .attr("class", "circle")
233210 .attr("data-radius", {
234− let r = bumpalo::format!(in bump, "{}", r);
211+ let r = bumpalo::format!(in cx.bump, "{}", r);
235212 r.into_bump_str()
236213 })
237− .attr("style", self.circle_style(bump, elapsed, n, r));
214+ .attr("style", self.circle_style(cx, elapsed, n, r));
238215
239216 if n > 0 {
240− circle = circle.child(self.circle(bump, elapsed, n - 1));
217+ circle = circle.child(self.circle(cx, elapsed, n - 1));
241218 }
242219
243220 circle.finish()
244221 }
245222
246223 /// Generate inline CSS styles for a circle.
247− fn circle_style<'a, 'bump>(
248− &'a self,
249− bump: &'bump Bump,
224+ fn circle_style<'a>(
225+ &self,
226+ cx: &mut RenderContext<'a>,
250227 elapsed: f64,
251228 n: u32,
252229 r: u32,
253− ) -> &'bump str
254− where
255− 'a: 'bump,
256− {
230+ ) -> &'a str {
257231 let (border, alpha) = self.circle_color(elapsed, n);
258232 let margin = r / 2 + 3;
259233 let style = bumpalo::format!(
260− in bump,
234+ in cx.bump,
261235 "border-color: rgba({}, {}, {}, {});\
262236 margin-left: -{}px;\
263237 margin-top: -{}px;\
examples/sierpinski-triangle/Cargo.tomlnew file+28
@@ -0,0 +1,28 @@
1+[package]
2+name = "dodrio-sierpinski"
3+version = "0.1.0"
4+authors = ["Steven Seguin <steven.seguin@gmail.com>"]
5+edition = "2018"
6+
7+[lib]
8+crate-type = ["cdylib"]
9+
10+[dependencies]
11+dodrio = { path = "../.." }
12+wasm-bindgen = "0.2.55"
13+console_error_panic_hook = "0.1.6"
14+js-sys = "0.3.32"
15+wasm-bindgen-futures = "0.4.5"
16+
17+[dependencies.web-sys]
18+version = "0.3.32"
19+features = [
20+ "Document",
21+ "HtmlElement",
22+ "Node",
23+ "Performance",
24+ "Window",
25+]
26+
27+[dev-dependencies]
28+wasm-bindgen-test = "0.3.5"
examples/sierpinski-triangle/README.mdnew file+24
@@ -0,0 +1,24 @@
1+# Sierpinski Triangle
2+
3+A Sierpinkski triangle that constantly shrinks and grows, and whose nodes have
4+a value that increments by one every second.
5+
6+Ported from the React Fiber example [here](https://github.com/claudiopro/react-fiber-vs-stack-demo).
7+
8+## Source
9+
10+See `src/lib.rs`.
11+
12+## Build
13+
14+```
15+wasm-pack build --target web
16+```
17+
18+## Serve
19+
20+Use any HTTP server, for example:
21+
22+```
23+python -m SimpleHTTPServer
24+```
examples/sierpinski-triangle/ferris.pngnew file

Binary file not shown.

examples/sierpinski-triangle/index.htmlnew file+69
@@ -0,0 +1,69 @@
1+<!DOCTYPE html>
2+<html>
3+
4+<head>
5+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
6+ <title>Sierpinski Triangle</title>
7+ <style>
8+ .container {
9+ position: absolute;
10+ transform-origin: 0 0;
11+ left: 50%;
12+ top: 50%;
13+ width: 10px;
14+ height: 10px;
15+ background: #eee;
16+ }
17+
18+ .dot {
19+ position: absolute;
20+ background: #61dafb;
21+ font: normal 15px sans-serif;
22+ text-align: center;
23+ cursor: pointer;
24+ }
25+
26+ .dot:hover {
27+ background: url("ferris.png") no-repeat center center;
28+ z-index: 999;
29+ }
30+
31+ code {
32+ position: relative;
33+ font-size: 150%;
34+ }
35+ </style>
36+</head>
37+
38+<body>
39+ <code>FPS: <output id="fps">...</output></code>
40+ <div id="scene"></div>
41+
42+ <script type="module">
43+ import init from "./pkg/dodrio_sierpinski.js";
44+ init("./pkg/dodrio_sierpinski_bg.wasm");
45+ </script>
46+
47+ <script>
48+ let lastTime = 0;
49+ let frameCount = 0;
50+ const fps = document.getElementById('fps');
51+
52+ function updateFPS(time) {
53+ frameCount++;
54+ if (lastTime + 1000.0 <= time) {
55+ fps.textContent = frameCount;
56+ lastTime = time;
57+ frameCount = 0;
58+ }
59+ }
60+
61+ requestAnimationFrame(function loop(now) {
62+ updateFPS(now);
63+ requestAnimationFrame(loop);
64+ })
65+
66+ </script>
67+</body>
68+
69+</html>
examples/sierpinski-triangle/src/lib.rsnew file+157
@@ -0,0 +1,157 @@
1+use dodrio::{bumpalo, Node, Render, RenderContext, Vdom};
2+use std::cell::RefCell;
3+use std::rc::Rc;
4+use wasm_bindgen::{prelude::*, JsCast};
5+use web_sys::Window;
6+
7+// Triangle container defining target size.
8+struct Container {
9+ size: f64,
10+}
11+
12+impl Container {
13+ // Construct a new container.
14+ pub fn new() -> Container {
15+ Container { size: 25.0 }
16+ }
17+
18+ // Generate the container style to fluctuate triangle width.
19+ fn container_transform<'a>(&self, cx: &mut RenderContext<'a>, elapsed: f64) -> &'a str {
20+ let t = elapsed % 10.0;
21+ let scale = 0.45 + (if t > 5.0 { 10.0 - t } else { t }) / 40.0;
22+ let transform = bumpalo::format!(
23+ in cx.bump,
24+ "transform: scaleX({}) scaleY(0.7) translateZ(0.1px)",
25+ scale
26+ );
27+ transform.into_bump_str()
28+ }
29+
30+ // Generate the dot's position on the grid.
31+ fn dot_style<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64) -> &'a str {
32+ let s = self.size * 1.3;
33+ let radius = s / 2.0;
34+ let styles = bumpalo::format!(
35+ in cx.bump,
36+ r#"
37+ width: {}px;
38+ height: {}px;
39+ left: {}px;
40+ top: {}px;
41+ border-radius: {}px;
42+ line-height: {}px;
43+ "#,
44+ s, s, x, y, radius, s
45+ );
46+ styles.into_bump_str()
47+ }
48+
49+ // Create a dot node.
50+ fn dot<'a>(&self, cx: &mut RenderContext<'a>, x: f64, y: f64, content: u32) -> Node<'a> {
51+ use dodrio::builder::{div, text};
52+
53+ div(&cx)
54+ .attr("class", "dot")
55+ .attr("style", self.dot_style(cx, x, y))
56+ .child(text(
57+ bumpalo::format!(in cx.bump, "{}", content).into_bump_str(),
58+ ))
59+ .finish()
60+ }
61+
62+ // Create a flattened vector of dot nodes (arranged in recursive triangles)
63+ fn triangle<'a>(
64+ &self,
65+ cx: &mut RenderContext<'a>,
66+ x: f64,
67+ y: f64,
68+ s: f64,
69+ content: u32,
70+ children: &mut bumpalo::collections::Vec<Node<'a>>,
71+ ) {
72+ if s <= self.size {
73+ children.push(self.dot(cx, x, y, content));
74+ return;
75+ }
76+
77+ let s = s / 2.0;
78+
79+ // Top of triangle
80+ self.triangle(cx, x, y - (s / 2.0), s, content, children);
81+ // Bottom-left of triangle
82+ self.triangle(cx, x - s, y + (s / 2.0), s, content, children);
83+ // Bottom-right of triangle
84+ self.triangle(cx, x + s, y + (s / 2.0), s, content, children);
85+ }
86+}
87+
88+impl<'a> Render<'a> for Container {
89+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
90+ use dodrio::builder::div;
91+
92+ let elapsed = web_sys::window()
93+ .unwrap_throw()
94+ .performance()
95+ .unwrap_throw()
96+ .now()
97+ / 1000.0;
98+
99+ let modulus = elapsed as u32 % 10;
100+
101+ let mut children = bumpalo::collections::Vec::new_in(cx.bump);
102+ self.triangle(cx, 0.0, 0.0, 1000.0, modulus, &mut children);
103+
104+ div(&cx)
105+ .attr("class", "container")
106+ .attr("style", self.container_transform(cx, elapsed))
107+ .children(children)
108+ .finish()
109+ }
110+}
111+
112+// Kick off a loop that keeps re-rendering on every animation frame.
113+fn animate(window: Window, vdom: Vdom) {
114+ let rc = <Rc<RefCell<Option<Closure<dyn FnMut()>>>>>::default();
115+ let rc2 = rc.clone();
116+ let window2 = window.clone();
117+ let weak = vdom.weak();
118+ let f = Closure::wrap(Box::new(move || {
119+ weak.schedule_render();
120+ window
121+ .request_animation_frame(
122+ rc.borrow()
123+ .as_ref()
124+ .unwrap_throw()
125+ .as_ref()
126+ .unchecked_ref::<js_sys::Function>(),
127+ )
128+ .unwrap_throw();
129+ }) as Box<dyn FnMut()>);
130+ window2
131+ .request_animation_frame(f.as_ref().unchecked_ref::<js_sys::Function>())
132+ .unwrap_throw();
133+ *rc2.borrow_mut() = Some(f);
134+
135+ // Run the virtual DOM and its listeners forever.
136+ vdom.forget();
137+}
138+
139+#[wasm_bindgen(start)]
140+pub fn run() {
141+ // Set up the panic hook
142+ console_error_panic_hook::set_once();
143+
144+ // Get the scene element to render within
145+ let window = web_sys::window().unwrap();
146+ let document = window.document().unwrap();
147+ let scene = document.get_element_by_id("scene").unwrap();
148+
149+ // Construct a new Container component.
150+ let container = Container::new();
151+
152+ // Mount our container component to the scene div.
153+ let vdom = Vdom::new(&scene, container);
154+
155+ // Kick off animation loop
156+ animate(window, vdom);
157+}
examples/todomvc/Cargo.toml+11−12
@@ -5,22 +5,21 @@ name = "dodrio-todomvc"
55version = "0.1.0"
66
77[dependencies]
8−cfg-if = "0.1.7"
9−dodrio = { path = "../.." }
10−futures = "0.1.25"
11−js-sys = "0.3.15"
12−serde = { features = ["derive"], version = "1.0.89" }
13−serde_json = "1.0.39"
14−wasm-bindgen = "0.2.38"
15−wasm-bindgen-futures = "0.3.15"
8+cfg-if = "0.1.10"
9+dodrio = { path = "../..", features = ["serde"] }
10+js-sys = "0.3.32"
11+serde = { features = ["derive"], version = "1.0.114" }
12+serde_json = "1.0.57"
13+wasm-bindgen = "0.2.55"
14+wasm-bindgen-futures = "0.4.5"
1615
1716# Optional dependencies for logging.
1817console_error_panic_hook = { optional = true, version = "0.1.6" }
19−console_log = { optional = true, version = "0.1.2" }
20−log = { optional = true, version = "0.4.6" }
18+console_log = { optional = true, version = "0.2.0" }
19+log = { optional = true, version = "0.4.11" }
2120
2221[dependencies.web-sys]
23−version = "0.3.15"
22+version = "0.3.32"
2423features = [
2524 "Document",
2625 "Event",
@@ -35,7 +34,7 @@ features = [
3534]
3635
3736[dev-dependencies]
38−wasm-bindgen-test = "0.2.38"
37+wasm-bindgen-test = "0.3.5"
3938
4039[features]
4140logging = [
examples/todomvc/README.md+1−1
@@ -21,7 +21,7 @@ most important are:
2121## Build
2222
2323```
24−wasm-pack build --target no-modules
24+wasm-pack build --target web
2525```
2626
2727## Serve
examples/todomvc/index.html+3−3
@@ -13,9 +13,9 @@
1313 <footer class="info">
1414 <p>Double-click to edit a todo</p>
1515 </footer>
16− <script src="pkg/dodrio_todomvc.js"></script>
17− <script>
18− wasm_bindgen("pkg/dodrio_todomvc_bg.wasm");
16+ <script type="module">
17+ import init from "./pkg/dodrio_todomvc.js";
18+ init("./pkg/dodrio_todomvc_bg.wasm");
1919 </script>
2020 </body>
2121</html>
examples/todomvc/src/router.rsmode 100644 → 100755+23−21
@@ -4,10 +4,8 @@ use crate::todos::Todos;
44use crate::utils;
55use crate::visibility::Visibility;
66use dodrio::VdomWeak;
7−use futures::prelude::*;
87use std::str::FromStr;
9−use wasm_bindgen::prelude::*;
10−use wasm_bindgen::JsCast;
8+use wasm_bindgen::{prelude::*, JsCast};
119
1210/// Start the router.
1311pub fn start(vdom: VdomWeak) {
@@ -30,23 +28,27 @@ pub fn start(vdom: VdomWeak) {
3028 v
3129 });
3230
33− wasm_bindgen_futures::spawn_local(
34− vdom.with_component({
35− let vdom = vdom.clone();
36− move |root| {
37− let todos = root.unwrap_mut::<Todos>();
38− // If the todos' visibility already matches the event's
39− // visibility, then there is nothing to do (ha). If they
40− // don't match, then we need to update the todos' visibility
41− // and re-render.
42− if todos.visibility() != new_vis {
43− todos.set_visibility(new_vis);
44− vdom.schedule_render();
45− }
46− }
47− })
48− .map_err(|_| ()),
49− );
31+ wasm_bindgen_futures::spawn_local({
32+ let vdom = vdom.clone();
33+ async move {
34+ let _ = vdom
35+ .with_component({
36+ let vdom = vdom.clone();
37+ move |root| {
38+ let todos = root.unwrap_mut::<Todos>();
39+ // If the todos' visibility already matches the event's
40+ // visibility, then there is nothing to do (ha). If they
41+ // don't match, then we need to update the todos' visibility
42+ // and re-render.
43+ if todos.visibility() != new_vis {
44+ todos.set_visibility(new_vis);
45+ vdom.schedule_render();
46+ }
47+ }
48+ })
49+ .await;
50+ }
51+ });
5052 };
5153
5254 // Call it once to handle the initial `#` fragment.
@@ -57,7 +59,7 @@ pub fn start(vdom: VdomWeak) {
5759 // Note that if we ever intended to unmount our todos app, we would want to
5860 // provide a method for removing this router's event listener and cleaning
5961 // up after ourselves.
60− let on_hash_change = Closure::wrap(Box::new(on_hash_change) as Box<FnMut()>);
62+ let on_hash_change = Closure::wrap(Box::new(on_hash_change) as Box<dyn FnMut()>);
6163 let window = utils::window();
6264 window
6365 .add_event_listener_with_callback("hashchange", on_hash_change.as_ref().unchecked_ref())
examples/todomvc/src/todo.rs+86−32
@@ -1,14 +1,19 @@
11//! Type definition and `dodrio::Render` implementation for a single todo item.
22
33use crate::keys;
4−use dodrio::{bumpalo::Bump, Node, Render, RootRender, VdomWeak};
4+use dodrio::{Cached, Node, Render, RenderContext, RootRender, VdomWeak};
55use serde::{Deserialize, Serialize};
66use std::marker::PhantomData;
77use wasm_bindgen::{prelude::*, JsCast};
88
99/// A single todo item.
10−#[derive(Serialize, Deserialize)]
10+#[derive(Default)]
1111pub struct Todo<C> {
12+ inner: Cached<TodoInner<C>>,
13+}
14+
15+#[derive(Serialize, Deserialize)]
16+struct TodoInner<C> {
1217 id: usize,
1318 title: String,
1419 completed: bool,
@@ -48,93 +53,110 @@ impl<C> Todo<C> {
4853 let completed = false;
4954 let edits = None;
5055 Todo {
51− id,
52− title,
53− completed,
54− edits,
55− _controller: PhantomData,
56+ inner: Cached::new(TodoInner {
57+ id,
58+ title,
59+ completed,
60+ edits,
61+ _controller: PhantomData,
62+ }),
5663 }
5764 }
5865
5966 /// Set this todo item's id.
6067 pub fn set_id(&mut self, id: usize) {
61− self.id = id;
68+ if id != self.inner.id {
69+ Cached::invalidate(&self.inner);
70+ }
71+ self.inner.id = id;
6272 }
6373
6474 /// Is this `Todo` complete?
6575 pub fn is_complete(&self) -> bool {
66− self.completed
76+ self.inner.completed
6777 }
6878
6979 /// Mark the `Todo` as complete or not.
7080 pub fn set_complete(&mut self, to: bool) {
71− self.completed = to;
81+ if to != self.inner.completed {
82+ Cached::invalidate(&self.inner);
83+ }
84+ self.inner.completed = to;
7285 }
7386
7487 /// Get this todo's title.
7588 pub fn title(&self) -> &str {
76− &self.title
89+ &self.inner.title
7790 }
7891
7992 /// Set this todo item's title.
8093 pub fn set_title<S: Into<String>>(&mut self, title: S) {
81− self.title = title.into();
94+ let title = title.into();
95+ if title != self.inner.title {
96+ Cached::invalidate(&self.inner);
97+ }
98+ self.inner.title = title;
8299 }
83100
84101 /// Set the edits for this todo.
85102 pub fn set_edits<S: Into<String>>(&mut self, edits: Option<S>) {
86− self.edits = edits.map(Into::into);
103+ let edits = edits.map(Into::into);
104+ if edits != self.inner.edits {
105+ Cached::invalidate(&self.inner);
106+ }
107+ self.inner.edits = edits;
87108 }
88109
89110 /// Take this todo's edits, leaving `None` in their place.
90111 pub fn take_edits(&mut self) -> Option<String> {
91− self.edits.take()
112+ if self.inner.edits.is_some() {
113+ Cached::invalidate(&self.inner);
114+ }
115+ self.inner.edits.take()
92116 }
93117}
94118
95−impl<C: TodoActions> Render for Todo<C> {
96− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
97− where
98− 'a: 'bump,
99− {
119+impl<'a, C: TodoActions> Render<'a> for Todo<C> {
120+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
100121 use dodrio::{
101122 builder::*,
102123 bumpalo::{self, collections::String},
103124 };
104125
105− let id = self.id;
106− let title = self.edits.as_ref().unwrap_or(&self.title);
126+ let id = self.inner.id;
127+ let title = self.inner.edits.as_ref().unwrap_or(&self.inner.title);
128+ let title = bumpalo::format!(in cx.bump, "{}", title).into_bump_str();
107129
108− li(bump)
130+ li(&cx)
109131 .attr("class", {
110− let mut class = String::new_in(bump);
111− if self.completed {
132+ let mut class = String::new_in(cx.bump);
133+ if self.inner.completed {
112134 class.push_str("completed ");
113135 }
114− if self.edits.is_some() {
136+ if self.inner.edits.is_some() {
115137 class.push_str("editing");
116138 }
117139 class.into_bump_str()
118140 })
119141 .children([
120− div(bump)
142+ div(&cx)
121143 .attr("class", "view")
122144 .children([
123− input(bump)
145+ input(&cx)
124146 .attr("class", "toggle")
125147 .attr("type", "checkbox")
126− .bool_attr("checked", self.completed)
148+ .bool_attr("checked", self.inner.completed)
127149 .on("click", move |root, vdom, _event| {
128150 C::toggle_completed(root, vdom, id);
129151 })
130152 .finish(),
131− label(bump)
153+ label(&cx)
132154 .on("dblclick", move |root, vdom, _event| {
133155 C::begin_editing(root, vdom, id);
134156 })
135157 .children([text(title)])
136158 .finish(),
137− button(bump)
159+ button(&cx)
138160 .attr("class", "destroy")
139161 .on("click", move |root, vdom, _event| {
140162 C::delete(root, vdom, id);
@@ -142,13 +164,13 @@ impl<C: TodoActions> Render for Todo<C> {
142164 .finish(),
143165 ])
144166 .finish(),
145− input(bump)
167+ input(&cx)
146168 .attr("class", "edit")
147169 .attr("value", title)
148170 .attr("name", "title")
149171 .attr(
150172 "id",
151− bumpalo::format!(in bump, "todo-{}", id).into_bump_str(),
173+ bumpalo::format!(in cx.bump, "todo-{}", id).into_bump_str(),
152174 )
153175 .on("input", move |root, vdom, event| {
154176 let input = event
@@ -173,3 +195,35 @@ impl<C: TodoActions> Render for Todo<C> {
173195 .finish()
174196 }
175197}
198+
199+impl<C> Default for TodoInner<C> {
200+ fn default() -> Self {
201+ TodoInner {
202+ id: Default::default(),
203+ title: Default::default(),
204+ completed: Default::default(),
205+ edits: Default::default(),
206+ _controller: PhantomData,
207+ }
208+ }
209+}
210+
211+impl<C> serde::Serialize for Todo<C> {
212+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
213+ where
214+ S: serde::Serializer,
215+ {
216+ self.inner.serialize(serializer)
217+ }
218+}
219+
220+impl<'de, C> Deserialize<'de> for Todo<C> {
221+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
222+ where
223+ D: serde::Deserializer<'de>,
224+ {
225+ Ok(Todo {
226+ inner: <Cached<TodoInner<C>>>::deserialize(deserializer)?,
227+ })
228+ }
229+}
examples/todomvc/src/todos.rsmode 100644 → 100755+40−56
@@ -5,15 +5,11 @@ use crate::controller::Controller;
55use crate::todo::{Todo, TodoActions};
66use crate::visibility::Visibility;
77use crate::{keys, utils};
8−use dodrio::{
9− bumpalo::{self, Bump},
10− Node, Render, RootRender, VdomWeak,
11−};
8+use dodrio::{bumpalo, Node, Render, RenderContext, RootRender, VdomWeak};
129use serde::{Deserialize, Serialize};
1310use std::marker::PhantomData;
1411use std::mem;
15−use wasm_bindgen::prelude::*;
16−use wasm_bindgen::JsCast;
12+use wasm_bindgen::{prelude::*, JsCast};
1713
1814/// A collection of todos.
1915#[derive(Default, Serialize, Deserialize)]
@@ -32,7 +28,7 @@ pub struct Todos<C = Controller> {
3228}
3329
3430/// Actions for `Todos` that can be triggered by UI interactions.
35−pub trait TodosActions: TodoActions {
31+pub trait TodosActions: 'static + TodoActions {
3632 /// Toggle the completion state of all todo items.
3733 fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak);
3834
@@ -53,7 +49,7 @@ pub trait TodosActions: TodoActions {
5349impl<C> Todos<C> {
5450 /// Construct a new todos set.
5551 ///
56− /// If an existing set is available in local storage, then us that,
52+ /// If an existing set is available in local storage, then use that,
5753 /// otherwise create a new set.
5854 pub fn new() -> Self
5955 where
@@ -136,17 +132,14 @@ impl<C> Todos<C> {
136132
137133/// Rendering helpers.
138134impl<C: TodosActions> Todos<C> {
139− fn header<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
140− where
141− 'a: 'bump,
142− {
135+ fn header<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
143136 use dodrio::builder::*;
144137
145− header(bump)
138+ header(&cx)
146139 .attr("class", "header")
147140 .children([
148− h1(bump).children([text("todos")]).finish(),
149− input(bump)
141+ h1(&cx).children([text("todos")]).finish(),
142+ input(&cx)
150143 .on("input", |root, vdom, event| {
151144 let input = event
152145 .target()
@@ -163,19 +156,19 @@ impl<C: TodosActions> Todos<C> {
163156 .attr("class", "new-todo")
164157 .attr("placeholder", "What needs to be done?")
165158 .attr("autofocus", "")
166− .attr("value", &self.draft)
159+ .attr(
160+ "value",
161+ bumpalo::format!(in cx.bump, "{}", self.draft).into_bump_str(),
162+ )
167163 .finish(),
168164 ])
169165 .finish()
170166 }
171167
172− fn todos_list<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
173− where
174− 'a: 'bump,
175− {
168+ fn todos_list<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
176169 use dodrio::{builder::*, bumpalo::collections::Vec};
177170
178− let mut todos = Vec::with_capacity_in(self.todos.len(), bump);
171+ let mut todos = Vec::with_capacity_in(self.todos.len(), cx.bump);
179172 todos.extend(
180173 self.todos
181174 .iter()
@@ -184,10 +177,10 @@ impl<C: TodosActions> Todos<C> {
184177 Visibility::Active => !t.is_complete(),
185178 Visibility::Completed => t.is_complete(),
186179 })
187− .map(|t| t.render(bump)),
180+ .map(|t| t.render(cx)),
188181 );
189182
190− section(bump)
183+ section(&cx)
191184 .attr("class", "main")
192185 .attr(
193186 "visibility",
@@ -198,7 +191,7 @@ impl<C: TodosActions> Todos<C> {
198191 },
199192 )
200193 .children([
201− input(bump)
194+ input(&cx)
202195 .attr("class", "toggle-all")
203196 .attr("id", "toggle-all")
204197 .attr("type", "checkbox")
@@ -208,19 +201,16 @@ impl<C: TodosActions> Todos<C> {
208201 C::toggle_all(root, vdom);
209202 })
210203 .finish(),
211− label(bump)
204+ label(&cx)
212205 .attr("for", "toggle-all")
213206 .children([text("Mark all as complete")])
214207 .finish(),
215− ul(bump).attr("class", "todo-list").children(todos).finish(),
208+ ul(&cx).attr("class", "todo-list").children(todos).finish(),
216209 ])
217210 .finish()
218211 }
219212
220− fn footer<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
221− where
222− 'a: 'bump,
223− {
213+ fn footer<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
224214 use dodrio::builder::*;
225215
226216 let completed_count = self.todos.iter().filter(|t| t.is_complete()).count();
@@ -230,36 +220,36 @@ impl<C: TodosActions> Todos<C> {
230220 } else {
231221 " items left"
232222 };
233− let incomplete_count = bumpalo::format!(in bump, "{}", incomplete_count);
223+ let incomplete_count = bumpalo::format!(in cx.bump, "{}", incomplete_count);
234224
235225 let clear_completed_text = bumpalo::format!(
236− in bump,
226+ in cx.bump,
237227 "Clear completed ({})",
238228 self.todos.iter().filter(|t| t.is_complete()).count()
239229 );
240230
241− footer(bump)
231+ footer(&cx)
242232 .attr("class", "footer")
243233 .bool_attr("hidden", self.todos.is_empty())
244234 .children([
245− span(bump)
235+ span(&cx)
246236 .attr("class", "todo-count")
247237 .children([
248− strong(bump)
238+ strong(&cx)
249239 .children([text(incomplete_count.into_bump_str())])
250240 .finish(),
251241 text(items_left),
252242 ])
253243 .finish(),
254− ul(bump)
244+ ul(&cx)
255245 .attr("class", "filters")
256246 .children([
257− self.visibility_swap(bump, "#/", Visibility::All),
258− self.visibility_swap(bump, "#/active", Visibility::Active),
259− self.visibility_swap(bump, "#/completed", Visibility::Completed),
247+ self.visibility_swap(cx, "#/", Visibility::All),
248+ self.visibility_swap(cx, "#/active", Visibility::Active),
249+ self.visibility_swap(cx, "#/completed", Visibility::Completed),
260250 ])
261251 .finish(),
262− button(bump)
252+ button(&cx)
263253 .on("click", |root, vdom, _event| {
264254 C::delete_completed(root, vdom);
265255 })
@@ -271,22 +261,19 @@ impl<C: TodosActions> Todos<C> {
271261 .finish()
272262 }
273263
274− fn visibility_swap<'a, 'bump>(
275− &'a self,
276− bump: &'bump Bump,
264+ fn visibility_swap<'a>(
265+ &self,
266+ cx: &mut RenderContext<'a>,
277267 url: &'static str,
278268 target_vis: Visibility,
279− ) -> Node<'bump>
280− where
281− 'a: 'bump,
282− {
269+ ) -> Node<'a> {
283270 use dodrio::builder::*;
284271
285− li(bump)
272+ li(&cx)
286273 .on("click", move |root, vdom, _event| {
287274 C::change_visibility(root, vdom, target_vis);
288275 })
289− .children([a(bump)
276+ .children([a(&cx)
290277 .attr("href", url)
291278 .attr(
292279 "class",
@@ -302,15 +289,12 @@ impl<C: TodosActions> Todos<C> {
302289 }
303290}
304291
305−impl<C: TodosActions> Render for Todos<C> {
306− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
307− where
308− 'a: 'bump,
309− {
292+impl<'a, C: TodosActions> Render<'a> for Todos<C> {
293+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
310294 use dodrio::builder::*;
311295
312− div(bump)
313− .children([self.header(bump), self.todos_list(bump), self.footer(bump)])
296+ div(&cx)
297+ .children([self.header(cx), self.todos_list(cx), self.footer(cx)])
314298 .finish()
315299 }
316300}
js/change-list-interpreter.jsnew file+377
@@ -0,0 +1,377 @@
1+const decoder = new TextDecoder();
2+
3+function top(stack) {
4+ return stack[stack.length - 1];
5+}
6+
7+function string(mem, pointer, length) {
8+ const buf = mem.subarray(pointer, pointer + length);
9+ return decoder.decode(buf);
10+}
11+
12+const OP_TABLE = [
13+ // 0
14+ function setText(interpreter, mem8, mem32, i) {
15+ const pointer = mem32[i++];
16+ const length = mem32[i++];
17+ const str = string(mem8, pointer, length);
18+ top(interpreter.stack).textContent = str;
19+ return i;
20+ },
21+
22+ // 1
23+ function removeSelfAndNextSiblings(interpreter, mem8, mem32, i) {
24+ const node = interpreter.stack.pop();
25+ let sibling = node.nextSibling;
26+ while (sibling) {
27+ const temp = sibling.nextSibling;
28+ sibling.remove();
29+ sibling = temp;
30+ }
31+ node.remove();
32+ return i;
33+ },
34+
35+ // 2
36+ function replaceWith(interpreter, mem8, mem32, i) {
37+ const newNode = interpreter.stack.pop();
38+ const oldNode = interpreter.stack.pop();
39+ oldNode.replaceWith(newNode);
40+ interpreter.stack.push(newNode);
41+ return i;
42+ },
43+
44+ // 3
45+ function setAttribute(interpreter, mem8, mem32, i) {
46+ const nameId = mem32[i++];
47+ const valueId = mem32[i++];
48+ const name = interpreter.getCachedString(nameId);
49+ const value = interpreter.getCachedString(valueId);
50+ const node = top(interpreter.stack);
51+ node.setAttribute(name, value);
52+
53+ // Some attributes are "volatile" and don't work through `setAttribute`.
54+ if (name === "value") {
55+ node.value = value;
56+ }
57+ if (name === "checked") {
58+ node.checked = true;
59+ }
60+ if (name === "selected") {
61+ node.selected = true;
62+ }
63+
64+ return i;
65+ },
66+
67+ // 4
68+ function removeAttribute(interpreter, mem8, mem32, i) {
69+ const nameId = mem32[i++];
70+ const name = interpreter.getCachedString(nameId);
71+ const node = top(interpreter.stack);
72+ node.removeAttribute(name);
73+
74+ // Some attributes are "volatile" and don't work through `removeAttribute`.
75+ if (name === "value") {
76+ node.value = null;
77+ }
78+ if (name === "checked") {
79+ node.checked = false;
80+ }
81+ if (name === "selected") {
82+ node.selected = false;
83+ }
84+
85+ return i;
86+ },
87+
88+ // 5
89+ function pushReverseChild(interpreter, mem8, mem32, i) {
90+ const n = mem32[i++];
91+ const parent = top(interpreter.stack);
92+ const children = parent.childNodes;
93+ const child = children[children.length - n - 1];
94+ interpreter.stack.push(child);
95+ return i;
96+ },
97+
98+ // 6
99+ function popPushChild(interpreter, mem8, mem32, i) {
100+ const n = mem32[i++];
101+ interpreter.stack.pop();
102+ const parent = top(interpreter.stack);
103+ const children = parent.childNodes;
104+ const child = children[n];
105+ interpreter.stack.push(child);
106+ return i;
107+ },
108+
109+ // 7
110+ function pop(interpreter, mem8, mem32, i) {
111+ interpreter.stack.pop();
112+ return i;
113+ },
114+
115+ // 8
116+ function appendChild(interpreter, mem8, mem32, i) {
117+ const child = interpreter.stack.pop();
118+ top(interpreter.stack).appendChild(child);
119+ return i;
120+ },
121+
122+ // 9
123+ function createTextNode(interpreter, mem8, mem32, i) {
124+ const pointer = mem32[i++];
125+ const length = mem32[i++];
126+ const text = string(mem8, pointer, length);
127+ interpreter.stack.push(document.createTextNode(text));
128+ return i;
129+ },
130+
131+ // 10
132+ function createElement(interpreter, mem8, mem32, i) {
133+ const tagNameId = mem32[i++];
134+ const tagName = interpreter.getCachedString(tagNameId);
135+ interpreter.stack.push(document.createElement(tagName));
136+ return i;
137+ },
138+
139+ // 11
140+ function newEventListener(interpreter, mem8, mem32, i) {
141+ const eventId = mem32[i++];
142+ const eventType = interpreter.getCachedString(eventId);
143+ const a = mem32[i++];
144+ const b = mem32[i++];
145+ const el = top(interpreter.stack);
146+ el.addEventListener(eventType, interpreter.eventHandler);
147+ el[`dodrio-a-${eventType}`] = a;
148+ el[`dodrio-b-${eventType}`] = b;
149+ return i;
150+ },
151+
152+ // 12
153+ function updateEventListener(interpreter, mem8, mem32, i) {
154+ const eventId = mem32[i++];
155+ const eventType = interpreter.getCachedString(eventId);
156+ const el = top(interpreter.stack);
157+ el[`dodrio-a-${eventType}`] = mem32[i++];
158+ el[`dodrio-b-${eventType}`] = mem32[i++];
159+ return i;
160+ },
161+
162+ // 13
163+ function removeEventListener(interpreter, mem8, mem32, i) {
164+ const eventId = mem32[i++];
165+ const eventType = interpreter.getCachedString(eventId);
166+ const el = top(interpreter.stack);
167+ el.removeEventListener(eventType, interpreter.eventHandler);
168+ return i;
169+ },
170+
171+ // 14
172+ function addCachedString(interpreter, mem8, mem32, i) {
173+ const pointer = mem32[i++];
174+ const length = mem32[i++];
175+ const id = mem32[i++];
176+ const str = string(mem8, pointer, length);
177+ interpreter.addCachedString(str, id);
178+ return i;
179+ },
180+
181+ // 15
182+ function dropCachedString(interpreter, mem8, mem32, i) {
183+ const id = mem32[i++];
184+ interpreter.dropCachedString(id);
185+ return i;
186+ },
187+
188+ // 16
189+ function createElementNS(interpreter, mem8, mem32, i) {
190+ const tagNameId = mem32[i++];
191+ const tagName = interpreter.getCachedString(tagNameId);
192+ const nsId = mem32[i++];
193+ const ns = interpreter.getCachedString(nsId);
194+ interpreter.stack.push(document.createElementNS(ns, tagName));
195+ return i;
196+ },
197+
198+ // 17
199+ function saveChildrenToTemporaries(interpreter, mem8, mem32, i) {
200+ let temp = mem32[i++];
201+ const start = mem32[i++];
202+ const end = mem32[i++];
203+ const parent = top(interpreter.stack);
204+ const children = parent.childNodes;
205+ for (let i = start; i < end; i++) {
206+ interpreter.temporaries[temp++] = children[i];
207+ }
208+ return i;
209+ },
210+
211+ // 18
212+ function pushChild(interpreter, mem8, mem32, i) {
213+ const parent = top(interpreter.stack);
214+ const n = mem32[i++];
215+ const child = parent.childNodes[n];
216+ interpreter.stack.push(child);
217+ return i;
218+ },
219+
220+ // 19
221+ function pushTemporary(interpreter, mem8, mem32, i) {
222+ const temp = mem32[i++];
223+ interpreter.stack.push(interpreter.temporaries[temp]);
224+ return i;
225+ },
226+
227+ // 20
228+ function insertBefore(interpreter, mem8, mem32, i) {
229+ const before = interpreter.stack.pop();
230+ const after = interpreter.stack.pop();
231+ after.parentNode.insertBefore(before, after);
232+ interpreter.stack.push(before);
233+ return i;
234+ },
235+
236+ // 21
237+ function popPushReverseChild(interpreter, mem8, mem32, i) {
238+ const n = mem32[i++];
239+ interpreter.stack.pop();
240+ const parent = top(interpreter.stack);
241+ const children = parent.childNodes;
242+ const child = children[children.length - n - 1];
243+ interpreter.stack.push(child);
244+ return i;
245+ },
246+
247+ // 22
248+ function removeChild(interpreter, mem8, mem32, i) {
249+ const n = mem32[i++];
250+ const parent = top(interpreter.stack);
251+ const child = parent.childNodes[n];
252+ child.remove();
253+ return i;
254+ },
255+
256+ // 23
257+ function setClass(interpreter, mem8, mem32, i) {
258+ const classId = mem32[i++];
259+ const className = interpreter.getCachedString(classId);
260+ top(interpreter.stack).className = className;
261+ return i;
262+ },
263+
264+ // 24
265+ function saveTemplate(interpreter, mem8, mem32, i) {
266+ const id = mem32[i++];
267+ const template = top(interpreter.stack);
268+ interpreter.saveTemplate(id, template.cloneNode(true));
269+ return i;
270+ },
271+
272+ // 25
273+ function pushTemplate(interpreter, mem8, mem32, i) {
274+ const id = mem32[i++];
275+ const template = interpreter.getTemplate(id);
276+ interpreter.stack.push(template.cloneNode(true));
277+ return i;
278+ }
279+];
280+
281+export class ChangeListInterpreter {
282+ constructor(container) {
283+ this.trampoline = null;
284+ this.container = container;
285+ this.ranges = [];
286+ this.stack = [];
287+ this.strings = new Map();
288+ this.temporaries = [];
289+ this.templates = new Map();
290+ }
291+
292+ unmount() {
293+ this.trampoline.mounted = false;
294+
295+ // Null out all of our properties just to ensure that if we mistakenly ever
296+ // call a method on this instance again, it will throw.
297+ this.trampoline = null;
298+ this.container = null;
299+ this.ranges = null;
300+ this.stack = null;
301+ this.strings = null;
302+ this.temporaries = null;
303+ this.templates = null;
304+ }
305+
306+ addChangeListRange(start, len) {
307+ this.ranges.push(start);
308+ this.ranges.push(len);
309+ }
310+
311+ applyChanges(memory) {
312+ if (this.ranges.length == 0) {
313+ return;
314+ }
315+
316+ this.stack.push(this.container.firstChild);
317+ const mem8 = new Uint8Array(memory.buffer);
318+ const mem32 = new Uint32Array(memory.buffer);
319+
320+ for (let i = 0; i < this.ranges.length; i += 2) {
321+ const start = this.ranges[i];
322+ const len = this.ranges[i + 1];
323+ this.applyChangeRange(mem8, mem32, start, len);
324+ }
325+
326+ this.ranges.length = 0;
327+ this.stack.length = 0;
328+ this.temporaries.length = 0;
329+ }
330+
331+ applyChangeRange(mem8, mem32, start, len) {
332+ const end = (start + len) / 4;
333+ for (let i = start / 4; i < end; ) {
334+ const op = mem32[i++];
335+ i = OP_TABLE[op](this, mem8, mem32, i);
336+ }
337+ }
338+
339+ addCachedString(str, id) {
340+ this.strings.set(id, str);
341+ }
342+
343+ dropCachedString(id) {
344+ this.strings.delete(id);
345+ }
346+
347+ getCachedString(id) {
348+ return this.strings.get(id);
349+ }
350+
351+ saveTemplate(id, template) {
352+ this.templates.set(id, template);
353+ }
354+
355+ getTemplate(id) {
356+ return this.templates.get(id);
357+ }
358+
359+ initEventsTrampoline(trampoline) {
360+ this.trampoline = trampoline;
361+ trampoline.mounted = true;
362+ this.eventHandler = function(event) {
363+ if (!trampoline.mounted) {
364+ throw new Error("invocation of listener after VDOM has been unmounted");
365+ }
366+
367+ // `this` always refers to the element the handler was added to.
368+ // Since we're adding the handler to all elements our content wants
369+ // to listen for events on, this ensures that we always get the right
370+ // values for `a` and `b`.
371+ const type = event.type;
372+ const a = this[`dodrio-a-${type}`];
373+ const b = this[`dodrio-b-${type}`];
374+ trampoline(event, a, b);
375+ }
376+ }
377+}
js/change-list.jsdeleted−268
@@ -1,268 +0,0 @@
1−const decoder = new TextDecoder();
2−
3−function top(stack) {
4− return stack[stack.length - 1];
5−}
6−
7−function string(mem, pointer, length) {
8− const buf = mem.subarray(pointer, pointer + length);
9− return decoder.decode(buf);
10−}
11−
12−const OP_TABLE = [
13− // 0
14− function setText(changeList, mem8, mem32, i) {
15− const pointer = mem32[i++];
16− const length = mem32[i++];
17− const str = string(mem8, pointer, length);
18− top(changeList.stack).textContent = str;
19− return i;
20− },
21−
22− // 1
23− function removeSelfAndNextSiblings(changeList, mem8, mem32, i) {
24− const node = changeList.stack.pop();
25− let sibling = node.nextSibling;
26− while (sibling) {
27− const temp = sibling.nextSibling;
28− sibling.remove();
29− sibling = temp;
30− }
31− node.remove();
32− return i;
33− },
34−
35− // 2
36− function replaceWith(changeList, mem8, mem32, i) {
37− const newNode = changeList.stack.pop();
38− const oldNode = changeList.stack.pop();
39− oldNode.replaceWith(newNode);
40− changeList.stack.push(newNode);
41− return i;
42− },
43−
44− // 3
45− function setAttribute(changeList, mem8, mem32, i) {
46− const nameId = mem32[i++];
47− const valueId = mem32[i++];
48− const name = changeList.getString(nameId);
49− const value = changeList.getString(valueId);
50− const node = top(changeList.stack);
51− node.setAttribute(name, value);
52−
53− // Some attributes are "volatile" and don't work through `setAttribute`.
54− if (name === "value") {
55− node.value = value;
56− }
57− if (name === "checked") {
58− node.checked = true;
59− }
60− if (name === "selected") {
61− node.selected = true;
62− }
63−
64− return i;
65− },
66−
67− // 4
68− function removeAttribute(changeList, mem8, mem32, i) {
69− const nameId = mem32[i++];
70− const name = changeList.getString(nameId);
71− const node = top(changeList.stack);
72− node.removeAttribute(name);
73−
74− // Some attributes are "volatile" and don't work through `removeAttribute`.
75− if (name === "value") {
76− node.value = null;
77− }
78− if (name === "checked") {
79− node.checked = false;
80− }
81− if (name === "selected") {
82− node.selected = false;
83− }
84−
85− return i;
86− },
87−
88− // 5
89− function pushFirstChild(changeList, mem8, mem32, i) {
90− changeList.stack.push(top(changeList.stack).firstChild);
91− return i;
92− },
93−
94− // 6
95− function popPushNextSibling(changeList, mem8, mem32, i) {
96− const node = changeList.stack.pop();
97− changeList.stack.push(node.nextSibling);
98− return i;
99− },
100−
101− // 7
102− function pop(changeList, mem8, mem32, i) {
103− changeList.stack.pop();
104− return i;
105− },
106−
107− // 8
108− function appendChild(changeList, mem8, mem32, i) {
109− const child = changeList.stack.pop();
110− top(changeList.stack).appendChild(child);
111− return i;
112− },
113−
114− // 9
115− function createTextNode(changeList, mem8, mem32, i) {
116− const pointer = mem32[i++];
117− const length = mem32[i++];
118− const text = string(mem8, pointer, length);
119− changeList.stack.push(document.createTextNode(text));
120− return i;
121− },
122−
123− // 10
124− function createElement(changeList, mem8, mem32, i) {
125− const tagNameId = mem32[i++];
126− const tagName = changeList.getString(tagNameId);
127− changeList.stack.push(document.createElement(tagName));
128− return i;
129− },
130−
131− // 11
132− function newEventListener(changeList, mem8, mem32, i) {
133− const eventId = mem32[i++];
134− const eventType = changeList.getString(eventId);
135− const a = mem32[i++];
136− const b = mem32[i++];
137− const el = top(changeList.stack);
138− el.addEventListener(eventType, changeList.eventHandler);
139− el[`dodrio-a-${eventType}`] = a;
140− el[`dodrio-b-${eventType}`] = b;
141− return i;
142− },
143−
144− // 12
145− function updateEventListener(changeList, mem8, mem32, i) {
146− const eventId = mem32[i++];
147− const eventType = changeList.getString(eventId);
148− const el = top(changeList.stack);
149− el[`dodrio-a-${eventType}`] = mem32[i++];
150− el[`dodrio-b-${eventType}`] = mem32[i++];
151− return i;
152− },
153−
154− // 13
155− function removeEventListener(changeList, mem8, mem32, i) {
156− const eventId = mem32[i++];
157− const eventType = changeList.getString(eventId);
158− const el = top(changeList.stack);
159− el.removeEventListener(eventType, changeList.eventHandler);
160− return i;
161− },
162−
163− // 14
164− function addString(changeList, mem8, mem32, i) {
165− const pointer = mem32[i++];
166− const length = mem32[i++];
167− const id = mem32[i++];
168− const str = string(mem8, pointer, length);
169− changeList.addString(str, id);
170− return i;
171− },
172−
173− // 15
174− function dropString(changeList, mem8, mem32, i) {
175− const id = mem32[i++];
176− changeList.dropString(id);
177− return i;
178− }
179−];
180−
181−// export
182−class ChangeList {
183− constructor(container) {
184− this.trampoline = null;
185− this.container = container;
186− this.ranges = [];
187− this.stack = [];
188− this.strings = new Map();
189− }
190−
191− unmount() {
192− this.trampoline.mounted = false;
193−
194− // Null out all of our properties just to ensure that if we mistakenly ever
195− // call a method on this instance again, it will throw.
196− this.trampoline = null;
197− this.container = null;
198− this.ranges = null;
199− this.stack = null;
200− this.strings = null;
201− }
202−
203− addChangeListRange(start, len) {
204− this.ranges.push(start);
205− this.ranges.push(len);
206− }
207−
208− applyChanges(memory) {
209− if (this.ranges.length == 0) {
210− return;
211− }
212−
213− this.stack.push(this.container.firstChild);
214− const mem8 = new Uint8Array(memory.buffer);
215− const mem32 = new Uint32Array(memory.buffer);
216−
217− for (let i = 0; i < this.ranges.length; i += 2) {
218− const start = this.ranges[i];
219− const len = this.ranges[i + 1];
220− this.applyChangeRange(mem8, mem32, start, len);
221− }
222−
223− this.ranges.length = 0;
224− this.stack.length = 0;
225− }
226−
227− applyChangeRange(mem8, mem32, start, len) {
228− const end = (start + len) / 4;
229− for (let i = start / 4; i < end; ) {
230− const op = mem32[i++];
231− i = OP_TABLE[op](this, mem8, mem32, i);
232− }
233− }
234−
235− addString(str, id) {
236− this.strings.set(id, str);
237− }
238−
239− dropString(id) {
240− this.strings.delete(id);
241− }
242−
243− getString(id) {
244− return this.strings.get(id);
245− }
246−
247− initEventsTrampoline(trampoline) {
248− this.trampoline = trampoline;
249− trampoline.mounted = true;
250− this.eventHandler = function(event) {
251− if (!trampoline.mounted) {
252− throw new Error("invocation of listener after VDOM has been unmounted");
253− }
254−
255− // `this` always refers to the element the handler was added to.
256− // Since we're adding the handler to all elements our content wants
257− // to listen for events on, this ensures that we always get the right
258− // values for `a` and `b`.
259− const type = event.type;
260− const a = this[`dodrio-a-${type}`];
261− const b = this[`dodrio-b-${type}`];
262− trampoline(event, a, b);
263− }
264− }
265−}
266−window.ChangeList = ChangeList;
267−
268−//# sourceURL=dodrio/change-list.js
js/package-lock.jsondeleted−5
@@ -1,5 +0,0 @@
1−{
2− "name": "dodrio",
3− "version": "0.1.0",
4− "lockfileVersion": 1
5−}
js/package.jsondeleted−11
@@ -1,11 +0,0 @@
1−{
2− "name": "dodrio",
3− "version": "0.1.0",
4− "description": "Dodrio",
5− "main": "dodrio.js",
6− "scripts": {
7− "test": "echo \"Error: no test specified\" && exit 1"
8− },
9− "author": "Nick Fitzgerald <fitzgen@gmail.com>",
10− "license": "MPL-2.0"
11−}
js/strace.jsnew file+80
@@ -0,0 +1,80 @@
1+export function initStrace() {
2+ instrumentAll(Element.prototype);
3+ instrumentAll(Document.prototype);
4+ instrumentAll(Node.prototype);
5+ instrumentAll(EventTarget.prototype);
6+}
7+
8+const counts = new Map;
9+counts.total = 0;
10+
11+let timer = null;
12+
13+function log(ctorName, kind, key) {
14+ counts.total += 1;
15+
16+ const entry = `${kind} ${ctorName}#${key}`
17+ const count = counts.get(entry);
18+ if (count === undefined) {
19+ counts.set(entry, 1);
20+ } else {
21+ counts.set(entry, count + 1);
22+ }
23+
24+ if (timer !== null) {
25+ clearTimeout(timer);
26+ }
27+ timer = setTimeout(dumpAndReset, 100);
28+}
29+
30+function dumpAndReset() {
31+ const data = [...counts]
32+ .sort((a, b) => b[1] - a[1])
33+ .map(a => ({ "DOM Method": a[0], "Count": a[1] }));
34+
35+ data.push({ "DOM Method": "<total>", "Count": counts.total });
36+
37+ console.table(data, ["DOM Method", "Count"]);
38+
39+ counts.clear();
40+ counts.total = 0;
41+}
42+
43+function instrument(proto, key, desc) {
44+ const ctorName = proto.constructor.name;
45+
46+ if (typeof desc.value == "function") {
47+ const f = desc.value;
48+ desc.value = function (...args) {
49+ log(ctorName, "call", key);
50+ return f.apply(this, args);
51+ };
52+ }
53+
54+ if (typeof desc.get == "function") {
55+ const get = desc.get;
56+ desc.get = function (...args) {
57+ log(ctorName, "get", key);
58+ return get.apply(this, args);
59+ };
60+ }
61+
62+ if (typeof desc.set == "function") {
63+ const set = desc.set;
64+ desc.set = function (...args) {
65+ log(ctorName, "set", key);
66+ return set.apply(this, args);
67+ };
68+ }
69+
70+ Object.defineProperty(proto, key, desc);
71+}
72+
73+function instrumentAll(proto) {
74+ for (const key of Object.keys(proto)) {
75+ const desc = Object.getOwnPropertyDescriptor(proto, key);
76+ if (desc.configurable) {
77+ instrument(proto, key, desc);
78+ }
79+ }
80+}
publish.shnew file+11
@@ -0,0 +1,11 @@
1+#!/usr/bin/env bash
2+
3+set -ex
4+
5+cd "$(dirname "$0")"
6+
7+cd crates/js-api
8+cargo publish --no-verify $PUBLISH_FLAGS
9+cd ../..
10+
11+cargo publish $PUBLISH_FLAGS
src/builder.rs+161−6
@@ -1,6 +1,6 @@
11//! Helpers for building virtual DOM nodes.
22
3−use crate::{Attribute, Listener, Node, RootRender, VdomWeak};
3+use crate::{node::NodeKey, Attribute, Listener, Node, RootRender, VdomWeak};
44use bumpalo::Bump;
55
66/// A virtual DOM element builder.
@@ -16,10 +16,12 @@ where
1616 Children: 'a + AsRef<[Node<'a>]>,
1717{
1818 bump: &'a Bump,
19+ key: NodeKey,
1920 tag_name: &'a str,
2021 listeners: Listeners,
2122 attributes: Attributes,
2223 children: Children,
24+ namespace: Option<&'a str>,
2325}
2426
2527impl<'a>
@@ -53,13 +55,19 @@ impl<'a>
5355 /// let my_element_builder = ElementBuilder::new(&b, tag_name);
5456 /// # fn flip_coin() -> bool { true }
5557 /// ```
56− pub fn new(bump: &'a Bump, tag_name: &'a str) -> Self {
58+ pub fn new<B>(bump: B, tag_name: &'a str) -> Self
59+ where
60+ B: Into<&'a Bump>,
61+ {
62+ let bump = bump.into();
5763 ElementBuilder {
5864 bump,
65+ key: NodeKey::NONE,
5966 tag_name,
6067 listeners: bumpalo::collections::Vec::new_in(bump),
6168 attributes: bumpalo::collections::Vec::new_in(bump),
6269 children: bumpalo::collections::Vec::new_in(bump),
70+ namespace: None,
6371 }
6472 }
6573}
@@ -104,10 +112,12 @@ where
104112 {
105113 ElementBuilder {
106114 bump: self.bump,
115+ key: self.key,
107116 tag_name: self.tag_name,
108117 listeners,
109118 attributes: self.attributes,
110119 children: self.children,
120+ namespace: self.namespace,
111121 }
112122 }
113123
@@ -141,10 +151,12 @@ where
141151 {
142152 ElementBuilder {
143153 bump: self.bump,
154+ key: self.key,
144155 tag_name: self.tag_name,
145156 listeners: self.listeners,
146157 attributes,
147158 children: self.children,
159+ namespace: self.namespace,
148160 }
149161 }
150162
@@ -178,13 +190,81 @@ where
178190 {
179191 ElementBuilder {
180192 bump: self.bump,
193+ key: self.key,
181194 tag_name: self.tag_name,
182195 listeners: self.listeners,
183196 attributes: self.attributes,
184197 children,
198+ namespace: self.namespace,
199+ }
200+ }
201+
202+ /// Set the namespace for this element.
203+ ///
204+ /// # Example
205+ ///
206+ /// ```no_run
207+ /// use dodrio::{builder::*, bumpalo::Bump};
208+ ///
209+ /// let b = Bump::new();
210+ ///
211+ /// // Create a `<td>` tag with an xhtml namespace
212+ /// let my_td = td(&b)
213+ /// .namespace(Some("http://www.w3.org/1999/xhtml"))
214+ /// .finish();
215+ /// ```
216+ #[inline]
217+ pub fn namespace(self, namespace: Option<&'a str>) -> Self {
218+ ElementBuilder {
219+ bump: self.bump,
220+ key: self.key,
221+ tag_name: self.tag_name,
222+ listeners: self.listeners,
223+ attributes: self.attributes,
224+ children: self.children,
225+ namespace,
185226 }
186227 }
187228
229+ /// Set this element's key.
230+ ///
231+ /// When diffing sets of siblings, if an old sibling and new sibling share a
232+ /// key, then they will always reuse the same physical DOM node. This is
233+ /// important when using CSS animations, web components, third party JS, or
234+ /// anything else that makes the diffing implementation observable.
235+ ///
236+ /// Do not use keys if such a scenario does not apply. Keyed diffing is
237+ /// generally more expensive than not, since it is putting greater
238+ /// constraints on the diffing algorithm.
239+ ///
240+ /// # Invariants You Must Uphold
241+ ///
242+ /// The key may not be `u32::MAX`, which is a reserved key value.
243+ ///
244+ /// Keys must be unique among siblings.
245+ ///
246+ /// All sibling nodes must be keyed, or they must all not be keyed. You may
247+ /// not mix keyed and unkeyed siblings.
248+ ///
249+ /// # Example
250+ ///
251+ /// ```no_run
252+ /// use dodrio::{builder::*, bumpalo::Bump};
253+ ///
254+ /// let b = Bump::new();
255+ ///
256+ /// let my_li = li(&b)
257+ /// .key(1337)
258+ /// .finish();
259+ /// ```
260+ #[inline]
261+ pub fn key(mut self, key: u32) -> Self {
262+ use std::u32;
263+ debug_assert!(key != u32::MAX);
264+ self.key = NodeKey(key);
265+ self
266+ }
267+
188268 /// Create the virtual DOM node described by this builder.
189269 ///
190270 /// # Example
@@ -202,12 +282,23 @@ where
202282 /// ```
203283 #[inline]
204284 pub fn finish(self) -> Node<'a> {
285+ let children: &'a Children = self.bump.alloc(self.children);
286+ let children: &'a [Node<'a>] = children.as_ref();
287+
288+ let listeners: &'a Listeners = self.bump.alloc(self.listeners);
289+ let listeners: &'a [Listener<'a>] = listeners.as_ref();
290+
291+ let attributes: &'a Attributes = self.bump.alloc(self.attributes);
292+ let attributes: &'a [Attribute<'a>] = attributes.as_ref();
293+
205294 Node::element(
206295 self.bump,
296+ self.key,
207297 self.tag_name,
208− self.listeners,
209− self.attributes,
210− self.children,
298+ listeners,
299+ attributes,
300+ children,
301+ self.namespace,
211302 )
212303 }
213304}
@@ -342,6 +433,28 @@ macro_rules! builder_constructors {
342433 ( $(
343434 $(#[$attr:meta])*
344435 $name:ident;
436+ )* ) => {
437+ $(
438+ $(#[$attr])*
439+ #[inline]
440+ pub fn $name<'a, B>(
441+ bump: B,
442+ ) -> ElementBuilder<
443+ 'a,
444+ bumpalo::collections::Vec<'a, Listener<'a>>,
445+ bumpalo::collections::Vec<'a, Attribute<'a>>,
446+ bumpalo::collections::Vec<'a, Node<'a>>,
447+ >
448+ where
449+ B: Into<&'a Bump>
450+ {
451+ ElementBuilder::new(bump, stringify!($name))
452+ }
453+ )*
454+ };
455+ ( $(
456+ $(#[$attr:meta])*
457+ $name:ident <> $namespace:tt;
345458 )* ) => {
346459 $(
347460 $(#[$attr])*
@@ -354,7 +467,8 @@ macro_rules! builder_constructors {
354467 bumpalo::collections::Vec<'a, Attribute<'a>>,
355468 bumpalo::collections::Vec<'a, Node<'a>>,
356469 > {
357− ElementBuilder::new(bump, stringify!($name))
470+ let builder = ElementBuilder::new(bump, stringify!($name));
471+ builder.namespace(Some($namespace))
358472 }
359473 )*
360474 }
@@ -861,6 +975,47 @@ builder_constructors! {
861975 template;
862976}
863977
978+builder_constructors! {
979+ // SVG components
980+
981+ /// Build a
982+ /// [`<svg>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg)
983+ /// element.
984+ svg <> "http://www.w3.org/2000/svg" ;
985+ /// Build a
986+ /// [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
987+ /// element.
988+ path <> "http://www.w3.org/2000/svg";
989+ /// Build a
990+ /// [`<circle>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)
991+ /// element.
992+ circle <> "http://www.w3.org/2000/svg";
993+ /// Build a
994+ /// [`<ellipse>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/ellipse)
995+ /// element.
996+ ellipse <> "http://www.w3.org/2000/svg";
997+ /// Build a
998+ /// [`<line>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
999+ /// element.
1000+ line <> "http://www.w3.org/2000/svg";
1001+ /// Build a
1002+ /// [`<polygon>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon)
1003+ /// element.
1004+ polygon <> "http://www.w3.org/2000/svg";
1005+ /// Build a
1006+ /// [`<polyline>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline)
1007+ /// element.
1008+ polyline <> "http://www.w3.org/2000/svg";
1009+ /// Build a
1010+ /// [`<rect>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)
1011+ /// element.
1012+ rect <> "http://www.w3.org/2000/svg";
1013+ /// Build a
1014+ /// [`<image>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image)
1015+ /// element.
1016+ image <> "http://www.w3.org/2000/svg";
1017+}
1018+
8641019/// Construct a text node.
8651020///
8661021/// This is `dodrio`'s virtual DOM equivalent of `document.createTextNode`.
src/cached.rs+139−73
@@ -1,46 +1,52 @@
1−use crate::{Node, Render};
2−use bumpalo::Bump;
3−use std::cell::RefCell;
4−use std::mem;
1+use crate::{
2+ cached_set::CachedSet,
3+ node::{CachedNode, NodeKey},
4+ Node, Render, RenderContext,
5+};
6+use std::any::TypeId;
7+use std::cell::Cell;
58use std::ops::{Deref, DerefMut};
69
7−// TODO: This implementation will only cache the rendering (generation of the
8−// virtual DOM) but not the diffing of the cached subtree. We could skip diffing
9−// for cached tree by adding `fn is_cached(&self) -> bool` to `Node` that we can
10−// check during diffing. This comes at the cost of bloating `Node`'s size (since
11−// we don't have any padding to sneak an extra `bool` field into). We should
12−// investigate whether it is worth adding this or not.
13−
1410/// A renderable that supports caching for when rendering is expensive but can
1511/// generate the same DOM tree.
16−#[derive(Debug)]
17−pub struct Cached<R> {
12+///
13+/// # Example
14+///
15+/// The [TodoMVC
16+/// example](https://github.com/fitzgen/dodrio/blob/master/examples/todomvc/src/todo.rs)
17+/// caches individual todo items.
18+#[derive(Clone, Debug, Default)]
19+pub struct Cached<R>
20+where
21+ R: Default,
22+{
1823 inner: R,
19− bump: bumpalo::Bump,
20− // Actually a self-reference into `self.bump`. Safe because we ensure that
21− // whenever we hand out a cached node, we use a lifetime that cannot outlive
22− // its owning `Cached<R>`.
23− cached: RefCell<Option<Node<'static>>>,
24+ cached: Cell<Option<CachedNode>>,
25+}
26+
27+pub_unstable_internal! {
28+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
29+ pub(crate) struct TemplateId(TypeId);
2430}
2531
26−impl<R> Cached<R> {
32+impl<R> Cached<R>
33+where
34+ R: Default,
35+{
2736 /// Construct a new `Cached<R>` of an inner `R`.
2837 ///
2938 /// # Example
3039 ///
3140 /// ```
32− /// use bumpalo::Bump;
33− /// use dodrio::{Cached, Node, Render};
41+ /// use dodrio::{Cached, Node, Render, RenderContext};
3442 ///
43+ /// #[derive(Default)]
3544 /// pub struct Counter {
3645 /// count: u32,
3746 /// }
3847 ///
39− /// impl Render for Counter {
40− /// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
41− /// where
42− /// 'a: 'bump
43− /// {
48+ /// impl<'a> Render<'a> for Counter {
49+ /// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
4450 /// // ...
4551 /// # unimplemented!()
4652 /// }
@@ -54,13 +60,8 @@ impl<R> Cached<R> {
5460 /// ```
5561 #[inline]
5662 pub fn new(inner: R) -> Cached<R> {
57− let bump = Bump::new();
58− let cached = RefCell::new(None);
59− Cached {
60− inner,
61− bump,
62− cached,
63− }
63+ let cached = Cell::new(None);
64+ Cached { inner, cached }
6465 }
6566
6667 /// Invalidate the cached rendering.
@@ -72,26 +73,24 @@ impl<R> Cached<R> {
7273 /// # Example
7374 ///
7475 /// The `Cached<Hello>` component must have its cache invalidated whenever
75− /// the `who` string is changed, or else the cached rendering whill keep
76+ /// the `who` string is changed, or else the cached rendering will keep
7677 /// displaying greetings to old `who`s.
7778 ///
7879 /// ```
79− /// use bumpalo::Bump;
80− /// use dodrio::{Cached, Node, Render};
80+ /// use dodrio::{bumpalo, Cached, Node, Render, RenderContext};
8181 ///
8282 /// /// A component that renders to "<p>Hello, {who}!</p>"
83+ /// #[derive(Default)]
8384 /// pub struct Hello {
8485 /// who: String
8586 /// }
8687 ///
87− /// impl Render for Hello {
88− /// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
89− /// where
90− /// 'a: 'bump,
91− /// {
88+ /// impl<'a> Render<'a> for Hello {
89+ /// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
9290 /// use dodrio::builder::*;
93− /// p(bump)
94− /// .children([text("Hello, "), text(&self.who), text("!")])
91+ /// let greeting = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
92+ /// p(&cx)
93+ /// .children([text(greeting.into_bump_str())])
9594 /// .finish()
9695 /// }
9796 /// }
@@ -104,18 +103,30 @@ impl<R> Cached<R> {
104103 /// }
105104 /// ```
106105 #[inline]
107− pub fn invalidate(cached: &mut Self) {
108− *cached.cached.borrow_mut() = None;
106+ pub fn invalidate(cached: &Self) {
107+ cached.cached.set(None);
109108 }
110109
111110 /// Convert a `Cached<R>` back into a plain `R`.
112111 #[inline]
113− pub fn into_inner(cached: Cached<R>) -> R {
112+ pub fn into_inner(cached: Self) -> R {
114113 cached.inner
115114 }
116115}
117116
118−impl<R> Deref for Cached<R> {
117+impl<R> Cached<R>
118+where
119+ R: 'static + Default,
120+{
121+ pub(crate) fn template_id() -> TemplateId {
122+ TemplateId(TypeId::of::<R>())
123+ }
124+}
125+
126+impl<R> Deref for Cached<R>
127+where
128+ R: Default,
129+{
119130 type Target = R;
120131
121132 fn deref(&self) -> &R {
@@ -123,41 +134,96 @@ impl<R> Deref for Cached<R> {
123134 }
124135}
125136
126−impl<R> DerefMut for Cached<R> {
137+impl<R> DerefMut for Cached<R>
138+where
139+ R: Default,
140+{
127141 fn deref_mut(&mut self) -> &mut R {
128142 &mut self.inner
129143 }
130144}
131145
132−unsafe fn extend_node_lifetime<'a>(node: Node<'a>) -> Node<'static> {
133− mem::transmute(node)
146+impl<'a, R> Render<'a> for Cached<R>
147+where
148+ R: 'static + Default + for<'b> Render<'b>,
149+{
150+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
151+ let template = cx.template::<R>();
152+ let cached = match self.cached.get() {
153+ // This does-the-cache-contain-this-id check is necessary because
154+ // the same `Cached<R>` instance can be rendered into vdom A, which
155+ // will save the results into A's cached set and yield id X. Then,
156+ // it can be rendered *again* into a second vdom B, and B's cached
157+ // set does not have the saved results for id X. If we didn't have
158+ // this check, instead of a cache miss, we would have a panic.
159+ //
160+ // This scenario should basically never happen in the real world,
161+ // however. If we ever find that it is worth sharing a cached render
162+ // component between multiple vdoms, and want to avoid these
163+ // "unnecessary" cache misses, we can do this:
164+ //
165+ // * Make each `Cached<R>` have an instance id and generation
166+ // counter.
167+ //
168+ // * On invalidation, bump the generation counter.
169+ //
170+ // * Add a generation member to the `CacheEntry`.
171+ //
172+ // * Each vdom maintains a map from `Cached<R>` instance id to
173+ // `CacheEntry`s.
174+ //
175+ // * We only re-use the cached results if the `Cached<R>`'s
176+ // generation counter matches the entry in the vdom's cached set.
177+ //
178+ // This is all do-able but is a bit more book keeping than we really
179+ // want to do unless it is well motivated.
180+ Some(cached)
181+ if {
182+ let cached_set = cx.cached_set.borrow();
183+ cached_set.contains(cached.id)
184+ } =>
185+ {
186+ cached
187+ }
188+ _ => {
189+ let mut key = NodeKey::NONE;
190+ let id = CachedSet::insert(cx, false, template, |nested_cx| {
191+ let node = self.inner.render(nested_cx);
192+ key = node.key();
193+ node
194+ });
195+ let cached = CachedNode { id, key };
196+ self.cached.set(Some(cached));
197+ cached
198+ }
199+ };
200+
201+ cached.into()
202+ }
134203}
135204
136−impl<R: Render> Render for Cached<R> {
137− fn render<'a, 'bump>(&'a self, _: &'bump Bump) -> Node<'bump>
205+#[cfg(feature = "serde")]
206+impl<R> serde::Serialize for Cached<R>
207+where
208+ R: Default + serde::Serialize,
209+{
210+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138211 where
139− 'a: 'bump,
212+ S: serde::Serializer,
140213 {
141− let mut cached = self.cached.borrow_mut();
142−
143− if let Some(cached) = cached.as_ref() {
144− // The cached node is actually a self-reference, so it has the `'a`
145− // lifetime.
146− let cached: Node<'a> = cached.clone();
147−
148− // But the `'a` lifetime outlives `'bump`, so we can safely convert
149− // it to the narrower `'bump` lifetime.
150− let cached: Node<'bump> = cached;
151−
152− // Return the cached rendering!
153− return cached;
154− }
214+ self.inner.serialize(serializer)
215+ }
216+}
155217
156− // We don't have a cached node. Render into our own `Bump`, cache it for
157− // future renders, and return it. Same lifetimes as above.
158− let node: Node<'a> = self.inner.render(&self.bump);
159− *cached = Some(unsafe { extend_node_lifetime(node.clone()) });
160− let node: Node<'bump> = node;
161− node
218+#[cfg(feature = "serde")]
219+impl<'de, R> serde::Deserialize<'de> for Cached<R>
220+where
221+ R: Default + serde::Deserialize<'de>,
222+{
223+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224+ where
225+ D: serde::Deserializer<'de>,
226+ {
227+ R::deserialize(deserializer).map(Cached::new)
162228 }
163229}
src/cached_set.rsnew file+186
@@ -0,0 +1,186 @@
1+use crate::{
2+ events::EventsRegistry,
3+ node::{Node, NodeKind},
4+ render_context::RenderContext,
5+};
6+use bumpalo::Bump;
7+use fxhash::{FxHashMap, FxHashSet};
8+use std::sync::atomic::{AtomicUsize, Ordering};
9+use std::u32;
10+use wasm_bindgen::prelude::*;
11+
12+static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
13+
14+pub_unstable_internal! {
15+ #[derive(Debug, Default)]
16+ pub(crate) struct CachedSet {
17+ items: FxHashMap<CacheId, CacheEntry>,
18+ }
19+}
20+
21+pub_unstable_internal! {
22+ #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
23+ pub(crate) struct CacheId(u32);
24+}
25+
26+#[derive(Debug)]
27+pub(crate) struct CacheEntry {
28+ bump: Bump,
29+
30+ // Actually a reference into `bump`.
31+ node: *const Node<'static>,
32+
33+ // All the other cached entries that `node` references (including transitive
34+ // edges). By eagerly following transitive edges and recording them the
35+ // once, we don't have to repeat the tracing every time we want to garbage
36+ // collect cache entries.
37+ edges: FxHashSet<CacheId>,
38+
39+ // The template for this cached virtual subtree.
40+ //
41+ // When a cache entry has a template, that means that the `ChangeList` will
42+ // lazily build a physical DOM subtree based on the template, and when we
43+ // create new versions of this cached result, we clone the template's
44+ // physical DOM subtree and then modify it, rather than building the cached
45+ // result up from scratch. This is a nice win because we bounce between JS
46+ // and DOM methods less often, and get the C++ DOM implementation to do most
47+ // of the subtree construction for us.
48+ template: Option<CacheId>,
49+
50+ // Whether this entry should never be garbage collected. Typically only
51+ // templates are pinned.
52+ pinned: bool,
53+}
54+
55+impl From<CacheId> for u32 {
56+ #[inline]
57+ fn from(id: CacheId) -> u32 {
58+ id.0
59+ }
60+}
61+
62+impl CachedSet {
63+ pub(crate) fn new_roots_set(&self) -> FxHashSet<CacheId> {
64+ let mut roots = FxHashSet::default();
65+ roots.reserve(self.items.len());
66+ roots
67+ }
68+
69+ pub(crate) fn gc(&mut self, registry: &mut EventsRegistry, roots: FxHashSet<CacheId>) {
70+ let mut marked = FxHashSet::default();
71+ marked.reserve(self.items.len());
72+
73+ for root in roots {
74+ if marked.insert(root) {
75+ let entry = self
76+ .items
77+ .get(&root)
78+ .expect_throw("CachedSet::gc: should have root in cached set");
79+
80+ // NB: no need to recurse here because `edges` already contains
81+ // transitive edges!
82+ marked.extend(entry.edges.iter().cloned());
83+ }
84+ }
85+
86+ self.items.retain(|id, entry| {
87+ let keep = entry.pinned || marked.contains(id);
88+ if !keep {
89+ let node: &Node = unsafe { &*entry.node };
90+ registry.remove_subtree(node);
91+ }
92+ keep
93+ });
94+ }
95+
96+ // Trace all the transitive edges to other cached entries that the given
97+ // node has.
98+ fn trace(&self, node: &Node) -> FxHashSet<CacheId> {
99+ let mut edges = FxHashSet::default();
100+ self.trace_recursive(&mut edges, node);
101+ edges
102+ }
103+
104+ fn trace_recursive(&self, edges: &mut FxHashSet<CacheId>, node: &Node) {
105+ match &node.kind {
106+ NodeKind::Text(_) => return,
107+ NodeKind::Cached(c) => {
108+ debug_assert!(self.items.contains_key(&c.id));
109+ edges.insert(c.id);
110+ edges.extend(
111+ self.items
112+ .get(&c.id)
113+ .expect_throw("CachedSet::trace: should have c.id in cached set")
114+ .edges
115+ .iter()
116+ .cloned(),
117+ );
118+ }
119+ NodeKind::Element(el) => {
120+ for child in el.children {
121+ self.trace_recursive(edges, child);
122+ }
123+ }
124+ }
125+ }
126+
127+ fn next_id(&mut self) -> CacheId {
128+ let next = ID_COUNTER.fetch_add(1, Ordering::AcqRel) as u32;
129+ let next = if next == u32::MAX { None } else { Some(next) };
130+ CacheId(next.expect_throw("ID_COUNTER overflowed"))
131+ }
132+
133+ pub(crate) fn insert<F>(
134+ cx: &mut RenderContext,
135+ pinned: bool,
136+ template: Option<CacheId>,
137+ f: F,
138+ ) -> CacheId
139+ where
140+ F: for<'a> FnOnce(&mut RenderContext<'a>) -> Node<'a>,
141+ {
142+ let set = cx.cached_set;
143+ let bump = Bump::new();
144+ let (node, edges) = {
145+ let mut nested_cx = RenderContext::new(&bump, cx.cached_set, cx.templates);
146+ let node = f(&mut nested_cx);
147+ let node = bump.alloc(node);
148+ let edges = {
149+ let set = set.borrow();
150+ set.trace(node)
151+ };
152+ (
153+ node as *mut Node<'_> as usize as *const Node<'static>,
154+ edges,
155+ )
156+ };
157+
158+ let entry = CacheEntry {
159+ bump,
160+ node,
161+ edges,
162+ template,
163+ pinned,
164+ };
165+
166+ let mut set = set.borrow_mut();
167+ let id = set.next_id();
168+ set.items.insert(id, entry);
169+ id
170+ }
171+
172+ /// Does the cached set contain a cached node with the given id?
173+ pub fn contains(&self, id: CacheId) -> bool {
174+ self.items.contains_key(&id)
175+ }
176+
177+ /// Get the cached node and its template (if any) for the given cache id.
178+ pub fn get(&self, id: CacheId) -> (&Node, Option<CacheId>) {
179+ let entry = self
180+ .items
181+ .get(&id)
182+ .expect_throw("CachedSet::get: should have id in set");
183+ let node: &Node = unsafe { &*entry.node };
184+ (node, entry.template)
185+ }
186+}
src/change_list.rsdeleted−426
@@ -1,426 +0,0 @@
1−use crate::Listener;
2−use bumpalo::Bump;
3−use fxhash::FxHashMap;
4−use std::fmt;
5−use std::sync::Once;
6−use wasm_bindgen::prelude::*;
7−
8−pub mod js {
9− use wasm_bindgen::prelude::*;
10−
11− #[wasm_bindgen]
12− extern "C" {
13− #[derive(Clone, Debug)]
14− pub type ChangeList;
15−
16− #[wasm_bindgen(constructor)]
17− pub fn new(container: &web_sys::Node) -> ChangeList;
18−
19− #[wasm_bindgen(structural, method)]
20− pub fn unmount(this: &ChangeList);
21−
22− #[wasm_bindgen(structural, method, js_name = addChangeListRange)]
23− pub fn add_change_list_range(this: &ChangeList, start: usize, len: usize);
24−
25− #[wasm_bindgen(structural, method, js_name = applyChanges)]
26− pub fn apply_changes(this: &ChangeList, memory: JsValue);
27−
28− #[wasm_bindgen(structural, method, js_name = initEventsTrampoline)]
29− pub fn init_events_trampoline(
30− this: &ChangeList,
31− trampoline: &Closure<Fn(web_sys::Event, u32, u32)>,
32− );
33− }
34−}
35−
36−struct StringsCacheEntry {
37− key: u32,
38− used: bool,
39−}
40−
41−pub(crate) struct ChangeList {
42− bump: Bump,
43− strings_cache: FxHashMap<String, StringsCacheEntry>,
44− next_string_key: u32,
45− js: js::ChangeList,
46− events_trampoline: Option<Closure<Fn(web_sys::Event, u32, u32)>>,
47−}
48−
49−impl Drop for ChangeList {
50− fn drop(&mut self) {
51− debug!("Dropping ChangeList");
52− self.js.unmount();
53− }
54−}
55−
56−impl fmt::Debug for ChangeList {
57− fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58− f.debug_struct("ChangeList")
59− .field("bump", &self.bump)
60− .field("js", &self.js)
61− .field("events_trampoline", &"..")
62− .finish()
63− }
64−}
65−
66−impl ChangeList {
67− pub(crate) fn new(container: &web_sys::Node) -> ChangeList {
68− // XXX: Because wasm-bindgen-test doesn't support third party JS
69− // dependencies, we can't use `wasm_bindgen(module = "...")` for our
70− // `ChangeList` JS import. Instead, this *should* be a local JS snippet,
71− // but that isn't implemented yet:
72− // https://github.com/rustwasm/rfcs/pull/6
73− static EVAL: Once = Once::new();
74− EVAL.call_once(|| {
75− js_sys::eval(include_str!("../js/change-list.js"))
76− .expect("should eval change-list.js OK");
77− });
78−
79− let bump = Bump::new();
80− let strings_cache = FxHashMap::default();
81− let js = js::ChangeList::new(container);
82− ChangeList {
83− bump,
84− strings_cache,
85− next_string_key: 0,
86− js,
87− events_trampoline: None,
88− }
89− }
90−
91− pub(crate) fn apply_changes(&mut self) {
92− let js = &self.js;
93− unsafe {
94− self.bump.each_allocated_chunk(|ch| {
95− js.add_change_list_range(ch.as_ptr() as usize, ch.len());
96− });
97− }
98− js.apply_changes(wasm_bindgen::memory());
99− self.bump.reset();
100− }
101−
102− pub(crate) fn init_events_trampoline(
103− &mut self,
104− trampoline: Closure<Fn(web_sys::Event, u32, u32)>,
105− ) {
106− debug_assert!(self.events_trampoline.is_none());
107− self.js.init_events_trampoline(&trampoline);
108− self.events_trampoline = Some(trampoline);
109− }
110−}
111−
112−#[repr(u32)]
113−#[derive(Clone, Copy, Debug)]
114−enum ChangeDiscriminant {
115− /// Immediates: `(pointer, length)`
116− ///
117− /// Stack: `[... TextNode] -> [... TextNode]`
118− ///
119− /// ```text
120− /// stack.top().textContent = String(pointer, length)
121− /// ```
122− SetText = 0,
123−
124− /// Immediates: `()`
125− ///
126− /// Stack: `[... Node] -> [...]`
127− ///
128− /// ```text
129− /// node = stack.pop()
130− /// while (node.nextSibling) {
131− /// node.nextSibling.remove();
132− /// }
133− /// node.remove()
134− /// ```
135− RemoveSelfAndNextSiblings = 1,
136−
137− /// Immediates: `()`
138− ///
139− /// Stack: `[... Node Node] -> [... Node]`
140− ///
141− /// ```text
142− /// new = stack.pop()
143− /// old = stack.pop()
144− /// old.replaceWith(new)
145− /// stack.push(new)
146− /// ```
147− ReplaceWith = 2,
148−
149− /// Immediates: `(pointer1, length1, pointer2, length2)`
150− ///
151− /// Stack: `[... Node] -> [... Node]`
152− ///
153− /// ```text
154− /// stack.top().setAttribute(String(pointer1, length1), String(pointer2, length2))
155− /// ```
156− SetAttribute = 3,
157−
158− /// Immediates: (pointer, length)
159− ///
160− /// Stack: `[... Node] -> [... Node]`
161− ///
162− /// ```text
163− /// stack.top().removeAttribute(String(pointer, length))
164− /// ```
165− RemoveAttribute = 4,
166−
167− /// Immediates: `()`
168− ///
169− /// Stack: `[... Node] -> [... Node node]`
170− ///
171− /// ```text
172− /// stack.push(stack.top().firstChild)
173− /// ```
174− PushFirstChild = 5,
175−
176− /// Immediates: `()`
177− ///
178− /// Stack: `[... Node] -> [... Node]`
179− ///
180− /// ```text
181− /// stack.push(stack.pop().nextSibling)
182− /// ```
183− PopPushNextSibling = 6,
184−
185− /// Immediates: `()`
186− ///
187− /// Stack: `[... T] -> [...]`
188− ///
189− /// ```text
190− /// stack.pop()
191− /// ```
192− Pop = 7,
193−
194− /// Immediates: `()`
195− ///
196− /// Stack: `[... Node Node] -> [... Node]`
197− ///
198− /// ```text
199− /// child = stack.pop()
200− /// stack.top().appendChild(child)
201− /// ```
202− AppendChild = 8,
203−
204− /// Immediates: `(pointer, length)`
205− ///
206− /// Stack: `[...] -> [... Node]`
207− ///
208− /// ```text
209− /// stack.push(createTextNode(String(pointer, length)))
210− /// ```
211− CreateTextNode = 9,
212−
213− /// Immediates: `(pointer, length)`
214− ///
215− /// Stack: `[...] -> [... Node]`
216− ///
217− /// ```text
218− /// stack.push(createElement(String(pointer, length))
219− /// ```
220− CreateElement = 10,
221−
222− /// Immediate: `(pointer, length, A, B)`
223− ///
224− /// Stack: `[... Node] -> [... Node]`
225− ///
226− /// ```text
227− /// event = String(pointer, length)
228− /// callback = ProxyToRustCallback(A, B);
229− /// stack.top().addEventListener(event, callback);
230− /// ```
231− NewEventListener = 11,
232−
233− /// Immediate: `(pointer, length, A, B)`
234− ///
235− /// Stack: `[... Node] -> [... Node]`
236− ///
237− /// ```text
238− /// event = String(pointer, length)
239− /// new_callback = ProxyToRustCallback(A, B);
240− /// stack.top().updateEventlistener(new_callback)
241− /// ```
242− UpdateEventListener = 12,
243−
244− /// Immediates: `(pointer, length)`
245− ///
246− /// Stack: `[... Node] -> [... Node]`
247− ///
248− /// ```text
249− /// stack.top().removeEventListener(String(pointer, length));
250− /// ```
251− RemoveEventListener = 13,
252−
253− /// Immediates: `(pointer, length, id)`
254− ///
255− /// Stack: `[...] -> [...]`
256− ///
257− /// ```text
258− /// addString(String(pointer, length), id);
259− /// ```
260− AddString = 14,
261−
262− /// Immediates: `(id)`
263− ///
264− /// Stack: `[...] -> [...]`
265− ///
266− /// ```text
267− /// dropString(id);
268− /// ```
269− DropString = 15,
270−}
271−
272−// Allocation utilities to ensure that we only allocation sequences of `u32`s
273−// into the change list's bump arena without any padding. This helps maintain
274−// the invariants required for `Bump::each_allocated_chunk`'s safety.
275−impl ChangeList {
276− // Allocate an opcode with zero immediates.
277− fn op0(&self, discriminant: ChangeDiscriminant) {
278− self.bump.alloc(discriminant as u32);
279− }
280−
281− // Allocate an opcode with one immediate.
282− fn op1(&self, discriminant: ChangeDiscriminant, a: u32) {
283− self.bump.alloc([discriminant as u32, a]);
284− }
285−
286− // Allocate an opcode with two immediates.
287− fn op2(&self, discriminant: ChangeDiscriminant, a: u32, b: u32) {
288− self.bump.alloc([discriminant as u32, a, b]);
289− }
290−
291− // Allocate an opcode with three immediates.
292− fn op3(&self, discriminant: ChangeDiscriminant, a: u32, b: u32, c: u32) {
293− self.bump.alloc([discriminant as u32, a, b, c]);
294− }
295−
296− // Note: no 4-immediate opcodes at this time.
297−}
298−
299−impl ChangeList {
300− fn ensure_string(&mut self, string: &str) -> u32 {
301− if let Some(entry) = self.strings_cache.get_mut(string) {
302− entry.used = true;
303− entry.key
304− } else {
305− let key = self.next_string_key;
306− self.next_string_key += 1;
307− let entry = StringsCacheEntry { key, used: true };
308− self.strings_cache.insert(string.to_string(), entry);
309− self.op3(
310− ChangeDiscriminant::AddString,
311− string.as_ptr() as u32,
312− string.len() as u32,
313− key,
314− );
315− key
316− }
317− }
318−
319− pub(crate) fn drop_unused_strings(&mut self) {
320− debug!("drop_unused_strings()");
321− let mut new_cache = FxHashMap::default();
322− for (key, val) in self.strings_cache.iter() {
323− if val.used {
324− new_cache.insert(
325− key.clone(),
326− StringsCacheEntry {
327− key: val.key,
328− used: false,
329− },
330− );
331− } else {
332− self.op1(ChangeDiscriminant::DropString, val.key);
333− }
334− }
335− self.strings_cache = new_cache;
336− }
337−
338− pub(crate) fn emit_set_text(&self, text: &str) {
339− debug!("emit_set_text({:?})", text);
340− self.op2(
341− ChangeDiscriminant::SetText,
342− text.as_ptr() as u32,
343− text.len() as u32,
344− );
345− }
346−
347− pub(crate) fn emit_remove_self_and_next_siblings(&self) {
348− debug!("emit_remove_self_and_next_siblings()");
349− self.op0(ChangeDiscriminant::RemoveSelfAndNextSiblings);
350− }
351−
352− pub(crate) fn emit_replace_with(&self) {
353− debug!("emit_replace_with()");
354− self.op0(ChangeDiscriminant::ReplaceWith);
355− }
356−
357− pub(crate) fn emit_set_attribute(&mut self, name: &str, value: &str) {
358− debug!("emit_set_attribute({:?}, {:?})", name, value);
359− let name_id = self.ensure_string(name);
360− let value_id = self.ensure_string(value);
361− self.op2(ChangeDiscriminant::SetAttribute, name_id, value_id);
362− }
363−
364− pub(crate) fn emit_remove_attribute(&mut self, name: &str) {
365− debug!("emit_remove_attribute({:?})", name);
366− let name_id = self.ensure_string(name);
367− self.op1(ChangeDiscriminant::RemoveAttribute, name_id);
368− }
369−
370− pub(crate) fn emit_push_first_child(&self) {
371− debug!("emit_push_first_child()");
372− self.op0(ChangeDiscriminant::PushFirstChild);
373− }
374−
375− pub(crate) fn emit_pop_push_next_sibling(&self) {
376− debug!("emit_pop_push_next_sibling()");
377− self.op0(ChangeDiscriminant::PopPushNextSibling);
378− }
379−
380− pub(crate) fn emit_pop(&self) {
381− debug!("emit_pop()");
382− self.op0(ChangeDiscriminant::Pop);
383− }
384−
385− pub(crate) fn emit_append_child(&self) {
386− debug!("emit_append_child()");
387− self.op0(ChangeDiscriminant::AppendChild);
388− }
389−
390− pub(crate) fn emit_create_text_node(&self, text: &str) {
391− debug!("emit_create_text_node({:?})", text);
392− self.op2(
393− ChangeDiscriminant::CreateTextNode,
394− text.as_ptr() as u32,
395− text.len() as u32,
396− );
397− }
398−
399− pub(crate) fn emit_create_element(&mut self, tag_name: &str) {
400− debug!("emit_create_element({:?})", tag_name);
401− let tag_name_id = self.ensure_string(tag_name);
402− self.op1(ChangeDiscriminant::CreateElement, tag_name_id);
403− }
404−
405− pub(crate) fn emit_new_event_listener(&mut self, listener: &Listener) {
406− debug!("emit_new_event_listener({:?})", listener);
407− let (a, b) = listener.get_callback_parts();
408− debug_assert!(a != 0);
409− let event_id = self.ensure_string(listener.event);
410− self.op3(ChangeDiscriminant::NewEventListener, event_id, a, b);
411− }
412−
413− pub(crate) fn emit_update_event_listener(&mut self, listener: &Listener) {
414− debug!("emit_update_event_listener({:?})", listener);
415− let (a, b) = listener.get_callback_parts();
416− debug_assert!(a != 0);
417− let event_id = self.ensure_string(listener.event);
418− self.op3(ChangeDiscriminant::UpdateEventListener, event_id, a, b);
419− }
420−
421− pub(crate) fn emit_remove_event_listener(&mut self, event: &str) {
422− debug!("emit_remove_event_listener({:?})", event);
423− let event_id = self.ensure_string(event);
424− self.op1(ChangeDiscriminant::RemoveEventListener, event_id);
425− }
426−}
src/change_list/emitter.rsnew file+292
@@ -0,0 +1,292 @@
1+//! The `InstructionEmitter` is responsible for encoding change list
2+//! instructions and ensuring that each instruction has the correct number of
3+//! immediates. It ensures that the resulting change list instruction stream is
4+//! *syntactically* correct (opcodes have the correct arity of immediate
5+//! arguments, etc), while `ChangeListBuilder` wraps an emitter and additionally
6+//! ensures that the resulting change list program is *semantically* correct
7+//! (doesn't reference cached strings before they've been added to the cache,
8+//! etc).
9+//!
10+//! We encode the instructions directly into a dedicated bump arena. We
11+//! eventually pass the bump arena's chunks to the interpreter in JS, so it is
12+//! critical that nothing other than change list instructions are allocated
13+//! inside this bump, and that the instructions themselves do not contain any
14+//! padding or uninitialized memory. See the documentation for the the
15+//! `Bump::each_allocated_chunk` method for details.
16+
17+use bumpalo::Bump;
18+
19+#[derive(Debug)]
20+pub(crate) struct InstructionEmitter {
21+ bump: Bump,
22+}
23+
24+impl InstructionEmitter {
25+ /// Construct a new `InstructionEmitter` with its own bump arena.
26+ pub fn new() -> InstructionEmitter {
27+ let bump = Bump::new();
28+ InstructionEmitter { bump }
29+ }
30+
31+ /// Invoke the given function with each of the allocated instruction
32+ /// sequences that this emitter has built up.
33+ #[cfg_attr(feature = "xxx-unstable-internal-use-only", allow(dead_code))]
34+ pub fn each_instruction_sequence<F>(&mut self, f: F)
35+ where
36+ F: FnMut(&[u8]),
37+ {
38+ // Note: the safety invariants required for `each_allocated_chunk` are
39+ // maintained by the fact that everything we encode as allocations in
40+ // the bump arena are in the form of `u32`s, and therefore we don't have
41+ // any uninitialized memory padding in the arena.
42+ unsafe {
43+ self.bump.each_allocated_chunk(f);
44+ }
45+ }
46+
47+ /// Reset to an empty sequence of instructions.
48+ pub fn reset(&mut self) {
49+ self.bump.reset();
50+ }
51+}
52+
53+macro_rules! define_change_list_instructions {
54+ ( $(
55+ $( #[$attr:meta] )*
56+ $name:ident (
57+ $($immediate:ident),*
58+ ) = $discriminant:expr,
59+ )* ) => {
60+ impl InstructionEmitter {
61+ $(
62+ $( #[$attr] )*
63+ #[inline]
64+ pub fn $name(&self $(, $immediate: u32)*) {
65+ self.bump.alloc_with(|| [$discriminant $(, $immediate )* ]);
66+ }
67+ )*
68+ }
69+ }
70+}
71+
72+define_change_list_instructions! {
73+ /// Stack: `[... TextNode] -> [... TextNode]`
74+ ///
75+ /// ```text
76+ /// stack.top().textContent = readString(pointer, length)
77+ /// ```
78+ set_text(pointer, length) = 0,
79+
80+ /// Stack: `[... Node] -> [...]`
81+ ///
82+ /// ```text
83+ /// node = stack.pop()
84+ /// while (node.nextSibling) {
85+ /// node.nextSibling.remove();
86+ /// }
87+ /// node.remove()
88+ /// ```
89+ remove_self_and_next_siblings() = 1,
90+
91+ /// Stack: `[... Node Node] -> [... Node]`
92+ ///
93+ /// ```text
94+ /// new = stack.pop()
95+ /// old = stack.pop()
96+ /// old.replaceWith(new)
97+ /// stack.push(new)
98+ /// ```
99+ replace_with() = 2,
100+
101+ /// Stack: `[... Node] -> [... Node]`
102+ ///
103+ /// ```text
104+ /// stack.top().setAttribute(getCachedString(attribute_key), getCachedString(value_key))
105+ /// ```
106+ set_attribute(attribute_key, value_key) = 3,
107+
108+ /// Stack: `[... Node] -> [... Node]`
109+ ///
110+ /// ```text
111+ /// stack.top().removeAttribute(getCachedString(attribute_key))
112+ /// ```
113+ remove_attribute(attribute_key) = 4,
114+
115+ /// Stack: `[... Node] -> [... Node Node]`
116+ ///
117+ /// ```text
118+ /// parent = stack.top()
119+ /// child = parent.childNodes[parent.childNodes.length - n - 1]
120+ /// stack.push(child)
121+ /// ```
122+ push_reverse_child(n) = 5,
123+
124+ /// Stack: `[... Node Node] -> [... Node Node]`
125+ ///
126+ /// ```text
127+ /// stack.pop();
128+ /// parent = stack.top();
129+ /// child = parent.childNodes[n]
130+ /// stack.push(child)
131+ /// ```
132+ pop_push_child(n) = 6,
133+
134+ /// Stack: `[... T] -> [...]`
135+ ///
136+ /// ```text
137+ /// stack.pop()
138+ /// ```
139+ pop() = 7,
140+
141+ /// Stack: `[... Node Node] -> [... Node]`
142+ ///
143+ /// ```text
144+ /// child = stack.pop()
145+ /// stack.top().appendChild(child)
146+ /// ```
147+ append_child() = 8,
148+
149+ /// Stack: `[...] -> [... Node]`
150+ ///
151+ /// ```text
152+ /// stack.push(document.createTextNode(readString(pointer, length)))
153+ /// ```
154+ create_text_node(pointer, length) = 9,
155+
156+ /// Stack: `[...] -> [... Node]`
157+ ///
158+ /// ```text
159+ /// stack.push(document.createElement(getCachedString(tag_name_key))
160+ /// ```
161+ create_element(tag_name_key) = 10,
162+
163+ /// Stack: `[... Node] -> [... Node]`
164+ ///
165+ /// ```text
166+ /// event = getCachedString(event_key)
167+ /// callback = createProxyToRustCallback(a, b)
168+ /// stack.top().addEventListener(event, callback)
169+ /// ```
170+ new_event_listener(event_key, a, b) = 11,
171+
172+ /// Stack: `[... Node] -> [... Node]`
173+ ///
174+ /// ```text
175+ /// event = getCachedString(event_key)
176+ /// new_callback = createProxyToRustCallback(a, b);
177+ /// stack.top().updateEventlistener(new_callback)
178+ /// ```
179+ update_event_listener(event_key, a, b) = 12,
180+
181+ /// Stack: `[... Node] -> [... Node]`
182+ ///
183+ /// ```text
184+ /// stack.top().removeEventListener(getCachedString(event_key));
185+ /// ```
186+ remove_event_listener(event_key) = 13,
187+
188+ /// Stack: `[...] -> [...]`
189+ ///
190+ /// ```text
191+ /// addCachedString(readString(pointer, length), key);
192+ /// ```
193+ add_cached_string(pointer, length, key) = 14,
194+
195+ /// Stack: `[...] -> [...]`
196+ ///
197+ /// ```text
198+ /// dropCachedString(key);
199+ /// ```
200+ drop_cached_string(key) = 15,
201+
202+ /// Stack: `[...] -> [... Node]`
203+ ///
204+ /// ```text
205+ /// tag_name = getCachedString(tag_name_key)
206+ /// namespace = getCachedString(tag_name_key)
207+ /// stack.push(document.createElementNS(tag_name, namespace))
208+ /// ```
209+ create_element_ns(tag_name_key, namespace_key) = 16,
210+
211+ /// Stack: `[...] -> [...]`
212+ ///
213+ /// ```text
214+ /// parent = stack.top()
215+ /// children = parent.childNodes
216+ /// temp = temp_base
217+ /// for i in start .. end:
218+ /// temporaries[temp] = children[i]
219+ /// temp += 1
220+ /// ```
221+ save_children_to_temporaries(temp_base, start, end) = 17,
222+
223+ /// Stack: `[... Node] -> [... Node Node]`
224+ ///
225+ /// ```text
226+ /// parent = stack.top()
227+ /// child = parent.childNodes[n]
228+ /// stack.push(child)
229+ /// ```
230+ push_child(n) = 18,
231+
232+ /// Stack: `[...] -> [... Node]`
233+ ///
234+ /// ```text
235+ /// stack.push(temporaries[temp])
236+ /// ```
237+ push_temporary(temp) = 19,
238+
239+ /// Stack: `[... Node Node] -> [... Node]`
240+ ///
241+ /// ```text
242+ /// before = stack.pop()
243+ /// after = stack.pop()
244+ /// after.insertBefore(before)
245+ /// stack.push(before)
246+ /// ```
247+ insert_before() = 20,
248+
249+ /// Stack: `[... Node Node] -> [... Node Node]`
250+ ///
251+ /// ```text
252+ /// stack.pop()
253+ /// parent = stack.top()
254+ /// child = parent.childNodes[parent.childNodes.length - n - 1]
255+ /// stack.push(child)
256+ /// ```
257+ pop_push_reverse_child(n) = 21,
258+
259+ /// Stack: `[... Node] -> [... Node]`
260+ ///
261+ /// ```text
262+ /// parent = stack.top()
263+ /// child = parent.childNodes[n]
264+ /// child.remove()
265+ /// ```
266+ remove_child(n) = 22,
267+
268+ /// Stack: `[... Node] -> [... Node]`
269+ ///
270+ /// ```text
271+ /// class = getCachedString(class)
272+ /// node = stack.top()
273+ /// node.className = class
274+ /// ```
275+ set_class(class) = 23,
276+
277+ /// Stack: `[... Node] -> [... Node]`
278+ ///
279+ /// ```text
280+ /// template = stack.top()
281+ /// saveTemplate(id, template)
282+ /// ```
283+ save_template(id) = 24,
284+
285+ /// Stack: `[...] -> [... Node]`
286+ ///
287+ /// ```text
288+ /// template = getTemplate(id)
289+ /// stack.push(template.cloneNode(true))
290+ /// ```
291+ push_template(id) = 25,
292+}
src/change_list/js.rsnew file+40
@@ -0,0 +1,40 @@
1+cfg_if::cfg_if! {
2+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
3+ #[derive(Clone, Debug)]
4+ pub struct ChangeListInterpreter {}
5+ impl ChangeListInterpreter {
6+ pub fn new(_container: &crate::Element) -> ChangeListInterpreter {
7+ ChangeListInterpreter {}
8+ }
9+ pub fn unmount(&self) {}
10+ pub fn add_change_list_range(&self, _start: usize, _len: usize) {}
11+ pub fn init_events_trampoline(&self, _trampoline: &crate::EventsTrampoline) {}
12+ }
13+ } else {
14+ use wasm_bindgen::prelude::*;
15+
16+ #[wasm_bindgen(module = "/js/change-list-interpreter.js")]
17+ extern "C" {
18+ #[derive(Clone, Debug)]
19+ pub type ChangeListInterpreter;
20+
21+ #[wasm_bindgen(constructor)]
22+ pub fn new(container: &web_sys::Element) -> ChangeListInterpreter;
23+
24+ #[wasm_bindgen(structural, method)]
25+ pub fn unmount(this: &ChangeListInterpreter);
26+
27+ #[wasm_bindgen(structural, method, js_name = addChangeListRange)]
28+ pub fn add_change_list_range(this: &ChangeListInterpreter, start: usize, len: usize);
29+
30+ #[wasm_bindgen(structural, method, js_name = applyChanges)]
31+ pub fn apply_changes(this: &ChangeListInterpreter, memory: JsValue);
32+
33+ #[wasm_bindgen(structural, method, js_name = initEventsTrampoline)]
34+ pub fn init_events_trampoline(
35+ this: &ChangeListInterpreter,
36+ trampoline: &crate::EventsTrampoline,
37+ );
38+ }
39+ }
40+}
src/change_list/mod.rsnew file+353
@@ -0,0 +1,353 @@
1+pub(crate) mod emitter;
2+pub(crate) mod strings;
3+pub(crate) mod traversal;
4+
5+// Note: has to be `pub` because of `wasm-bindgen` visibility restrictions.
6+pub mod js;
7+
8+use self::emitter::InstructionEmitter;
9+use self::strings::{StringKey, StringsCache};
10+use self::traversal::{MoveTo, Traversal};
11+use crate::{cached_set::CacheId, Listener};
12+use fxhash::FxHashSet;
13+
14+#[derive(Debug)]
15+pub(crate) struct ChangeListPersistentState {
16+ strings: StringsCache,
17+ emitter: InstructionEmitter,
18+ traversal: Traversal,
19+ interpreter: js::ChangeListInterpreter,
20+ templates: FxHashSet<CacheId>,
21+}
22+
23+pub(crate) struct ChangeListBuilder<'a> {
24+ state: &'a mut ChangeListPersistentState,
25+ next_temporary: u32,
26+ forcing_new_listeners: bool,
27+}
28+
29+impl Drop for ChangeListPersistentState {
30+ fn drop(&mut self) {
31+ self.interpreter.unmount();
32+ }
33+}
34+
35+impl ChangeListPersistentState {
36+ pub(crate) fn new(container: &crate::Element) -> ChangeListPersistentState {
37+ let strings = StringsCache::new();
38+ let emitter = InstructionEmitter::new();
39+ let traversal = Traversal::new();
40+ let interpreter = js::ChangeListInterpreter::new(container);
41+ let templates = Default::default();
42+ ChangeListPersistentState {
43+ strings,
44+ emitter,
45+ traversal,
46+ interpreter,
47+ templates,
48+ }
49+ }
50+
51+ pub(crate) fn init_events_trampoline(&mut self, trampoline: &crate::EventsTrampoline) {
52+ self.interpreter.init_events_trampoline(trampoline);
53+ }
54+
55+ pub(crate) fn builder<'a>(&'a mut self) -> ChangeListBuilder<'a> {
56+ ChangeListBuilder {
57+ state: self,
58+ next_temporary: 0,
59+ forcing_new_listeners: false,
60+ }
61+ }
62+}
63+
64+cfg_if::cfg_if! {
65+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
66+ impl ChangeListBuilder<'_> {
67+ pub(crate) fn finish(self) {
68+ self.state.strings.drop_unused_strings(&self.state.emitter);
69+
70+ // Nothing to actually apply the changes to.
71+
72+ self.state.emitter.reset();
73+ self.state.traversal.reset();
74+ }
75+ }
76+ } else {
77+ impl ChangeListBuilder<'_> {
78+ pub(crate) fn finish(self) {
79+ self.state.strings.drop_unused_strings(&self.state.emitter);
80+
81+ // Apply the changes.
82+ let interpreter = &self.state.interpreter;
83+ self.state.emitter.each_instruction_sequence(|seq| {
84+ interpreter.add_change_list_range(seq.as_ptr() as usize, seq.len());
85+ });
86+ interpreter.apply_changes(wasm_bindgen::memory());
87+
88+ self.state.emitter.reset();
89+ self.state.traversal.reset();
90+ }
91+ }
92+ }
93+}
94+
95+/// Traversal methods.
96+impl ChangeListBuilder<'_> {
97+ pub fn go_down(&mut self) {
98+ self.state.traversal.down();
99+ }
100+
101+ pub fn go_down_to_child(&mut self, index: usize) {
102+ self.state.traversal.down();
103+ self.state.traversal.sibling(index);
104+ }
105+
106+ pub fn go_down_to_reverse_child(&mut self, index: usize) {
107+ self.state.traversal.down();
108+ self.state.traversal.reverse_sibling(index);
109+ }
110+
111+ pub fn go_up(&mut self) {
112+ self.state.traversal.up();
113+ }
114+
115+ pub fn go_to_sibling(&mut self, index: usize) {
116+ self.state.traversal.sibling(index);
117+ }
118+
119+ pub fn go_to_temp_sibling(&mut self, temp: u32) {
120+ self.state.traversal.up();
121+ self.state.traversal.down_to_temp(temp);
122+ }
123+
124+ pub fn go_down_to_temp_child(&mut self, temp: u32) {
125+ self.state.traversal.down_to_temp(temp);
126+ }
127+
128+ pub fn commit_traversal(&mut self) {
129+ if self.state.traversal.is_committed() {
130+ return;
131+ }
132+
133+ for mv in self.state.traversal.commit() {
134+ match mv {
135+ MoveTo::Parent => {
136+ debug!("emit: pop");
137+ self.state.emitter.pop();
138+ }
139+ MoveTo::Child(n) => {
140+ debug!("emit: push_child({})", n);
141+ self.state.emitter.push_child(n);
142+ }
143+ MoveTo::ReverseChild(n) => {
144+ debug!("emit: push_reverse_child({})", n);
145+ self.state.emitter.push_reverse_child(n);
146+ }
147+ MoveTo::Sibling(n) => {
148+ debug!("emit: pop_push_child({})", n);
149+ self.state.emitter.pop_push_child(n);
150+ }
151+ MoveTo::ReverseSibling(n) => {
152+ debug!("emit: pop_push_reverse_child({})", n);
153+ self.state.emitter.pop_push_reverse_child(n);
154+ }
155+ MoveTo::TempChild(temp) => {
156+ debug!("emit: push_temporary({})", temp);
157+ self.state.emitter.push_temporary(temp);
158+ }
159+ }
160+ }
161+ }
162+
163+ pub fn traversal_is_committed(&self) -> bool {
164+ self.state.traversal.is_committed()
165+ }
166+}
167+
168+impl ChangeListBuilder<'_> {
169+ pub fn next_temporary(&self) -> u32 {
170+ self.next_temporary
171+ }
172+
173+ pub fn set_next_temporary(&mut self, next_temporary: u32) {
174+ self.next_temporary = next_temporary;
175+ }
176+
177+ pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 {
178+ debug_assert!(self.traversal_is_committed());
179+ debug_assert!(start < end);
180+ let temp_base = self.next_temporary;
181+ debug!(
182+ "emit: save_children_to_temporaries({}, {}, {})",
183+ temp_base, start, end
184+ );
185+ self.next_temporary = temp_base + (end - start) as u32;
186+ self.state
187+ .emitter
188+ .save_children_to_temporaries(temp_base, start as u32, end as u32);
189+ temp_base
190+ }
191+
192+ pub fn push_temporary(&self, temp: u32) {
193+ debug_assert!(self.traversal_is_committed());
194+ debug!("emit: push_temporary({})", temp);
195+ self.state.emitter.push_temporary(temp);
196+ }
197+
198+ pub fn remove_child(&self, child: usize) {
199+ debug_assert!(self.traversal_is_committed());
200+ debug!("emit: remove_child({})", child);
201+ self.state.emitter.remove_child(child as u32);
202+ }
203+
204+ pub fn insert_before(&self) {
205+ debug_assert!(self.traversal_is_committed());
206+ debug!("emit: insert_before()");
207+ self.state.emitter.insert_before();
208+ }
209+
210+ pub fn ensure_string(&mut self, string: &str) -> StringKey {
211+ self.state
212+ .strings
213+ .ensure_string(string, &self.state.emitter)
214+ }
215+
216+ pub fn set_text(&self, text: &str) {
217+ debug_assert!(self.traversal_is_committed());
218+ debug!("emit: set_text({:?})", text);
219+ self.state
220+ .emitter
221+ .set_text(text.as_ptr() as u32, text.len() as u32);
222+ }
223+
224+ pub fn remove_self_and_next_siblings(&self) {
225+ debug_assert!(self.traversal_is_committed());
226+ debug!("emit: remove_self_and_next_siblings()");
227+ self.state.emitter.remove_self_and_next_siblings();
228+ }
229+
230+ pub fn replace_with(&self) {
231+ debug_assert!(self.traversal_is_committed());
232+ debug!("emit: replace_with()");
233+ self.state.emitter.replace_with();
234+ }
235+
236+ pub fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) {
237+ debug_assert!(self.traversal_is_committed());
238+ if name == "class" && !is_namespaced {
239+ let class_id = self.ensure_string(value);
240+ debug!("emit: set_class({:?})", value);
241+ self.state.emitter.set_class(class_id.into());
242+ } else {
243+ let name_id = self.ensure_string(name);
244+ let value_id = self.ensure_string(value);
245+ debug!("emit: set_attribute({:?}, {:?})", name, value);
246+ self.state
247+ .emitter
248+ .set_attribute(name_id.into(), value_id.into());
249+ }
250+ }
251+
252+ pub fn remove_attribute(&mut self, name: &str) {
253+ debug_assert!(self.traversal_is_committed());
254+ debug!("emit: remove_attribute({:?})", name);
255+ let name_id = self.ensure_string(name);
256+ self.state.emitter.remove_attribute(name_id.into());
257+ }
258+
259+ pub fn append_child(&self) {
260+ debug_assert!(self.traversal_is_committed());
261+ debug!("emit: append_child()");
262+ self.state.emitter.append_child();
263+ }
264+
265+ pub fn create_text_node(&self, text: &str) {
266+ debug_assert!(self.traversal_is_committed());
267+ debug!("emit: create_text_node({:?})", text);
268+ self.state
269+ .emitter
270+ .create_text_node(text.as_ptr() as u32, text.len() as u32);
271+ }
272+
273+ pub fn create_element(&mut self, tag_name: &str) {
274+ debug_assert!(self.traversal_is_committed());
275+ debug!("emit: create_element({:?})", tag_name);
276+ let tag_name_id = self.ensure_string(tag_name);
277+ self.state.emitter.create_element(tag_name_id.into());
278+ }
279+
280+ pub fn create_element_ns(&mut self, tag_name: &str, ns: &str) {
281+ debug_assert!(self.traversal_is_committed());
282+ debug!("emit: create_element_ns({:?}, {:?})", tag_name, ns);
283+ let tag_name_id = self.ensure_string(tag_name);
284+ let ns_id = self.ensure_string(ns);
285+ self.state
286+ .emitter
287+ .create_element_ns(tag_name_id.into(), ns_id.into());
288+ }
289+
290+ pub fn push_force_new_listeners(&mut self) -> bool {
291+ let old = self.forcing_new_listeners;
292+ self.forcing_new_listeners = true;
293+ old
294+ }
295+
296+ pub fn pop_force_new_listeners(&mut self, previous: bool) {
297+ debug_assert!(self.forcing_new_listeners);
298+ self.forcing_new_listeners = previous;
299+ }
300+
301+ pub fn new_event_listener(&mut self, listener: &Listener) {
302+ debug_assert!(self.traversal_is_committed());
303+ debug!("emit: new_event_listener({:?})", listener);
304+ let (a, b) = listener.get_callback_parts();
305+ debug_assert!(a != 0);
306+ let event_id = self.ensure_string(listener.event);
307+ self.state.emitter.new_event_listener(event_id.into(), a, b);
308+ }
309+
310+ pub fn update_event_listener(&mut self, listener: &Listener) {
311+ debug_assert!(self.traversal_is_committed());
312+
313+ if self.forcing_new_listeners {
314+ self.new_event_listener(listener);
315+ return;
316+ }
317+
318+ debug!("emit: update_event_listener({:?})", listener);
319+ let (a, b) = listener.get_callback_parts();
320+ debug_assert!(a != 0);
321+ let event_id = self.ensure_string(listener.event);
322+ self.state
323+ .emitter
324+ .update_event_listener(event_id.into(), a, b);
325+ }
326+
327+ pub fn remove_event_listener(&mut self, event: &str) {
328+ debug_assert!(self.traversal_is_committed());
329+ debug!("emit: remove_event_listener({:?})", event);
330+ let event_id = self.ensure_string(event);
331+ self.state.emitter.remove_event_listener(event_id.into());
332+ }
333+
334+ #[inline]
335+ pub fn has_template(&mut self, id: CacheId) -> bool {
336+ self.state.templates.contains(&id)
337+ }
338+
339+ pub fn save_template(&mut self, id: CacheId) {
340+ debug_assert!(self.traversal_is_committed());
341+ debug_assert!(!self.has_template(id));
342+ debug!("emit: save_template({:?})", id);
343+ self.state.templates.insert(id);
344+ self.state.emitter.save_template(id.into());
345+ }
346+
347+ pub fn push_template(&mut self, id: CacheId) {
348+ debug_assert!(self.traversal_is_committed());
349+ debug_assert!(self.has_template(id));
350+ debug!("emit: push_template({:?})", id);
351+ self.state.emitter.push_template(id.into());
352+ }
353+}
src/change_list/strings.rsnew file+66
@@ -0,0 +1,66 @@
1+use crate::change_list::emitter::InstructionEmitter;
2+use fxhash::FxHashMap;
3+
4+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5+pub struct StringKey(u32);
6+
7+impl From<StringKey> for u32 {
8+ #[inline]
9+ fn from(key: StringKey) -> u32 {
10+ key.0
11+ }
12+}
13+
14+#[derive(Debug)]
15+struct StringsCacheEntry {
16+ key: StringKey,
17+ used: bool,
18+}
19+
20+#[derive(Debug, Default)]
21+pub(crate) struct StringsCache {
22+ entries: FxHashMap<String, StringsCacheEntry>,
23+ next_string_key: u32,
24+}
25+
26+impl StringsCache {
27+ /// Create a new, empty strings cache.
28+ pub fn new() -> StringsCache {
29+ Default::default()
30+ }
31+
32+ /// Ensure that the given string is cached, and get its key.
33+ pub fn ensure_string(&mut self, string: &str, emitter: &InstructionEmitter) -> StringKey {
34+ if let Some(entry) = self.entries.get_mut(string) {
35+ entry.used = true;
36+ entry.key
37+ } else {
38+ let key = StringKey(self.next_string_key);
39+ self.next_string_key += 1;
40+ let entry = StringsCacheEntry { key, used: true };
41+ self.entries.insert(string.to_string(), entry);
42+ emitter.add_cached_string(string.as_ptr() as u32, string.len() as u32, key.into());
43+ key
44+ }
45+ }
46+
47+ pub fn drop_unused_strings(&mut self, emitter: &InstructionEmitter) {
48+ self.entries.retain(|string, entry| {
49+ if entry.used {
50+ // Since this entry was used during while rendering this frame,
51+ // it is likely going to be used for the next frame as well. So
52+ // we keep it in the cache so that we don't have to repopulate
53+ // and resync the cache next frame (assuming it is reused), but
54+ // we set the `used` flag to false so that if it is not used
55+ // next frame, it will be cleaned up.
56+ entry.used = false;
57+ true
58+ } else {
59+ let key = entry.key.into();
60+ debug!("emit: drop_cached_string({}) = {:?}", key, string);
61+ emitter.drop_cached_string(key);
62+ false
63+ }
64+ });
65+ }
66+}
src/change_list/traversal.rsnew file+344
@@ -0,0 +1,344 @@
1+//! Keeps track of where we are moving in a DOM tree, and shortens traversal
2+//! paths between mutations to their minimal number of operations.
3+
4+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5+pub enum MoveTo {
6+ /// Move from the current node up to its parent.
7+ Parent,
8+
9+ /// Move to the current node's n^th child.
10+ Child(u32),
11+
12+ /// Move to the current node's n^th from last child.
13+ ReverseChild(u32),
14+
15+ /// Move to the n^th sibling. Not relative from the current
16+ /// location. Absolute indexed within all of the current siblings.
17+ Sibling(u32),
18+
19+ /// Move to the n^th from last sibling. Not relative from the current
20+ /// location. Absolute indexed within all of the current siblings.
21+ ReverseSibling(u32),
22+
23+ /// Move down to the given saved temporary child.
24+ TempChild(u32),
25+}
26+
27+#[derive(Debug)]
28+pub struct Traversal {
29+ uncommitted: Vec<MoveTo>,
30+}
31+
32+impl Traversal {
33+ /// Construct a new `Traversal` with its internal storage backed by the
34+ /// given bump arena.
35+ pub fn new() -> Traversal {
36+ Traversal {
37+ uncommitted: Vec::with_capacity(32),
38+ }
39+ }
40+
41+ /// Move the traversal up in the tree.
42+ pub fn up(&mut self) {
43+ match self.uncommitted.last() {
44+ Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
45+ self.uncommitted.pop();
46+ self.uncommitted.push(MoveTo::Parent);
47+ }
48+ Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) | Some(MoveTo::ReverseChild(_)) => {
49+ self.uncommitted.pop();
50+ // And we're back at the parent.
51+ }
52+ _ => {
53+ self.uncommitted.push(MoveTo::Parent);
54+ }
55+ }
56+ }
57+
58+ /// Move the traversal down in the tree to the first child of the current
59+ /// node.
60+ pub fn down(&mut self) {
61+ if let Some(&MoveTo::Parent) = self.uncommitted.last() {
62+ self.uncommitted.pop();
63+ self.sibling(0);
64+ } else {
65+ self.uncommitted.push(MoveTo::Child(0));
66+ }
67+ }
68+
69+ /// Move the traversal to the n^th sibling.
70+ pub fn sibling(&mut self, index: usize) {
71+ let index = index as u32;
72+ match self.uncommitted.last_mut() {
73+ Some(MoveTo::Sibling(ref mut n)) | Some(MoveTo::Child(ref mut n)) => {
74+ *n = index;
75+ }
76+ Some(MoveTo::ReverseSibling(_)) => {
77+ self.uncommitted.pop();
78+ self.uncommitted.push(MoveTo::Sibling(index));
79+ }
80+ Some(MoveTo::TempChild(_)) | Some(MoveTo::ReverseChild(_)) => {
81+ self.uncommitted.pop();
82+ self.uncommitted.push(MoveTo::Child(index))
83+ }
84+ _ => {
85+ self.uncommitted.push(MoveTo::Sibling(index));
86+ }
87+ }
88+ }
89+
90+ /// Move the the n^th from last sibling.
91+ pub fn reverse_sibling(&mut self, index: usize) {
92+ let index = index as u32;
93+ match self.uncommitted.last_mut() {
94+ Some(MoveTo::ReverseSibling(ref mut n)) | Some(MoveTo::ReverseChild(ref mut n)) => {
95+ *n = index;
96+ }
97+ Some(MoveTo::Sibling(_)) => {
98+ self.uncommitted.pop();
99+ self.uncommitted.push(MoveTo::ReverseSibling(index));
100+ }
101+ Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) => {
102+ self.uncommitted.pop();
103+ self.uncommitted.push(MoveTo::ReverseChild(index))
104+ }
105+ _ => {
106+ self.uncommitted.push(MoveTo::ReverseSibling(index));
107+ }
108+ }
109+ }
110+
111+ /// Go to the given saved temporary.
112+ pub fn down_to_temp(&mut self, temp: u32) {
113+ match self.uncommitted.last() {
114+ Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => {
115+ self.uncommitted.pop();
116+ }
117+ Some(MoveTo::Parent)
118+ | Some(MoveTo::TempChild(_))
119+ | Some(MoveTo::Child(_))
120+ | Some(MoveTo::ReverseChild(_))
121+ | None => {
122+ // Can't remove moves to parents since we rely on their stack
123+ // pops.
124+ }
125+ }
126+ self.uncommitted.push(MoveTo::TempChild(temp));
127+ }
128+
129+ /// Are all the traversal's moves committed? That is, are there no moves
130+ /// that have *not* been committed yet?
131+ #[inline]
132+ pub fn is_committed(&self) -> bool {
133+ self.uncommitted.is_empty()
134+ }
135+
136+ /// Commit this traversals moves and return the optimized path from the last
137+ /// commit.
138+ #[inline]
139+ pub fn commit(&mut self) -> Moves {
140+ Moves {
141+ inner: self.uncommitted.drain(..),
142+ }
143+ }
144+
145+ #[inline]
146+ pub fn reset(&mut self) {
147+ self.uncommitted.clear();
148+ }
149+}
150+
151+pub struct Moves<'a> {
152+ inner: std::vec::Drain<'a, MoveTo>,
153+}
154+
155+impl Iterator for Moves<'_> {
156+ type Item = MoveTo;
157+
158+ #[inline]
159+ fn next(&mut self) -> Option<MoveTo> {
160+ self.inner.next()
161+ }
162+}
163+
164+#[cfg(test)]
165+mod tests {
166+ use super::*;
167+
168+ #[test]
169+ fn test_traversal() {
170+ fn t<F>(f: F) -> Box<dyn FnMut(&mut Traversal)>
171+ where
172+ F: 'static + FnMut(&mut Traversal),
173+ {
174+ Box::new(f) as _
175+ }
176+
177+ for (mut traverse, expected_moves) in vec![
178+ (
179+ t(|t| {
180+ t.down();
181+ }),
182+ vec![MoveTo::Child(0)],
183+ ),
184+ (
185+ t(|t| {
186+ t.up();
187+ }),
188+ vec![MoveTo::Parent],
189+ ),
190+ (
191+ t(|t| {
192+ t.sibling(42);
193+ }),
194+ vec![MoveTo::Sibling(42)],
195+ ),
196+ (
197+ t(|t| {
198+ t.down();
199+ t.up();
200+ }),
201+ vec![],
202+ ),
203+ (
204+ t(|t| {
205+ t.down();
206+ t.sibling(2);
207+ t.up();
208+ }),
209+ vec![],
210+ ),
211+ (
212+ t(|t| {
213+ t.down();
214+ t.sibling(3);
215+ }),
216+ vec![MoveTo::Child(3)],
217+ ),
218+ (
219+ t(|t| {
220+ t.down();
221+ t.sibling(4);
222+ t.sibling(8);
223+ }),
224+ vec![MoveTo::Child(8)],
225+ ),
226+ (
227+ t(|t| {
228+ t.sibling(1);
229+ t.sibling(1);
230+ }),
231+ vec![MoveTo::Sibling(1)],
232+ ),
233+ (
234+ t(|t| {
235+ t.reverse_sibling(3);
236+ }),
237+ vec![MoveTo::ReverseSibling(3)],
238+ ),
239+ (
240+ t(|t| {
241+ t.down();
242+ t.reverse_sibling(3);
243+ }),
244+ vec![MoveTo::ReverseChild(3)],
245+ ),
246+ (
247+ t(|t| {
248+ t.down();
249+ t.reverse_sibling(3);
250+ t.up();
251+ }),
252+ vec![],
253+ ),
254+ (
255+ t(|t| {
256+ t.down();
257+ t.reverse_sibling(3);
258+ t.reverse_sibling(6);
259+ }),
260+ vec![MoveTo::ReverseChild(6)],
261+ ),
262+ (
263+ t(|t| {
264+ t.up();
265+ t.reverse_sibling(3);
266+ t.reverse_sibling(6);
267+ }),
268+ vec![MoveTo::Parent, MoveTo::ReverseSibling(6)],
269+ ),
270+ (
271+ t(|t| {
272+ t.up();
273+ t.sibling(3);
274+ t.sibling(6);
275+ }),
276+ vec![MoveTo::Parent, MoveTo::Sibling(6)],
277+ ),
278+ (
279+ t(|t| {
280+ t.sibling(3);
281+ t.sibling(6);
282+ t.up();
283+ }),
284+ vec![MoveTo::Parent],
285+ ),
286+ (
287+ t(|t| {
288+ t.reverse_sibling(3);
289+ t.reverse_sibling(6);
290+ t.up();
291+ }),
292+ vec![MoveTo::Parent],
293+ ),
294+ (
295+ t(|t| {
296+ t.down();
297+ t.down_to_temp(3);
298+ }),
299+ vec![MoveTo::Child(0), MoveTo::TempChild(3)],
300+ ),
301+ (
302+ t(|t| {
303+ t.down_to_temp(3);
304+ t.sibling(5);
305+ }),
306+ vec![MoveTo::Child(5)],
307+ ),
308+ (
309+ t(|t| {
310+ t.down_to_temp(3);
311+ t.reverse_sibling(5);
312+ }),
313+ vec![MoveTo::ReverseChild(5)],
314+ ),
315+ (
316+ t(|t| {
317+ t.down_to_temp(3);
318+ t.up();
319+ }),
320+ vec![],
321+ ),
322+ (
323+ t(|t| {
324+ t.sibling(2);
325+ t.up();
326+ t.down_to_temp(3);
327+ }),
328+ vec![MoveTo::Parent, MoveTo::TempChild(3)],
329+ ),
330+ (
331+ t(|t| {
332+ t.up();
333+ t.down_to_temp(3);
334+ }),
335+ vec![MoveTo::Parent, MoveTo::TempChild(3)],
336+ ),
337+ ] {
338+ let mut traversal = Traversal::new();
339+ traverse(&mut traversal);
340+ let actual_moves: Vec<_> = traversal.commit().collect();
341+ assert_eq!(actual_moves, expected_moves);
342+ }
343+ }
344+}
src/diff.rsnew file+1125
@@ -0,0 +1,1125 @@
1+use crate::{
2+ cached_set::{CacheId, CachedSet},
3+ change_list::ChangeListBuilder,
4+ events::EventsRegistry,
5+ node::{Attribute, ElementNode, Listener, Node, NodeKind, TextNode},
6+};
7+use fxhash::{FxHashMap, FxHashSet};
8+use std::cmp::Ordering;
9+use std::u32;
10+use wasm_bindgen::UnwrapThrowExt;
11+
12+// Diff the `old` node with the `new` node. Emits instructions to modify a
13+// physical DOM node that reflects `old` into something that reflects `new`.
14+//
15+// Upon entry to this function, the physical DOM node must be on the top of the
16+// change list stack:
17+//
18+// [... node]
19+//
20+// The change list stack is in the same state when this function exits.
21+pub(crate) fn diff(
22+ cached_set: &CachedSet,
23+ change_list: &mut ChangeListBuilder,
24+ registry: &mut EventsRegistry,
25+ old: &Node,
26+ new: &Node,
27+ cached_roots: &mut FxHashSet<CacheId>,
28+) {
29+ match (&new.kind, &old.kind) {
30+ (
31+ &NodeKind::Text(TextNode { text: new_text }),
32+ &NodeKind::Text(TextNode { text: old_text }),
33+ ) => {
34+ if new_text != old_text {
35+ change_list.commit_traversal();
36+ change_list.set_text(new_text);
37+ }
38+ }
39+
40+ (&NodeKind::Text(_), &NodeKind::Element(_)) => {
41+ change_list.commit_traversal();
42+ create(cached_set, change_list, registry, new, cached_roots);
43+ registry.remove_subtree(&old);
44+ change_list.replace_with();
45+ }
46+
47+ (&NodeKind::Element(_), &NodeKind::Text(_)) => {
48+ change_list.commit_traversal();
49+ create(cached_set, change_list, registry, new, cached_roots);
50+ // Note: text nodes cannot have event listeners, so we don't need to
51+ // remove the old node's listeners from our registry her.
52+ change_list.replace_with();
53+ }
54+
55+ (
56+ &NodeKind::Element(ElementNode {
57+ key: _,
58+ tag_name: new_tag_name,
59+ listeners: new_listeners,
60+ attributes: new_attributes,
61+ children: new_children,
62+ namespace: new_namespace,
63+ }),
64+ &NodeKind::Element(ElementNode {
65+ key: _,
66+ tag_name: old_tag_name,
67+ listeners: old_listeners,
68+ attributes: old_attributes,
69+ children: old_children,
70+ namespace: old_namespace,
71+ }),
72+ ) => {
73+ if new_tag_name != old_tag_name || new_namespace != old_namespace {
74+ change_list.commit_traversal();
75+ create(cached_set, change_list, registry, new, cached_roots);
76+ registry.remove_subtree(&old);
77+ change_list.replace_with();
78+ return;
79+ }
80+ diff_listeners(change_list, registry, old_listeners, new_listeners);
81+ diff_attributes(change_list, old_attributes, new_attributes, new_namespace.is_some());
82+ diff_children(
83+ cached_set,
84+ change_list,
85+ registry,
86+ old_children,
87+ new_children,
88+ cached_roots,
89+ );
90+ }
91+
92+ // Both the new and old nodes are cached.
93+ (&NodeKind::Cached(ref new), &NodeKind::Cached(ref old)) => {
94+ cached_roots.insert(new.id);
95+
96+ if new.id == old.id {
97+ // This is the same cached node, so nothing has changed!
98+ return;
99+ }
100+
101+ let (new, new_template) = cached_set.get(new.id);
102+ let (old, old_template) = cached_set.get(old.id);
103+ if new_template == old_template {
104+ // If they are both using the same template, then just diff the
105+ // subtrees.
106+ diff(cached_set, change_list, registry, old, new, cached_roots);
107+ } else {
108+ // Otherwise, they are probably different enough that
109+ // re-constructing the subtree from scratch should be faster.
110+ // This doubly holds true if we have a new template.
111+ change_list.commit_traversal();
112+ create_and_replace(
113+ cached_set,
114+ change_list,
115+ registry,
116+ new_template,
117+ old,
118+ new,
119+ cached_roots,
120+ );
121+ }
122+ }
123+
124+ // New cached node when the old node was not cached. In this scenario,
125+ // we assume that they are pretty different, and it isn't worth diffing
126+ // the subtrees, so we just create the new cached node afresh.
127+ (&NodeKind::Cached(ref c), _) => {
128+ change_list.commit_traversal();
129+ cached_roots.insert(c.id);
130+ let (new, new_template) = cached_set.get(c.id);
131+ create_and_replace(
132+ cached_set,
133+ change_list,
134+ registry,
135+ new_template,
136+ old,
137+ new,
138+ cached_roots,
139+ );
140+ }
141+
142+ // Old cached node and new non-cached node. Again, assume that they are
143+ // probably pretty different and create the new non-cached node afresh.
144+ (_, &NodeKind::Cached(_)) => {
145+ change_list.commit_traversal();
146+ create(cached_set, change_list, registry, new, cached_roots);
147+ registry.remove_subtree(&old);
148+ change_list.replace_with();
149+ }
150+ }
151+}
152+
153+// Diff event listeners between `old` and `new`.
154+//
155+// The listeners' node must be on top of the change list stack:
156+//
157+// [... node]
158+//
159+// The change list stack is left unchanged.
160+fn diff_listeners(
161+ change_list: &mut ChangeListBuilder,
162+ registry: &mut EventsRegistry,
163+ old: &[Listener],
164+ new: &[Listener],
165+) {
166+ if !old.is_empty() || !new.is_empty() {
167+ change_list.commit_traversal();
168+ }
169+
170+ 'outer1: for new_l in new {
171+ unsafe {
172+ // Safety relies on removing `new_l` from the registry manually at
173+ // the end of its lifetime. This happens below in the `'outer2`
174+ // loop, and elsewhere in diffing when removing old dom trees.
175+ registry.add(new_l);
176+ }
177+
178+ for old_l in old {
179+ if new_l.event == old_l.event {
180+ change_list.update_event_listener(new_l);
181+ continue 'outer1;
182+ }
183+ }
184+
185+ change_list.new_event_listener(new_l);
186+ }
187+
188+ 'outer2: for old_l in old {
189+ registry.remove(old_l);
190+
191+ for new_l in new {
192+ if new_l.event == old_l.event {
193+ continue 'outer2;
194+ }
195+ }
196+ change_list.remove_event_listener(old_l.event);
197+ }
198+}
199+
200+// Diff a node's attributes.
201+//
202+// The attributes' node must be on top of the change list stack:
203+//
204+// [... node]
205+//
206+// The change list stack is left unchanged.
207+fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute], is_namespaced: bool) {
208+ // Do O(n^2) passes to add/update and remove attributes, since
209+ // there are almost always very few attributes.
210+ 'outer: for new_attr in new {
211+ if new_attr.is_volatile() {
212+ change_list.commit_traversal();
213+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
214+ } else {
215+ for old_attr in old {
216+ if old_attr.name == new_attr.name {
217+ if old_attr.value != new_attr.value {
218+ change_list.commit_traversal();
219+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
220+ }
221+ continue 'outer;
222+ }
223+ }
224+
225+ change_list.commit_traversal();
226+ change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced);
227+ }
228+ }
229+
230+ 'outer2: for old_attr in old {
231+ for new_attr in new {
232+ if old_attr.name == new_attr.name {
233+ continue 'outer2;
234+ }
235+ }
236+
237+ change_list.commit_traversal();
238+ change_list.remove_attribute(old_attr.name);
239+ }
240+}
241+
242+// Diff the given set of old and new children.
243+//
244+// The parent must be on top of the change list stack when this function is
245+// entered:
246+//
247+// [... parent]
248+//
249+// the change list stack is in the same state when this function returns.
250+fn diff_children(
251+ cached_set: &CachedSet,
252+ change_list: &mut ChangeListBuilder,
253+ registry: &mut EventsRegistry,
254+ old: &[Node],
255+ new: &[Node],
256+ cached_roots: &mut FxHashSet<CacheId>,
257+) {
258+ if new.is_empty() {
259+ if !old.is_empty() {
260+ change_list.commit_traversal();
261+ remove_all_children(change_list, registry, old);
262+ }
263+ return;
264+ }
265+
266+ if new.len() == 1 {
267+ match (old.first(), &new[0]) {
268+ (
269+ Some(&Node {
270+ kind: NodeKind::Text(TextNode { text: old_text }),
271+ }),
272+ &Node {
273+ kind: NodeKind::Text(TextNode { text: new_text }),
274+ },
275+ ) if old_text == new_text => {
276+ // Don't take this fast path...
277+ }
278+ (
279+ _,
280+ &Node {
281+ kind: NodeKind::Text(TextNode { text }),
282+ },
283+ ) => {
284+ change_list.commit_traversal();
285+ change_list.set_text(text);
286+ for o in old {
287+ registry.remove_subtree(o);
288+ }
289+ return;
290+ }
291+ (_, _) => {}
292+ }
293+ }
294+
295+ if old.is_empty() {
296+ if !new.is_empty() {
297+ change_list.commit_traversal();
298+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
299+ }
300+ return;
301+ }
302+
303+ let new_is_keyed = new[0].key().is_some();
304+ let old_is_keyed = old[0].key().is_some();
305+
306+ debug_assert!(
307+ new.iter().all(|n| n.key().is_some() == new_is_keyed),
308+ "all siblings must be keyed or all siblings must be non-keyed"
309+ );
310+ debug_assert!(
311+ old.iter().all(|o| o.key().is_some() == old_is_keyed),
312+ "all siblings must be keyed or all siblings must be non-keyed"
313+ );
314+
315+ if new_is_keyed && old_is_keyed {
316+ let t = change_list.next_temporary();
317+ diff_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
318+ change_list.set_next_temporary(t);
319+ } else {
320+ diff_non_keyed_children(cached_set, change_list, registry, old, new, cached_roots);
321+ }
322+}
323+
324+// Diffing "keyed" children.
325+//
326+// With keyed children, we care about whether we delete, move, or create nodes
327+// versus mutate existing nodes in place. Presumably there is some sort of CSS
328+// transition animation that makes the virtual DOM diffing algorithm
329+// observable. By specifying keys for nodes, we know which virtual DOM nodes
330+// must reuse (or not reuse) the same physical DOM nodes.
331+//
332+// This is loosely based on Inferno's keyed patching implementation. However, we
333+// have to modify the algorithm since we are compiling the diff down into change
334+// list instructions that will be executed later, rather than applying the
335+// changes to the DOM directly as we compare virtual DOMs.
336+//
337+// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
338+//
339+// When entering this function, the parent must be on top of the change list
340+// stack:
341+//
342+// [... parent]
343+//
344+// Upon exiting, the change list stack is in the same state.
345+fn diff_keyed_children(
346+ cached_set: &CachedSet,
347+ change_list: &mut ChangeListBuilder,
348+ registry: &mut EventsRegistry,
349+ old: &[Node],
350+ new: &[Node],
351+ cached_roots: &mut FxHashSet<CacheId>,
352+) {
353+ if cfg!(debug_assertions) {
354+ let mut keys = FxHashSet::default();
355+ let mut assert_unique_keys = |children: &[Node]| {
356+ keys.clear();
357+ for child in children {
358+ let key = child.key();
359+ debug_assert!(
360+ key.is_some(),
361+ "if any sibling is keyed, all siblings must be keyed"
362+ );
363+ keys.insert(key);
364+ }
365+ debug_assert_eq!(
366+ children.len(),
367+ keys.len(),
368+ "keyed siblings must each have a unique key"
369+ );
370+ };
371+ assert_unique_keys(old);
372+ assert_unique_keys(new);
373+ }
374+
375+ // First up, we diff all the nodes with the same key at the beginning of the
376+ // children.
377+ //
378+ // `shared_prefix_count` is the count of how many nodes at the start of
379+ // `new` and `old` share the same keys.
380+ let shared_prefix_count =
381+ match diff_keyed_prefix(cached_set, change_list, registry, old, new, cached_roots) {
382+ KeyedPrefixResult::Finished => return,
383+ KeyedPrefixResult::MoreWorkToDo(count) => count,
384+ };
385+
386+ // Next, we find out how many of the nodes at the end of the children have
387+ // the same key. We do _not_ diff them yet, since we want to emit the change
388+ // list instructions such that they can be applied in a single pass over the
389+ // DOM. Instead, we just save this information for later.
390+ //
391+ // `shared_suffix_count` is the count of how many nodes at the end of `new`
392+ // and `old` share the same keys.
393+ let shared_suffix_count = old[shared_prefix_count..]
394+ .iter()
395+ .rev()
396+ .zip(new[shared_prefix_count..].iter().rev())
397+ .take_while(|&(old, new)| old.key() == new.key())
398+ .count();
399+
400+ let old_shared_suffix_start = old.len() - shared_suffix_count;
401+ let new_shared_suffix_start = new.len() - shared_suffix_count;
402+
403+ // Ok, we now hopefully have a smaller range of children in the middle
404+ // within which to re-order nodes with the same keys, remove old nodes with
405+ // now-unused keys, and create new nodes with fresh keys.
406+ diff_keyed_middle(
407+ cached_set,
408+ change_list,
409+ registry,
410+ &old[shared_prefix_count..old_shared_suffix_start],
411+ &new[shared_prefix_count..new_shared_suffix_start],
412+ cached_roots,
413+ shared_prefix_count,
414+ shared_suffix_count,
415+ old_shared_suffix_start,
416+ );
417+
418+ // Finally, diff the nodes at the end of `old` and `new` that share keys.
419+ let old_suffix = &old[old_shared_suffix_start..];
420+ let new_suffix = &new[new_shared_suffix_start..];
421+ debug_assert_eq!(old_suffix.len(), new_suffix.len());
422+ if !old_suffix.is_empty() {
423+ diff_keyed_suffix(
424+ cached_set,
425+ change_list,
426+ registry,
427+ old_suffix,
428+ new_suffix,
429+ cached_roots,
430+ new_shared_suffix_start,
431+ );
432+ }
433+}
434+
435+enum KeyedPrefixResult {
436+ // Fast path: we finished diffing all the children just by looking at the
437+ // prefix of shared keys!
438+ Finished,
439+ // There is more diffing work to do. Here is a count of how many children at
440+ // the beginning of `new` and `old` we already processed.
441+ MoreWorkToDo(usize),
442+}
443+
444+// Diff the prefix of children in `new` and `old` that share the same keys in
445+// the same order.
446+//
447+// Upon entry of this function, the change list stack must be:
448+//
449+// [... parent]
450+//
451+// Upon exit, the change list stack is the same.
452+fn diff_keyed_prefix(
453+ cached_set: &CachedSet,
454+ change_list: &mut ChangeListBuilder,
455+ registry: &mut EventsRegistry,
456+ old: &[Node],
457+ new: &[Node],
458+ cached_roots: &mut FxHashSet<CacheId>,
459+) -> KeyedPrefixResult {
460+ change_list.go_down();
461+ let mut shared_prefix_count = 0;
462+
463+ for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() {
464+ if old.key() != new.key() {
465+ break;
466+ }
467+
468+ change_list.go_to_sibling(i);
469+ diff(cached_set, change_list, registry, old, new, cached_roots);
470+ shared_prefix_count += 1;
471+ }
472+
473+ // If that was all of the old children, then create and append the remaining
474+ // new children and we're finished.
475+ if shared_prefix_count == old.len() {
476+ change_list.go_up();
477+ change_list.commit_traversal();
478+ create_and_append_children(
479+ cached_set,
480+ change_list,
481+ registry,
482+ &new[shared_prefix_count..],
483+ cached_roots,
484+ );
485+ return KeyedPrefixResult::Finished;
486+ }
487+
488+ // And if that was all of the new children, then remove all of the remaining
489+ // old children and we're finished.
490+ if shared_prefix_count == new.len() {
491+ change_list.go_to_sibling(shared_prefix_count);
492+ change_list.commit_traversal();
493+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
494+ return KeyedPrefixResult::Finished;
495+ }
496+
497+ change_list.go_up();
498+ KeyedPrefixResult::MoreWorkToDo(shared_prefix_count)
499+}
500+
501+// The most-general, expensive code path for keyed children diffing.
502+//
503+// We find the longest subsequence within `old` of children that are relatively
504+// ordered the same way in `new` (via finding a longest-increasing-subsequence
505+// of the old child's index within `new`). The children that are elements of
506+// this subsequence will remain in place, minimizing the number of DOM moves we
507+// will have to do.
508+//
509+// Upon entry to this function, the change list stack must be:
510+//
511+// [... parent]
512+//
513+// Upon exit from this function, it will be restored to that same state.
514+fn diff_keyed_middle(
515+ cached_set: &CachedSet,
516+ change_list: &mut ChangeListBuilder,
517+ registry: &mut EventsRegistry,
518+ old: &[Node],
519+ mut new: &[Node],
520+ cached_roots: &mut FxHashSet<CacheId>,
521+ shared_prefix_count: usize,
522+ shared_suffix_count: usize,
523+ old_shared_suffix_start: usize,
524+) {
525+ // Should have already diffed the shared-key prefixes and suffixes.
526+ debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key()));
527+ debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key()));
528+
529+ // The algorithm below relies upon using `u32::MAX` as a sentinel
530+ // value, so if we have that many new nodes, it won't work. This
531+ // check is a bit academic (hence only enabled in debug), since
532+ // wasm32 doesn't have enough address space to hold that many nodes
533+ // in memory.
534+ debug_assert!(new.len() < u32::MAX as usize);
535+
536+ // Map from each `old` node's key to its index within `old`.
537+ let mut old_key_to_old_index = FxHashMap::default();
538+ old_key_to_old_index.reserve(old.len());
539+ old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i)));
540+
541+ // The set of shared keys between `new` and `old`.
542+ let mut shared_keys = FxHashSet::default();
543+ // Map from each index in `new` to the index of the node in `old` that
544+ // has the same key.
545+ let mut new_index_to_old_index = Vec::with_capacity(new.len());
546+ new_index_to_old_index.extend(new.iter().map(|n| {
547+ let key = n.key();
548+ if let Some(&i) = old_key_to_old_index.get(&key) {
549+ shared_keys.insert(key);
550+ i
551+ } else {
552+ u32::MAX as usize
553+ }
554+ }));
555+
556+ // If none of the old keys are reused by the new children, then we
557+ // remove all the remaining old children and create the new children
558+ // afresh.
559+ if shared_suffix_count == 0 && shared_keys.is_empty() {
560+ if shared_prefix_count == 0 {
561+ change_list.commit_traversal();
562+ remove_all_children(change_list, registry, old);
563+ } else {
564+ change_list.go_down_to_child(shared_prefix_count);
565+ change_list.commit_traversal();
566+ remove_self_and_next_siblings(change_list, registry, &old[shared_prefix_count..]);
567+ }
568+ create_and_append_children(cached_set, change_list, registry, new, cached_roots);
569+ return;
570+ }
571+
572+ // Save each of the old children whose keys are reused in the new
573+ // children.
574+ let mut old_index_to_temp = vec![u32::MAX; old.len()];
575+ let mut start = 0;
576+ loop {
577+ let end = (start..old.len())
578+ .find(|&i| {
579+ let key = old[i].key();
580+ !shared_keys.contains(&key)
581+ })
582+ .unwrap_or(old.len());
583+
584+ if end - start > 0 {
585+ change_list.commit_traversal();
586+ let mut t = change_list.save_children_to_temporaries(
587+ shared_prefix_count + start,
588+ shared_prefix_count + end,
589+ );
590+ for i in start..end {
591+ old_index_to_temp[i] = t;
592+ t += 1;
593+ }
594+ }
595+
596+ debug_assert!(end <= old.len());
597+ if end == old.len() {
598+ break;
599+ } else {
600+ start = end + 1;
601+ }
602+ }
603+
604+ // Remove any old children whose keys were not reused in the new
605+ // children. Remove from the end first so that we don't mess up indices.
606+ let mut removed_count = 0;
607+ for (i, old_child) in old.iter().enumerate().rev() {
608+ if !shared_keys.contains(&old_child.key()) {
609+ registry.remove_subtree(old_child);
610+ change_list.commit_traversal();
611+ change_list.remove_child(i + shared_prefix_count);
612+ removed_count += 1;
613+ }
614+ }
615+
616+ // If there aren't any more new children, then we are done!
617+ if new.is_empty() {
618+ return;
619+ }
620+
621+ // The longest increasing subsequence within `new_index_to_old_index`. This
622+ // is the longest sequence on DOM nodes in `old` that are relatively ordered
623+ // correctly within `new`. We will leave these nodes in place in the DOM,
624+ // and only move nodes that are not part of the LIS. This results in the
625+ // maximum number of DOM nodes left in place, AKA the minimum number of DOM
626+ // nodes moved.
627+ let mut new_index_is_in_lis = FxHashSet::default();
628+ new_index_is_in_lis.reserve(new_index_to_old_index.len());
629+ let mut predecessors = vec![0; new_index_to_old_index.len()];
630+ let mut starts = vec![0; new_index_to_old_index.len()];
631+ longest_increasing_subsequence::lis_with(
632+ &new_index_to_old_index,
633+ &mut new_index_is_in_lis,
634+ |a, b| a < b,
635+ &mut predecessors,
636+ &mut starts,
637+ );
638+
639+ // Now we will iterate from the end of the new children back to the
640+ // beginning, diffing old children we are reusing and if they aren't in the
641+ // LIS moving them to their new destination, or creating new children. Note
642+ // that iterating in reverse order lets us use `Node.prototype.insertBefore`
643+ // to move/insert children.
644+ //
645+ // But first, we ensure that we have a child on the change list stack that
646+ // we can `insertBefore`. We handle this once before looping over `new`
647+ // children, so that we don't have to keep checking on every loop iteration.
648+ if shared_suffix_count > 0 {
649+ // There is a shared suffix after these middle children. We will be
650+ // inserting before that shared suffix, so add the first child of that
651+ // shared suffix to the change list stack.
652+ //
653+ // [... parent]
654+ change_list.go_down_to_child(old_shared_suffix_start - removed_count);
655+ // [... parent first_child_of_shared_suffix]
656+ } else {
657+ // There is no shared suffix coming after these middle children.
658+ // Therefore we have to process the last child in `new` and move it to
659+ // the end of the parent's children if it isn't already there.
660+ let last_index = new.len() - 1;
661+ let last = new.last().unwrap_throw();
662+ new = &new[..new.len() - 1];
663+ if shared_keys.contains(&last.key()) {
664+ let old_index = new_index_to_old_index[last_index];
665+ let temp = old_index_to_temp[old_index];
666+ // [... parent]
667+ change_list.go_down_to_temp_child(temp);
668+ // [... parent last]
669+ diff(
670+ cached_set,
671+ change_list,
672+ registry,
673+ &old[old_index],
674+ last,
675+ cached_roots,
676+ );
677+ if new_index_is_in_lis.contains(&last_index) {
678+ // Don't move it, since it is already where it needs to be.
679+ } else {
680+ change_list.commit_traversal();
681+ // [... parent last]
682+ change_list.append_child();
683+ // [... parent]
684+ change_list.go_down_to_temp_child(temp);
685+ // [... parent last]
686+ }
687+ } else {
688+ change_list.commit_traversal();
689+ // [... parent]
690+ create(cached_set, change_list, registry, last, cached_roots);
691+ // [... parent last]
692+ change_list.append_child();
693+ // [... parent]
694+ change_list.go_down_to_reverse_child(0);
695+ // [... parent last]
696+ }
697+ }
698+
699+ for (new_index, new_child) in new.iter().enumerate().rev() {
700+ let old_index = new_index_to_old_index[new_index];
701+ if old_index == u32::MAX as usize {
702+ debug_assert!(!shared_keys.contains(&new_child.key()));
703+ change_list.commit_traversal();
704+ // [... parent successor]
705+ create(cached_set, change_list, registry, new_child, cached_roots);
706+ // [... parent successor new_child]
707+ change_list.insert_before();
708+ // [... parent new_child]
709+ } else {
710+ debug_assert!(shared_keys.contains(&new_child.key()));
711+ let temp = old_index_to_temp[old_index];
712+ debug_assert_ne!(temp, u32::MAX);
713+
714+ if new_index_is_in_lis.contains(&new_index) {
715+ // [... parent successor]
716+ change_list.go_to_temp_sibling(temp);
717+ // [... parent new_child]
718+ } else {
719+ change_list.commit_traversal();
720+ // [... parent successor]
721+ change_list.push_temporary(temp);
722+ // [... parent successor new_child]
723+ change_list.insert_before();
724+ // [... parent new_child]
725+ }
726+
727+ diff(
728+ cached_set,
729+ change_list,
730+ registry,
731+ &old[old_index],
732+ new_child,
733+ cached_roots,
734+ );
735+ }
736+ }
737+
738+ // [... parent child]
739+ change_list.go_up();
740+ // [... parent]
741+}
742+
743+// Diff the suffix of keyed children that share the same keys in the same order.
744+//
745+// The parent must be on the change list stack when we enter this function:
746+//
747+// [... parent]
748+//
749+// When this function exits, the change list stack remains the same.
750+fn diff_keyed_suffix(
751+ cached_set: &CachedSet,
752+ change_list: &mut ChangeListBuilder,
753+ registry: &mut EventsRegistry,
754+ old: &[Node],
755+ new: &[Node],
756+ cached_roots: &mut FxHashSet<CacheId>,
757+ new_shared_suffix_start: usize,
758+) {
759+ debug_assert_eq!(old.len(), new.len());
760+ debug_assert!(!old.is_empty());
761+
762+ // [... parent]
763+ change_list.go_down();
764+ // [... parent new_child]
765+
766+ for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() {
767+ change_list.go_to_sibling(new_shared_suffix_start + i);
768+ diff(
769+ cached_set,
770+ change_list,
771+ registry,
772+ old_child,
773+ new_child,
774+ cached_roots,
775+ );
776+ }
777+
778+ // [... parent]
779+ change_list.go_up();
780+}
781+
782+// Diff children that are not keyed.
783+//
784+// The parent must be on the top of the change list stack when entering this
785+// function:
786+//
787+// [... parent]
788+//
789+// the change list stack is in the same state when this function returns.
790+fn diff_non_keyed_children(
791+ cached_set: &CachedSet,
792+ change_list: &mut ChangeListBuilder,
793+ registry: &mut EventsRegistry,
794+ old: &[Node],
795+ new: &[Node],
796+ cached_roots: &mut FxHashSet<CacheId>,
797+) {
798+ // Handled these cases in `diff_children` before calling this function.
799+ debug_assert!(!new.is_empty());
800+ debug_assert!(!old.is_empty());
801+
802+ // [... parent]
803+ change_list.go_down();
804+ // [... parent child]
805+
806+ for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() {
807+ // [... parent prev_child]
808+ change_list.go_to_sibling(i);
809+ // [... parent this_child]
810+ diff(
811+ cached_set,
812+ change_list,
813+ registry,
814+ old_child,
815+ new_child,
816+ cached_roots,
817+ );
818+ }
819+
820+ match old.len().cmp(&new.len()) {
821+ Ordering::Greater => {
822+ // [... parent prev_child]
823+ change_list.go_to_sibling(new.len());
824+ // [... parent first_child_to_remove]
825+ change_list.commit_traversal();
826+ remove_self_and_next_siblings(change_list, registry, &old[new.len()..]);
827+ // [... parent]
828+ }
829+ Ordering::Less => {
830+ // [... parent last_child]
831+ change_list.go_up();
832+ // [... parent]
833+ change_list.commit_traversal();
834+ create_and_append_children(
835+ cached_set,
836+ change_list,
837+ registry,
838+ &new[old.len()..],
839+ cached_roots,
840+ );
841+ }
842+ Ordering::Equal => {
843+ // [... parent child]
844+ change_list.go_up();
845+ // [... parent]
846+ }
847+ }
848+}
849+
850+// Create the given children and append them to the parent node.
851+//
852+// The parent node must currently be on top of the change list stack:
853+//
854+// [... parent]
855+//
856+// When this function returns, the change list stack is in the same state.
857+fn create_and_append_children(
858+ cached_set: &CachedSet,
859+ change_list: &mut ChangeListBuilder,
860+ registry: &mut EventsRegistry,
861+ new: &[Node],
862+ cached_roots: &mut FxHashSet<CacheId>,
863+) {
864+ debug_assert!(change_list.traversal_is_committed());
865+ for child in new {
866+ create(cached_set, change_list, registry, child, cached_roots);
867+ change_list.append_child();
868+ }
869+}
870+
871+// Remove all of a node's children.
872+//
873+// The change list stack must have this shape upon entry to this function:
874+//
875+// [... parent]
876+//
877+// When this function returns, the change list stack is in the same state.
878+fn remove_all_children(
879+ change_list: &mut ChangeListBuilder,
880+ registry: &mut EventsRegistry,
881+ old: &[Node],
882+) {
883+ debug_assert!(change_list.traversal_is_committed());
884+ for child in old {
885+ registry.remove_subtree(child);
886+ }
887+ // Fast way to remove all children: set the node's textContent to an empty
888+ // string.
889+ change_list.set_text("");
890+}
891+
892+// Remove the current child and all of its following siblings.
893+//
894+// The change list stack must have this shape upon entry to this function:
895+//
896+// [... parent child]
897+//
898+// After the function returns, the child is no longer on the change list stack:
899+//
900+// [... parent]
901+fn remove_self_and_next_siblings(
902+ change_list: &mut ChangeListBuilder,
903+ registry: &mut EventsRegistry,
904+ old: &[Node],
905+) {
906+ debug_assert!(change_list.traversal_is_committed());
907+ for child in old {
908+ registry.remove_subtree(child);
909+ }
910+ change_list.remove_self_and_next_siblings();
911+}
912+
913+// Emit instructions to create the given virtual node.
914+//
915+// The change list stack may have any shape upon entering this function:
916+//
917+// [...]
918+//
919+// When this function returns, the new node is on top of the change list stack:
920+//
921+// [... node]
922+fn create(
923+ cached_set: &CachedSet,
924+ change_list: &mut ChangeListBuilder,
925+ registry: &mut EventsRegistry,
926+ node: &Node,
927+ cached_roots: &mut FxHashSet<CacheId>,
928+) {
929+ debug_assert!(change_list.traversal_is_committed());
930+ match node.kind {
931+ NodeKind::Text(TextNode { text }) => {
932+ change_list.create_text_node(text);
933+ }
934+ NodeKind::Element(&ElementNode {
935+ key: _,
936+ tag_name,
937+ listeners,
938+ attributes,
939+ children,
940+ namespace,
941+ }) => {
942+ if let Some(namespace) = namespace {
943+ change_list.create_element_ns(tag_name, namespace);
944+ } else {
945+ change_list.create_element(tag_name);
946+ }
947+
948+ for l in listeners {
949+ unsafe {
950+ registry.add(l);
951+ }
952+ change_list.new_event_listener(l);
953+ }
954+
955+ for attr in attributes {
956+ change_list.set_attribute(&attr.name, &attr.value, namespace.is_some());
957+ }
958+
959+ // Fast path: if there is a single text child, it is faster to
960+ // create-and-append the text node all at once via setting the
961+ // parent's `textContent` in a single change list instruction than
962+ // to emit three instructions to (1) create a text node, (2) set its
963+ // text content, and finally (3) append the text node to this
964+ // parent.
965+ if children.len() == 1 {
966+ if let Node {
967+ kind: NodeKind::Text(TextNode { text }),
968+ } = children[0]
969+ {
970+ change_list.set_text(text);
971+ return;
972+ }
973+ }
974+
975+ for child in children {
976+ create(cached_set, change_list, registry, child, cached_roots);
977+ change_list.append_child();
978+ }
979+ }
980+ NodeKind::Cached(ref c) => {
981+ cached_roots.insert(c.id);
982+ let (node, template) = cached_set.get(c.id);
983+ if let Some(template) = template {
984+ create_with_template(
985+ cached_set,
986+ change_list,
987+ registry,
988+ template,
989+ node,
990+ cached_roots,
991+ );
992+ } else {
993+ create(cached_set, change_list, registry, node, cached_roots);
994+ }
995+ }
996+ }
997+}
998+
999+// Get or create the template.
1000+//
1001+// Upon entering this function the change list stack may be in any shape:
1002+//
1003+// [...]
1004+//
1005+// When this function returns, it leaves a freshly cloned copy of the template
1006+// on the top of the change list stack:
1007+//
1008+// [... template]
1009+#[inline]
1010+fn get_or_create_template<'a>(
1011+ cached_set: &'a CachedSet,
1012+ change_list: &mut ChangeListBuilder,
1013+ registry: &mut EventsRegistry,
1014+ cached_roots: &mut FxHashSet<CacheId>,
1015+ template_id: CacheId,
1016+) -> (&'a Node<'a>, bool) {
1017+ let (template, template_template) = cached_set.get(template_id);
1018+ debug_assert!(
1019+ template_template.is_none(),
1020+ "templates should not be templated themselves"
1021+ );
1022+
1023+ // If we haven't already created and saved the physical DOM subtree for this
1024+ // template, do that now.
1025+ if change_list.has_template(template_id) {
1026+ // Clone the template and push it onto the stack.
1027+ //
1028+ // [...]
1029+ change_list.push_template(template_id);
1030+ // [... template]
1031+
1032+ (template, true)
1033+ } else {
1034+ // [...]
1035+ create(cached_set, change_list, registry, template, cached_roots);
1036+ // [... template]
1037+ change_list.save_template(template_id);
1038+ // [... template]
1039+
1040+ (template, false)
1041+ }
1042+}
1043+
1044+fn create_and_replace(
1045+ cached_set: &CachedSet,
1046+ change_list: &mut ChangeListBuilder,
1047+ registry: &mut EventsRegistry,
1048+ new_template: Option<CacheId>,
1049+ old: &Node,
1050+ new: &Node,
1051+ cached_roots: &mut FxHashSet<CacheId>,
1052+) {
1053+ debug_assert!(change_list.traversal_is_committed());
1054+
1055+ if let Some(template_id) = new_template {
1056+ let (template, needs_listeners) =
1057+ get_or_create_template(cached_set, change_list, registry, cached_roots, template_id);
1058+ change_list.replace_with();
1059+
1060+ let mut old_forcing = None;
1061+ if needs_listeners {
1062+ old_forcing = Some(change_list.push_force_new_listeners());
1063+ }
1064+
1065+ diff(
1066+ cached_set,
1067+ change_list,
1068+ registry,
1069+ template,
1070+ new,
1071+ cached_roots,
1072+ );
1073+
1074+ if let Some(old) = old_forcing {
1075+ change_list.pop_force_new_listeners(old);
1076+ }
1077+
1078+ change_list.commit_traversal();
1079+ } else {
1080+ create(cached_set, change_list, registry, new, cached_roots);
1081+ change_list.replace_with();
1082+ }
1083+ registry.remove_subtree(old);
1084+}
1085+
1086+fn create_with_template(
1087+ cached_set: &CachedSet,
1088+ change_list: &mut ChangeListBuilder,
1089+ registry: &mut EventsRegistry,
1090+ template_id: CacheId,
1091+ node: &Node,
1092+ cached_roots: &mut FxHashSet<CacheId>,
1093+) {
1094+ debug_assert!(change_list.traversal_is_committed());
1095+
1096+ // [...]
1097+ let (template, needs_listeners) =
1098+ get_or_create_template(cached_set, change_list, registry, cached_roots, template_id);
1099+ // [... template]
1100+
1101+ // Now diff the node with its template.
1102+ //
1103+ // We must force adding new listeners instead of updating existing ones,
1104+ // since listeners don't get cloned in `cloneNode`.
1105+ let mut old_forcing = None;
1106+ if needs_listeners {
1107+ old_forcing = Some(change_list.push_force_new_listeners());
1108+ }
1109+
1110+ diff(
1111+ cached_set,
1112+ change_list,
1113+ registry,
1114+ template,
1115+ node,
1116+ cached_roots,
1117+ );
1118+
1119+ if let Some(old) = old_forcing {
1120+ change_list.pop_force_new_listeners(old);
1121+ }
1122+
1123+ // Make sure that we come back up to the level we were at originally.
1124+ change_list.commit_traversal();
1125+}
src/events.rs+123−83
@@ -1,98 +1,138 @@
1−use crate::vdom::{VdomInner, VdomWeak};
2−use crate::{Listener, ListenerCallback};
1+use crate::{node::Node, vdom::VdomInner, Listener};
32use std::cell::RefCell;
4−use std::collections::HashMap;
5−use std::fmt;
6−use std::mem;
73use std::rc::{Rc, Weak};
8−use wasm_bindgen::closure::Closure;
9−use wasm_bindgen::prelude::*;
104
11−/// The events registry manages event listeners for a virtual DOM.
12−///
13−/// The events registry is persistent across virtual DOM rendering and double
14−/// buffering.
15−pub(crate) struct EventsRegistry {
16− vdom: Weak<VdomInner>,
17− active: HashMap<(u32, u32), ListenerCallback<'static>>,
18−}
5+cfg_if::cfg_if! {
6+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
7+ #[derive(Debug)]
8+ pub(crate) struct EventsRegistry {}
9+ impl EventsRegistry {
10+ pub(crate) fn new(_vdom: Weak<VdomInner>) -> (
11+ Rc<RefCell<EventsRegistry>>,
12+ crate::EventsTrampoline,
13+ ) {
14+ (Rc::new(RefCell::new(EventsRegistry {})), ())
15+ }
16+ pub(crate) fn remove(&mut self, _listener: &Listener) {}
17+ pub(crate) fn remove_subtree(&mut self, _node: &Node) {}
18+ pub(crate) unsafe fn add<'a>(&mut self, _listener: &'a Listener<'a>) {}
19+ pub(crate) fn clear_active_listeners(&mut self) {}
20+ }
21+ } else {
22+ use crate::{
23+ node::{ElementNode, ListenerCallback, NodeKind},
24+ vdom::VdomWeak,
25+ };
26+ use fxhash::FxHashMap;
27+ use std::fmt;
28+ use std::mem;
29+ use wasm_bindgen::closure::Closure;
30+ use wasm_bindgen::prelude::*;
1931
20−impl fmt::Debug for EventsRegistry {
21− fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
22− f.debug_struct("EventsRegistry")
23− .field("active", &self.active.keys().collect::<Vec<_>>())
24− .finish()
25− }
26−}
32+ /// The events registry manages event listeners for a virtual DOM.
33+ ///
34+ /// The events registry is persistent across virtual DOM rendering and double
35+ /// buffering.
36+ pub(crate) struct EventsRegistry {
37+ vdom: Weak<VdomInner>,
38+ active: FxHashMap<(u32, u32), ListenerCallback<'static>>,
39+ }
2740
28−impl EventsRegistry {
29− /// Construct a new events registry and JS function trampoline that weakly
30− /// holds the new registry and can be used by JS to invoke listeners on the
31− /// Rust side.
32− pub(crate) fn new(
33− vdom: Weak<VdomInner>,
34− ) -> (
35− Rc<RefCell<EventsRegistry>>,
36− Closure<Fn(web_sys::Event, u32, u32)>,
37− ) {
38− let registry = Rc::new(RefCell::new(EventsRegistry {
39− vdom,
40− active: HashMap::new(),
41− }));
41+ impl fmt::Debug for EventsRegistry {
42+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43+ f.debug_struct("EventsRegistry")
44+ .field("active", &self.active.keys().collect::<Vec<_>>())
45+ .finish()
46+ }
47+ }
4248
43− let weak_registry = Rc::downgrade(&registry);
44− let closure = Closure::wrap(Box::new(move |event, a, b| {
45− debug!("Events trampoline invoked with (0x{:x}, 0x{:x})", a, b);
46− debug_assert!(a != 0);
49+ impl EventsRegistry {
50+ /// Construct a new events registry and JS function trampoline that weakly
51+ /// holds the new registry and can be used by JS to invoke listeners on the
52+ /// Rust side.
53+ pub(crate) fn new(
54+ vdom: Weak<VdomInner>,
55+ ) -> (
56+ Rc<RefCell<EventsRegistry>>,
57+ crate::EventsTrampoline,
58+ ) {
59+ let registry = Rc::new(RefCell::new(EventsRegistry {
60+ vdom,
61+ active: FxHashMap::default(),
62+ }));
4763
48− // if the VdomInnerExclusive is keeping this closure alive, then the
49− // VdomInnerExclusive should also be keeping the registry alive
50− let registry = weak_registry.upgrade().unwrap_throw();
51− let registry = registry.borrow();
64+ let weak_registry = Rc::downgrade(&registry);
65+ let closure = Closure::wrap(Box::new(move |event, a, b| {
66+ debug_assert!(a != 0);
5267
53− match registry.active.get(&(a, b)) {
54− None => warn!(
55− "EventsRegistry closure invoked with unknown listener parts: \
56− (0x{:x}, 0x{:x})",
57− a, b
58− ),
59− Some(callback) => {
60− let vdom = registry.vdom.upgrade().expect_throw(
61− "if the registry is still around, then the vdom should still be around",
62− );
63− let vdom_weak = VdomWeak::new(&vdom);
64− let mut vdom = vdom.exclusive.borrow_mut();
65− let component = vdom.component_raw_mut();
66− callback(component, vdom_weak, event);
67− }
68+ // if the VdomInnerExclusive is keeping this closure alive, then the
69+ // VdomInnerExclusive should also be keeping the registry alive
70+ let registry = weak_registry.upgrade().unwrap_throw();
71+ let registry = registry.borrow();
72+
73+ match registry.active.get(&(a, b)) {
74+ None => warn!(
75+ "EventsRegistry closure invoked with unknown listener parts: \
76+ (0x{:x}, 0x{:x})",
77+ a, b
78+ ),
79+ Some(callback) => {
80+ let vdom = registry.vdom.upgrade().expect_throw(
81+ "if the registry is still around, then the vdom should still be around",
82+ );
83+ let vdom_weak = VdomWeak::new(&vdom);
84+ let mut vdom = vdom.exclusive.borrow_mut();
85+ let component = vdom.component_raw_mut();
86+ callback(component, vdom_weak, event);
87+ }
88+ }
89+ }) as Box<dyn Fn(web_sys::Event, u32, u32)>);
90+
91+ (registry, closure)
6892 }
69− }) as Box<Fn(web_sys::Event, u32, u32)>);
7093
71− (registry, closure)
72− }
94+ pub(crate) fn remove(&mut self, listener: &Listener) {
95+ let id = listener.get_callback_parts();
96+ debug_assert!(id.0 != 0);
97+ self.active.remove(&id);
98+ }
99+
100+ pub(crate) fn remove_subtree(&mut self, node: &Node) {
101+ match node.kind {
102+ NodeKind::Cached(_) | NodeKind::Text(_) => {},
103+ NodeKind::Element(&ElementNode {listeners, children, ..}) => {
104+ for l in listeners {
105+ self.remove(l);
106+ }
107+ for child in children {
108+ self.remove_subtree(child)
109+ }
110+ }
111+ }
112+ }
73113
74− /// Add an event listener to the registry, exposing to JS.
75− ///
76− /// # Unsafety
77− ///
78− /// The listener's lifetime is extended to `'static` and it is the caller's
79− /// responsibility to ensure that the listener is not kept in the registry
80− /// after it is dropped. This is typically done via clearing the whole
81− /// registry on each render and repopulating it during diffing.
82− pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
83− let id = listener.get_callback_parts();
84− debug!("EventsRegistry::add(0x{:x}, 0x{:x})", id.0, id.1);
85− debug_assert!(id.0 != 0);
114+ /// Add an event listener to the registry, exposing to JS.
115+ ///
116+ /// # Unsafety
117+ ///
118+ /// The listener's lifetime is extended to `'static` and it is the
119+ /// caller's responsibility to ensure that the listener is not kept
120+ /// in the registry after it is dropped. This is maintained during
121+ /// diffing.
122+ pub(crate) unsafe fn add<'a>(&mut self, listener: &'a Listener<'a>) {
123+ let id = listener.get_callback_parts();
124+ debug_assert!(id.0 != 0);
86125
87− let callback =
88− mem::transmute::<ListenerCallback<'a>, ListenerCallback<'static>>(listener.callback);
89− let old = self.active.insert(id, callback);
90− debug_assert!(old.is_none());
91− }
126+ let callback =
127+ mem::transmute::<ListenerCallback<'a>, ListenerCallback<'static>>(listener.callback);
128+ let old = self.active.insert(id, callback);
129+ debug_assert!(old.is_none());
130+ }
92131
93− /// Clear all event listeners from the registry.
94− pub(crate) fn clear_active_listeners(&mut self) {
95− debug!("EventsRegistry::clear_active_listeners()");
96− self.active.clear();
132+ /// Clear all event listeners from the registry.
133+ pub(crate) fn clear_active_listeners(&mut self) {
134+ self.active.clear();
135+ }
136+ }
97137 }
98138}
src/lib.rs+64−11
@@ -3,7 +3,7 @@
33//! ## Example
44//!
55//! ```no_run
6−//! use dodrio::{bumpalo::Bump, Attribute, Node, Render};
6+//! use dodrio::{bumpalo, Attribute, Node, Render, RenderContext};
77//! use wasm_bindgen::UnwrapThrowExt;
88//!
99//! /// A component that greets someone.
@@ -18,16 +18,14 @@
1818//! }
1919//! }
2020//!
21−//! impl<'who> Render for Hello<'who> {
22−//! fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
23−//! where
24−//! 'a: 'bump,
25−//! {
21+//! impl<'a, 'who> Render<'a> for Hello<'who> {
22+//! fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
2623//! use dodrio::builder::*;
2724//!
28−//! let id = bumpalo::format!(in bump, "hello-{}", self.who);
25+//! let id = bumpalo::format!(in cx.bump, "hello-{}", self.who);
26+//! let who = bumpalo::collections::String::from_str_in(self.who, cx.bump).into_bump_str();
2927//!
30−//! div(bump)
28+//! div(&cx)
3129//! .attr("id", id.into_bump_str())
3230//! .on("click", |root, _vdom, _event| {
3331//! let hello = root.unwrap_mut::<Hello>();
@@ -37,9 +35,9 @@
3735//! })
3836//! .children([
3937//! text("Hello, "),
40−//! strong(bump)
38+//! strong(&cx)
4139//! .children([
42−//! text(self.who),
40+//! text(who),
4341//! text("!"),
4442//! ])
4543//! .finish(),
@@ -63,20 +61,75 @@ cfg_if::cfg_if! {
6361 }
6462}
6563
64+// A macro to expose items unstably, only for internal/testing usage.
65+cfg_if::cfg_if! {
66+ if #[cfg(feature = "xxx-unstable-internal-use-only")] {
67+ macro_rules! pub_unstable_internal {
68+ ( $(#[$attr:meta])* pub(crate) $( $thing:tt )* ) => {
69+ #[doc(hidden)]
70+ $( #[$attr] )*
71+ pub $( $thing )*
72+ }
73+ }
74+ } else {
75+ macro_rules! pub_unstable_internal {
76+ ( $(#[$attr:meta])* pub(crate) $( $thing:tt )* ) => {
77+ $( #[$attr] )*
78+ pub(crate) $( $thing )*
79+ }
80+ }
81+ }
82+}
83+
6684// Only `pub` so that the wasm-bindgen bindings work.
6785#[doc(hidden)]
6886pub mod change_list;
6987
7088mod cached;
89+mod cached_set;
90+mod diff;
7191mod events;
7292mod node;
7393mod render;
94+mod render_context;
95+mod strace;
7496mod vdom;
7597
7698pub mod builder;
7799
78100// Re-export items at the top level.
79101pub use self::cached::Cached;
80−pub use self::node::{Attribute, ElementNode, Listener, ListenerCallback, Node, TextNode};
102+pub use self::node::{Attribute, Listener, Node, NodeKey};
81103pub use self::render::{Render, RootRender};
104+pub use self::render_context::RenderContext;
82105pub use self::vdom::{Vdom, VdomWeak};
106+
107+cfg_if::cfg_if! {
108+ if #[cfg(all(target_arch = "wasm32", not(feature = "xxx-unstable-internal-use-only")))] {
109+ use wasm_bindgen::__rt::WasmRefCell as RefCell;
110+ } else {
111+ use std::cell::RefCell;
112+ }
113+}
114+
115+// Polyfill some Web stuff for benchmarking...
116+cfg_if::cfg_if! {
117+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
118+ /// An element node in the physical DOM.
119+ pub type Element = ();
120+
121+ pub(crate) type EventsTrampoline = ();
122+ } else {
123+ /// An element node in the physical DOM.
124+ pub type Element = web_sys::Element;
125+
126+ pub(crate) type EventsTrampoline = wasm_bindgen::closure::Closure<dyn Fn(web_sys::Event, u32, u32)>;
127+ }
128+}
129+
130+cfg_if::cfg_if! {
131+ if #[cfg(feature = "xxx-unstable-internal-use-only")] {
132+ pub use self::cached_set::{CachedSet};
133+ pub use self::node::{ElementNode, NodeKind, TextNode};
134+ }
135+}
src/node.rs+150−77
@@ -1,33 +1,111 @@
1−use crate::{RootRender, VdomWeak};
1+use crate::{cached_set::CacheId, RootRender, VdomWeak};
22use bumpalo::Bump;
33use std::fmt;
4+use std::iter;
45use std::mem;
6+use std::u32;
57
6−/// A node is either a text node or an element.
8+/// A virtual DOM node.
79#[derive(Debug, Clone)]
8−pub enum Node<'a> {
9− /// A text node.
10− Text(TextNode<'a>),
10+pub struct Node<'a> {
11+ #[cfg(feature = "xxx-unstable-internal-use-only")]
12+ #[doc(hidden)]
13+ pub kind: NodeKind<'a>,
1114
12− /// An element potentially with attributes and children.
13− Element(ElementNode<'a>),
15+ #[cfg(not(feature = "xxx-unstable-internal-use-only"))]
16+ pub(crate) kind: NodeKind<'a>,
1417}
1518
16−/// Text nodes are just a string of text. They cannot have attributes or
17−/// children.
18−#[derive(Debug, Clone)]
19−pub struct TextNode<'a> {
20− pub(crate) text: &'a str,
19+pub_unstable_internal! {
20+ /// A node is either a text node or an element.
21+ #[derive(Debug, Clone)]
22+ pub(crate) enum NodeKind<'a> {
23+ /// A text node.
24+ Text(TextNode<'a>),
25+
26+ /// An element potentially with attributes and children.
27+ Element(&'a ElementNode<'a>),
28+
29+ /// A node in the vdom's `CachedSet`. This allows us to avoid
30+ /// re-rendering and re-diffing subtrees.
31+ Cached(CachedNode),
32+ }
2133}
2234
23−/// Elements have a tag name, zero or more attributes, and zero or more
24−/// children.
25−#[derive(Debug, Clone)]
26−pub struct ElementNode<'a> {
27− pub(crate) tag_name: &'a str,
28− pub(crate) listeners: &'a [Listener<'a>],
29− pub(crate) attributes: &'a [Attribute<'a>],
30− pub(crate) children: &'a [Node<'a>],
35+pub_unstable_internal! {
36+ /// Text nodes are just a string of text. They cannot have attributes or
37+ /// children.
38+ #[derive(Debug, Clone)]
39+ pub(crate) struct TextNode<'a> {
40+ pub text: &'a str,
41+ }
42+}
43+
44+pub_unstable_internal! {
45+ /// Elements have a tag name, zero or more attributes, and zero or more
46+ /// children.
47+ #[derive(Debug, Clone)]
48+ pub(crate) struct ElementNode<'a> {
49+ pub key: NodeKey,
50+ pub tag_name: &'a str,
51+ pub listeners: &'a [Listener<'a>],
52+ pub attributes: &'a [Attribute<'a>],
53+ pub children: &'a [Node<'a>],
54+ pub namespace: Option<&'a str>,
55+ }
56+}
57+
58+pub_unstable_internal! {
59+ /// A cached node exists in an arena that is internal to the `Vdom`. It
60+ /// allows us to avoid both re-rendering a sub-tree and re-diffing
61+ /// it. `CachedNode`s cannot contain cycles, since they can only contain
62+ /// other `CachedNode`s that already exist when they are being constructed.
63+ #[derive(Debug, Clone, Copy)]
64+ pub(crate) struct CachedNode {
65+ pub id: CacheId,
66+ // Always a copy of the inner cached node's key.
67+ pub key: NodeKey,
68+ }
69+}
70+
71+/// The key for keyed children.
72+///
73+/// Keys must be unique among siblings.
74+///
75+/// If any sibling is keyed, then they all must be keyed.
76+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77+pub struct NodeKey(pub(crate) u32);
78+
79+impl Default for NodeKey {
80+ fn default() -> NodeKey {
81+ NodeKey::NONE
82+ }
83+}
84+
85+impl NodeKey {
86+ /// The default, lack of a key.
87+ pub const NONE: NodeKey = NodeKey(u32::MAX);
88+
89+ /// Is this key `NodeKey::NONE`?
90+ #[inline]
91+ pub fn is_none(&self) -> bool {
92+ *self == Self::NONE
93+ }
94+
95+ /// Is this key not `NodeKey::NONE`?
96+ #[inline]
97+ pub fn is_some(&self) -> bool {
98+ !self.is_none()
99+ }
100+
101+ /// Create a new `NodeKey`.
102+ ///
103+ /// `key` must not be `u32::MAX`.
104+ #[inline]
105+ pub fn new(key: u32) -> Self {
106+ debug_assert_ne!(key, u32::MAX);
107+ NodeKey(key)
108+ }
31109}
32110
33111/// An event listener callback function.
@@ -36,8 +114,8 @@ pub struct ElementNode<'a> {
36114///
37115/// 1. The virtual DOM's root rendering component.
38116/// 2. A capability to scheduler virtual DOM re-rendering.
39−/// 3. The event that ocurred.
40−pub type ListenerCallback<'a> =
117+/// 3. The event that occurred.
118+pub(crate) type ListenerCallback<'a> =
41119 &'a (dyn Fn(&mut dyn RootRender, VdomWeak, web_sys::Event) + 'static);
42120
43121/// An event listener.
@@ -56,6 +134,15 @@ pub struct Attribute<'a> {
56134 pub(crate) value: &'a str,
57135}
58136
137+impl<'a> From<CachedNode> for Node<'a> {
138+ #[inline]
139+ fn from(c: CachedNode) -> Self {
140+ Node {
141+ kind: NodeKind::Cached(c),
142+ }
143+ }
144+}
145+
59146impl fmt::Debug for Listener<'_> {
60147 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
61148 let (a, b) = self.get_callback_parts();
@@ -95,81 +182,67 @@ impl<'a> Attribute<'a> {
95182}
96183
97184impl<'a> Node<'a> {
98− /// Construct a new element node with the given tag name and children.
185+ /// Low-level constructor for making a new `Node` of type element with given
186+ /// parts.
187+ ///
188+ /// This is primarily intended for JSX and templating proc-macros to compile
189+ /// down into. If you are building nodes by-hand, prefer using the
190+ /// `dodrio::builder::*` APIs.
99191 #[inline]
100− pub(crate) fn element<Listeners, Attributes, Children>(
192+ pub fn element(
101193 bump: &'a Bump,
194+ key: NodeKey,
102195 tag_name: &'a str,
103− listeners: Listeners,
104− attributes: Attributes,
105− children: Children,
106− ) -> Node<'a>
107− where
108− Listeners: 'a + AsRef<[Listener<'a>]>,
109− Attributes: 'a + AsRef<[Attribute<'a>]>,
110− Children: 'a + AsRef<[Node<'a>]>,
111− {
112− let children: &'a Children = bump.alloc(children);
113− let children: &'a [Node<'a>] = children.as_ref();
114−
115− let listeners: &'a Listeners = bump.alloc(listeners);
116− let listeners: &'a [Listener<'a>] = listeners.as_ref();
117−
118− let attributes: &'a Attributes = bump.alloc(attributes);
119− let attributes: &'a [Attribute<'a>] = attributes.as_ref();
120−
121− Node::Element(ElementNode {
196+ listeners: &'a [Listener<'a>],
197+ attributes: &'a [Attribute<'a>],
198+ children: &'a [Node<'a>],
199+ namespace: Option<&'a str>,
200+ ) -> Node<'a> {
201+ let element = bump.alloc_with(|| ElementNode {
202+ key,
122203 tag_name,
123204 listeners,
124205 attributes,
125206 children,
126− })
127− }
128−
129− /// Is this node a text node?
130− pub fn is_text(&self) -> bool {
131− match self {
132− Node::Text(_) => true,
133− _ => false,
134− }
135− }
207+ namespace,
208+ });
136209
137− /// Is this node an element?
138− pub fn is_element(&self) -> bool {
139− match self {
140− Node::Element { .. } => true,
141− _ => false,
210+ Node {
211+ kind: NodeKind::Element(element),
142212 }
143213 }
144214
145215 /// Construct a new text node with the given text.
146216 #[inline]
147217 pub(crate) fn text(text: &'a str) -> Node<'a> {
148− Node::Text(TextNode { text })
218+ Node {
219+ kind: NodeKind::Text(TextNode { text }),
220+ }
149221 }
150−}
151222
152−impl<'a> TextNode<'a> {
153− /// Get this text node's text content.
154− pub fn text(&self) -> &'a str {
155− self.text
223+ #[inline]
224+ pub(crate) fn key(&self) -> NodeKey {
225+ match &self.kind {
226+ NodeKind::Text(_) => NodeKey::NONE,
227+ NodeKind::Element(e) => e.key,
228+ NodeKind::Cached(c) => c.key,
229+ }
156230 }
157231}
158232
159−impl<'a> ElementNode<'a> {
160− /// Get this element's tag name.
161− pub fn tag_name(&self) -> &'a str {
162− self.tag_name
163− }
164−
165− /// Get this element's attributes.
166− pub fn attributes(&self) -> &'a [Attribute<'a>] {
167− self.attributes
168− }
233+/// A node can become an iterator that yields the node itself once.
234+///
235+/// This implementation of `IntoIterator` mostly exists to improve the
236+/// `typed-html` ergonomics, where the macro invokes `.into_iter()` on the child
237+/// contents of a tag. By implementing `IntoIterator` here, we avoid having to
238+/// do nasty shenanigans like `<p>vec![$contents]</p>` instead of plain old
239+/// `<p>$contents</p>`.
240+impl<'a> IntoIterator for Node<'a> {
241+ type Item = Node<'a>;
242+ type IntoIter = iter::Once<Self::Item>;
169243
170− /// Get this element's attributes.
171− pub fn children(&self) -> &'a [Node<'a>] {
172− self.children
244+ fn into_iter(self) -> Self::IntoIter {
245+ iter::once(self)
173246 }
174247}
175248
src/render.rs+65−50
@@ -1,5 +1,4 @@
1−use crate::Node;
2−use bumpalo::Bump;
1+use crate::{Node, RenderContext};
32use std::any::Any;
43use std::rc::Rc;
54use wasm_bindgen::UnwrapThrowExt;
@@ -11,72 +10,52 @@ use wasm_bindgen::UnwrapThrowExt;
1110///
1211/// ## `Bump` Allocation
1312///
14−/// `Render` implementations can use the provided `Bump` for very fast
15−/// allocation for anything that needs to be allocated during rendering.
16−///
17−/// ## The `'a: 'bump` Lifetime Bound
18−///
19−/// The `'a: 'bump` bounds enforce that `self` outlives the given bump
20−/// allocator. This means that if `self` contains a string, the string does not
21−/// need to be copied into the output `Node` and can be used by reference
22−/// instead (i.e. it prevents accidentally using the string after its been
23−/// freed). The `'a: 'bump` bound also enables abstractions like
24−/// `dodrio::Cached` that can re-use cached `Node`s across `render`s without
25−/// copying them.
13+/// `Render` implementations can use the `Bump` inside the provided
14+/// `RenderContext` for very fast allocation for anything that needs to be
15+/// temporarily allocated during rendering.
2616///
2717/// ## Example
2818///
2919/// ```no_run
30−/// use dodrio::{bumpalo::Bump, Node, Render};
20+/// use dodrio::{Node, Render, RenderContext};
3121///
3222/// pub struct MyComponent;
3323///
34−/// impl Render for MyComponent {
35−/// fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
36−/// where
37−/// 'a: 'bump
38−/// {
24+/// impl<'a> Render<'a> for MyComponent {
25+/// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
3926/// use dodrio::builder::*;
4027///
41−/// p(bump)
28+/// p(&cx)
4229/// .children([
4330/// text("This is "),
44−/// strong(bump).children([text("my component")]).finish(),
31+/// strong(&cx).children([text("my component")]).finish(),
4532/// text(" rendered!"),
4633/// ])
4734/// .finish()
4835/// }
4936/// }
5037/// ```
51−pub trait Render {
52− /// Render `self` as a virtual DOM. Use the given `Bump` for temporary
53− /// allocations.
54− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
55− where
56− 'a: 'bump;
38+pub trait Render<'a> {
39+ /// Render `self` as a virtual DOM. Use the given context's `Bump` for
40+ /// temporary allocations.
41+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a>;
5742}
5843
59−impl<'r, R> Render for &'r R
44+impl<'a, 'r, R> Render<'a> for &'r R
6045where
61− R: Render,
46+ R: Render<'a>,
6247{
63− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
64− where
65− 'a: 'bump,
66− {
67− (**self).render(bump)
48+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
49+ (**self).render(cx)
6850 }
6951}
7052
71−impl<R> Render for Rc<R>
53+impl<'a, R> Render<'a> for Rc<R>
7254where
73− R: Render,
55+ R: Render<'a>,
7456{
75− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
76− where
77− 'a: 'bump,
78− {
79− (**self).render(bump)
57+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
58+ (**self).render(cx)
8059 }
8160}
8261
@@ -90,21 +69,24 @@ where
9069/// You do not need to implement this trait by hand: there is a blanket
9170/// implementation for all `Render` types that fulfill the `RootRender`
9271/// requirements.
93−pub trait RootRender: Any + Render {
72+pub trait RootRender: Any + for<'a> Render<'a> {
9473 /// Get this `&RootRender` trait object as an `&Any` trait object reference.
95− fn as_any(&self) -> &Any;
74+ fn as_any(&self) -> &dyn Any;
9675
9776 /// Get this `&mut RootRender` trait object as an `&mut Any` trait object
9877 /// reference.
99− fn as_any_mut(&mut self) -> &mut Any;
78+ fn as_any_mut(&mut self) -> &mut dyn Any;
10079}
10180
102−impl<T: Any + Render> RootRender for T {
103− fn as_any(&self) -> &Any {
81+impl<T> RootRender for T
82+where
83+ T: Any + for<'a> Render<'a>,
84+{
85+ fn as_any(&self) -> &dyn Any {
10486 self
10587 }
10688
107− fn as_any_mut(&mut self) -> &mut Any {
89+ fn as_any_mut(&mut self) -> &mut dyn Any {
10890 self
10991 }
11092}
@@ -117,7 +99,10 @@ impl dyn RootRender {
11799 ///
118100 /// Panics if this virtual DOM's root rendering component is not an `R`
119101 /// instance.
120− pub fn unwrap_ref<R: RootRender>(&self) -> &R {
102+ pub fn unwrap_ref<R>(&self) -> &R
103+ where
104+ R: RootRender,
105+ {
121106 self.as_any()
122107 .downcast_ref::<R>()
123108 .expect_throw("bad `RootRender::unwrap_ref` call")
@@ -130,7 +115,10 @@ impl dyn RootRender {
130115 ///
131116 /// Panics if this virtual DOM's root rendering component is not an `R`
132117 /// instance.
133− pub fn unwrap_mut<R: RootRender>(&mut self) -> &mut R {
118+ pub fn unwrap_mut<R>(&mut self) -> &mut R
119+ where
120+ R: RootRender,
121+ {
134122 self.as_any_mut()
135123 .downcast_mut::<R>()
136124 .expect_throw("bad `RootRender::unwrap_ref` call")
@@ -150,4 +138,31 @@ mod tests {
150138 #[allow(dead_code)]
151139 fn takes_dyn_render(_: &dyn super::RootRender) {}
152140 }
141+
142+ #[test]
143+ fn render_bump_scoped_child() {
144+ use crate::{builder::*, bumpalo::collections::String, Node, Render, RenderContext};
145+
146+ struct Child<'a> {
147+ name: &'a str,
148+ }
149+
150+ impl<'a> Render<'a> for Child<'a> {
151+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
152+ text(self.name)
153+ }
154+ }
155+
156+ struct Parent;
157+
158+ impl<'a> Render<'a> for Parent {
159+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
160+ let child_name = String::from_str_in("child", cx.bump).into_bump_str();
161+
162+ div(&cx)
163+ .children([Child { name: child_name }.render(cx)])
164+ .finish()
165+ }
166+ }
167+ }
153168}
src/render_context.rsnew file+104
@@ -0,0 +1,104 @@
1+use crate::{
2+ cached::{Cached, TemplateId},
3+ cached_set::{CacheId, CachedSet},
4+ Node, Render,
5+};
6+use bumpalo::Bump;
7+use fxhash::FxHashMap;
8+use std::fmt;
9+
10+/// Common context available to all `Render` implementations.
11+///
12+/// Notably, the `RenderContext` gives access to the bump arena that the virtual
13+/// DOM should be allocated within. This is available via the `bump` field.
14+pub struct RenderContext<'a> {
15+ /// The underlying bump arena that virtual DOMs are rendered into.
16+ ///
17+ /// ## Example
18+ ///
19+ /// ```
20+ /// use dodrio::RenderContext;
21+ ///
22+ /// // Given a rendering context, allocate an i32 inside its bump arena.
23+ /// fn foo<'a>(cx: &mut RenderContext<'a>) -> &'a mut i32 {
24+ /// cx.bump.alloc(42)
25+ /// }
26+ /// ```
27+ pub bump: &'a Bump,
28+
29+ pub(crate) cached_set: &'a crate::RefCell<CachedSet>,
30+
31+ pub(crate) templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>,
32+
33+ // Prevent exhaustive matching on the rendering context, so we can always
34+ // add more members in a semver-compatible way.
35+ _non_exhaustive: (),
36+}
37+
38+impl fmt::Debug for RenderContext<'_> {
39+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40+ f.debug_struct("RenderContext")
41+ .field("bump", &self.bump)
42+ .finish()
43+ }
44+}
45+
46+impl<'a> RenderContext<'a> {
47+ pub_unstable_internal! {
48+ pub(crate) fn new(
49+ bump: &'a Bump,
50+ cached_set: &'a crate::RefCell<CachedSet>,
51+ templates: &'a mut FxHashMap<TemplateId, Option<CacheId>>
52+ ) -> Self {
53+ RenderContext {
54+ bump,
55+ cached_set,
56+ templates,
57+ _non_exhaustive: (),
58+ }
59+ }
60+ }
61+
62+ pub(crate) fn cache<F>(&mut self, pinned: bool, template: Option<CacheId>, f: F) -> CacheId
63+ where
64+ F: for<'b> FnOnce(&mut RenderContext<'b>) -> Node<'b>,
65+ {
66+ CachedSet::insert(self, pinned, template, f)
67+ }
68+
69+ /// Get or create the cached template for `Cached<R>`.
70+ pub(crate) fn template<R>(&mut self) -> Option<CacheId>
71+ where
72+ R: 'static + Default + for<'b> Render<'b>,
73+ {
74+ let template_id = Cached::<R>::template_id();
75+ if let Some(cache_id) = self.templates.get(&template_id).cloned() {
76+ return cache_id;
77+ }
78+
79+ // Prevent re-entrancy from infinite looping. Any attempts to get `R`'s
80+ // template while constructing the template will simply fail to use the
81+ // templated fast path.
82+ self.templates.insert(template_id, None);
83+
84+ // Render the default `R` and save that as the template for all
85+ // `Cached<R>`s.
86+ let cache_id = self.cache(true, None, |nested_cx| R::default().render(nested_cx));
87+ self.templates.insert(template_id, Some(cache_id));
88+ Some(cache_id)
89+ }
90+}
91+
92+impl<'a, 'b> From<&'b RenderContext<'a>> for &'a Bump {
93+ #[inline]
94+ fn from(cx: &'b RenderContext<'a>) -> &'a Bump {
95+ cx.bump
96+ }
97+}
98+
99+impl<'a, 'b, 'c> From<&'c &'b mut RenderContext<'a>> for &'a Bump {
100+ #[inline]
101+ fn from(cx: &'c &'b mut RenderContext<'a>) -> &'a Bump {
102+ cx.bump
103+ }
104+}
src/strace.rsnew file+22
@@ -0,0 +1,22 @@
1+cfg_if::cfg_if! {
2+ if #[cfg(feature = "xxx-unstable-strace")] {
3+ use wasm_bindgen::prelude::*;
4+
5+ #[wasm_bindgen(module = "/js/strace.js")]
6+ extern "C" {
7+ #[wasm_bindgen(js_name = initStrace)]
8+ fn really_init_strace();
9+ }
10+
11+ pub fn init_strace() {
12+ use std::sync::Once;
13+ static STRACE: Once = Once::new();
14+ STRACE.call_once(|| {
15+ really_init_strace();
16+ });
17+ }
18+ } else {
19+ #[inline]
20+ pub fn init_strace() {}
21+ }
22+}
src/vdom.rs+182−338
@@ -1,13 +1,16 @@
1−use super::change_list::ChangeList;
2−use super::node::{Attribute, ElementNode, Listener, Node, TextNode};
1+use super::change_list::ChangeListPersistentState;
32use super::RootRender;
3+use crate::cached::TemplateId;
4+use crate::cached_set::{CacheId, CachedSet};
45use crate::events::EventsRegistry;
6+use crate::node::{Node, NodeKey};
7+use crate::RenderContext;
58use bumpalo::Bump;
6−use futures::future::Future;
9+use fxhash::FxHashMap;
710use std::cell::Cell;
811use std::cell::RefCell;
9−use std::cmp;
1012use std::fmt;
13+use std::future::Future;
1114use std::mem;
1215use std::mem::ManuallyDrop;
1316use std::rc::{Rc, Weak};
@@ -52,12 +55,15 @@ pub(crate) struct VdomInnerExclusive {
5255 // Always `Some` except just before we drop. Just an option so that
5356 // `unmount` can take the component out but we can still have a Drop
5457 // implementation.
55− component: Option<Box<RootRender>>,
58+ component: Option<Box<dyn RootRender>>,
5659
57− dom_buffers: [Bump; 2],
58− change_list: ManuallyDrop<ChangeList>,
59− container: web_sys::Element,
60+ dom_buffers: Option<[Bump; 2]>,
61+ change_list: ManuallyDrop<ChangeListPersistentState>,
62+ container: crate::Element,
6063 events_registry: Option<Rc<RefCell<EventsRegistry>>>,
64+ events_trampoline: Option<crate::EventsTrampoline>,
65+ cached_set: crate::RefCell<CachedSet>,
66+ templates: FxHashMap<TemplateId, Option<CacheId>>,
6167
6268 // Actually a reference into `self.dom_buffers[0]` or if `self.component` is
6369 // caching renders, into `self.component`'s bump.
@@ -90,6 +96,7 @@ impl fmt::Debug for VdomInnerExclusive {
9096 .field("change_list", &self.change_list)
9197 .field("container", &self.container)
9298 .field("events_registry", &self.events_registry)
99+ .field("events_trampoline", &"..")
93100 .field("current_root", &self.current_root)
94101 .finish()
95102 }
@@ -109,7 +116,36 @@ impl Drop for VdomInnerExclusive {
109116 let mut registry = registry.borrow_mut();
110117 registry.clear_active_listeners();
111118
112− self.container.set_inner_html("");
119+ empty_container(&self.container);
120+ }
121+}
122+
123+cfg_if::cfg_if! {
124+ if #[cfg(all(feature = "xxx-unstable-internal-use-only", not(target_arch = "wasm32")))] {
125+ fn empty_container(_container: &crate::Element) {}
126+ fn initialize_container(_container: &crate::Element) {}
127+ } else {
128+ fn empty_container(container: &crate::Element) {
129+ container.set_inner_html("");
130+ }
131+
132+ fn initialize_container(container: &crate::Element) {
133+ empty_container(container);
134+
135+ // Create the dummy `<div/>` child in the container.
136+ let window = web_sys::window().expect_throw("should have access to the Window");
137+ let document = window
138+ .document()
139+ .expect("should have access to the Document");
140+ container
141+ .append_child(
142+ document
143+ .create_element("div")
144+ .expect("should create element OK")
145+ .as_ref(),
146+ )
147+ .expect("should append child OK");
148+ }
113149 }
114150}
115151
@@ -118,40 +154,29 @@ impl Vdom {
118154 /// rendering component.
119155 ///
120156 /// This will box the given component into trait object.
121− pub fn new<R>(container: &web_sys::Element, component: R) -> Vdom
157+ pub fn new<R>(container: &crate::Element, component: R) -> Vdom
122158 where
123159 R: RootRender,
124160 {
125− Self::with_boxed_root_render(container, Box::new(component) as Box<RootRender>)
161+ Self::with_boxed_root_render(container, Box::new(component) as Box<dyn RootRender>)
126162 }
127163
128164 /// Construct a `Vdom` with the already-boxed-as-a-trait-object root
129165 /// rendering component.
130166 pub fn with_boxed_root_render(
131− container: &web_sys::Element,
132− component: Box<RootRender>,
167+ container: &crate::Element,
168+ component: Box<dyn RootRender>,
133169 ) -> Vdom {
134− let dom_buffers = [Bump::new(), Bump::new()];
135− let change_list = ManuallyDrop::new(ChangeList::new(container));
170+ crate::strace::init_strace();
136171
137− // Ensure that the container is empty.
138− container.set_inner_html("");
172+ let dom_buffers = [Bump::new(), Bump::new()];
173+ let change_list = ManuallyDrop::new(ChangeListPersistentState::new(container));
139174
140175 // Create a dummy `<div/>` in our container.
141− let current_root = Node::element(&dom_buffers[0], "div", [], [], []);
176+ initialize_container(container);
177+ let current_root =
178+ Node::element(&dom_buffers[0], NodeKey::NONE, "div", &[], &[], &[], None);
142179 let current_root = Some(unsafe { extend_node_lifetime(current_root) });
143− let window = web_sys::window().expect("should have acess to the Window");
144− let document = window
145− .document()
146− .expect("should have access to the Document");
147− container
148− .append_child(
149− document
150− .create_element("div")
151− .expect("should create element OK")
152− .as_ref(),
153− )
154− .expect("should append child OK");
155180
156181 let container = container.clone();
157182 let inner = Rc::new(VdomInner {
@@ -160,11 +185,14 @@ impl Vdom {
160185 },
161186 exclusive: RefCell::new(VdomInnerExclusive {
162187 component: Some(component),
163− dom_buffers,
188+ dom_buffers: Some(dom_buffers),
164189 change_list,
165190 container,
166191 current_root,
167192 events_registry: None,
193+ events_trampoline: None,
194+ cached_set: crate::RefCell::new(Default::default()),
195+ templates: Default::default(),
168196 }),
169197 });
170198
@@ -173,7 +201,9 @@ impl Vdom {
173201 {
174202 let mut inner = inner.exclusive.borrow_mut();
175203 inner.events_registry = Some(events_registry);
176− inner.change_list.init_events_trampoline(events_trampoline);
204+ inner.change_list.init_events_trampoline(&events_trampoline);
205+ debug_assert!(inner.events_trampoline.is_none());
206+ inner.events_trampoline = Some(events_trampoline);
177207
178208 // Diff and apply the `contents` against our dummy `<div/>`.
179209 inner.render();
@@ -182,6 +212,19 @@ impl Vdom {
182212 Vdom { inner }
183213 }
184214
215+ /// Immediately re-render and diff. Only for internal testing and
216+ /// benchmarking purposes.
217+ #[cfg(feature = "xxx-unstable-internal-use-only")]
218+ pub fn immediately_render_and_diff<R>(&self, component: R)
219+ where
220+ R: RootRender,
221+ {
222+ let mut exclusive = self.inner.exclusive.borrow_mut();
223+ let component = Box::new(component) as Box<dyn RootRender>;
224+ exclusive.component = Some(component);
225+ exclusive.render();
226+ }
227+
185228 /// Run this virtual DOM and its listeners forever and never unmount it.
186229 #[inline]
187230 pub fn forget(self) {
@@ -197,7 +240,7 @@ impl Vdom {
197240 /// Unmount this virtual DOM, unregister its event listeners, and return its
198241 /// root render component.
199242 #[inline]
200− pub fn unmount(self) -> Box<RootRender> {
243+ pub fn unmount(self) -> Box<dyn RootRender> {
201244 Rc::try_unwrap(self.inner.clone())
202245 .map_err(|_| ())
203246 .unwrap_throw()
@@ -221,268 +264,69 @@ impl VdomInnerExclusive {
221264 pub(crate) fn render(&mut self) {
222265 unsafe {
223266 let events_registry = self.events_registry.take().unwrap();
224−
225267 {
226− // All the old listeners are no longer active. We will build a new
227− // set of active listeners when diffing.
228− //
229− // NB: if we end up avoiding diffing cached renders (instead of just
230− // avoiding re-rendering them) then we will need to maintain cached
231− // active listeners, and can't just clear all active listeners and
232− // rebuild them here.
233268 let mut registry = events_registry.borrow_mut();
234− registry.clear_active_listeners();
235269
236270 // Reset the inactive bump arena's pointer.
237− self.dom_buffers[1].reset();
271+ let mut dom_buffers = self.dom_buffers.take().unwrap_throw();
272+ dom_buffers[1].reset();
238273
239274 // Render the new current contents into the inactive bump arena.
240− let new_contents = self
241− .component
242− .as_ref()
243− .unwrap_throw()
244− .render(&self.dom_buffers[1]);
275+ let mut cx =
276+ RenderContext::new(&dom_buffers[1], &self.cached_set, &mut self.templates);
277+ let new_contents = self.component.as_ref().unwrap_throw().render(&mut cx);
245278 let new_contents = extend_node_lifetime(new_contents);
246279
247280 // Diff the old contents with the new contents.
248281 let old_contents = self.current_root.take().unwrap();
249− self.diff(&mut registry, old_contents, new_contents.clone());
282+ let mut cache_roots;
283+ {
284+ let cached_set = self.cached_set.borrow();
285+ cache_roots = cached_set.new_roots_set();
286+ let mut change_list = self.change_list.builder();
287+ crate::diff::diff(
288+ &cached_set,
289+ &mut change_list,
290+ &mut registry,
291+ &old_contents,
292+ &new_contents,
293+ &mut cache_roots,
294+ );
295+
296+ // Tell JS to apply our diff-generated changes to the physical DOM!
297+ change_list.finish();
298+ }
299+
300+ {
301+ // Clean up unused cached renders.
302+ let mut cached_set = self.cached_set.borrow_mut();
303+ cached_set.gc(&mut registry, cache_roots);
304+ }
250305
251306 // Swap the buffers to make the bump arena with the new contents the
252307 // active arena, and the old one into the inactive arena.
253− self.swap_buffers();
308+ self.swap_buffers(dom_buffers);
254309 self.set_current_root(new_contents);
255310 }
256311
257312 self.events_registry = Some(events_registry);
258−
259− // Find and drop cached strings that aren't in use anymore.
260− self.change_list.drop_unused_strings();
261−
262− // Tell JS to apply our diff-generated changes to the physical DOM!
263− self.change_list.apply_changes();
264313 }
265314 }
266315
267− fn swap_buffers(&mut self) {
268− let (first, second) = self.dom_buffers.as_mut().split_at_mut(1);
316+ fn swap_buffers(&mut self, mut dom_buffers: [Bump; 2]) {
317+ debug_assert!(self.dom_buffers.is_none());
318+ let (first, second) = dom_buffers.as_mut().split_at_mut(1);
269319 mem::swap(&mut first[0], &mut second[0]);
320+ self.dom_buffers = Some(dom_buffers);
270321 }
271322
272323 unsafe fn set_current_root(&mut self, current: Node<'static>) {
273324 debug_assert!(self.current_root.is_none());
274325 self.current_root = Some(current);
275326 }
276−
277− fn diff<'a>(&mut self, registry: &mut EventsRegistry, old: Node<'a>, new: Node<'a>) {
278− // debug!("---------------------------------------------------------");
279− // debug!("dodrio::Vdom::diff");
280− // debug!(" old = {:#?}", old);
281− // debug!(" new = {:#?}", new);
282− match (&new, old) {
283− (&Node::Text(TextNode { text: new_text }), Node::Text(TextNode { text: old_text })) => {
284− debug!(" both are text nodes");
285− if new_text != old_text {
286− debug!(" text needs updating");
287− self.change_list.emit_set_text(new_text);
288− }
289− }
290− (&Node::Text(TextNode { .. }), Node::Element(ElementNode { .. })) => {
291− debug!(" replacing a text node with an element");
292− self.create(registry, new);
293− self.change_list.emit_replace_with();
294− }
295− (&Node::Element(ElementNode { .. }), Node::Text(TextNode { .. })) => {
296− debug!(" replacing an element with a text node");
297− self.create(registry, new);
298− self.change_list.emit_replace_with();
299− }
300− (
301− &Node::Element(ElementNode {
302− tag_name: new_tag_name,
303− listeners: new_listeners,
304− attributes: new_attributes,
305− children: new_children,
306− }),
307− Node::Element(ElementNode {
308− tag_name: old_tag_name,
309− listeners: old_listeners,
310− attributes: old_attributes,
311− children: old_children,
312− }),
313− ) => {
314− debug!(" updating an element");
315− if new_tag_name != old_tag_name {
316− debug!(" different tag names; creating new element and replacing old element");
317− self.create(registry, new);
318− self.change_list.emit_replace_with();
319− return;
320− }
321− self.diff_listeners(registry, old_listeners, new_listeners);
322− self.diff_attributes(old_attributes, new_attributes);
323− self.diff_children(registry, old_children, new_children);
324− }
325− }
326− }
327−
328− fn diff_listeners<'a>(
329− &mut self,
330− registry: &mut EventsRegistry,
331− old: &'a [Listener<'a>],
332− new: &'a [Listener<'a>],
333− ) {
334− debug!(" updating event listeners");
335−
336− 'outer1: for new_l in new {
337− unsafe {
338− // Safety relies on removing `new_l` from the registry manually
339− // at the end of its lifetime. This happens when we invoke
340− // `clear_active_listeners` at the start of a new rendering
341− // phase.
342− registry.add(new_l);
343− }
344− for old_l in old {
345− if new_l.event == old_l.event {
346− self.change_list.emit_update_event_listener(new_l);
347− continue 'outer1;
348− }
349− }
350− self.change_list.emit_new_event_listener(new_l);
351− }
352−
353− 'outer2: for old_l in old {
354− for new_l in new {
355− if new_l.event == old_l.event {
356− continue 'outer2;
357− }
358− }
359− self.change_list.emit_remove_event_listener(old_l.event);
360− }
361− }
362−
363− fn diff_attributes(&mut self, old: &[Attribute], new: &[Attribute]) {
364− debug!(" updating attributes");
365−
366− // Do O(n^2) passes to add/update and remove attributes, since
367− // there are almost always very few attributes.
368− 'outer: for new_attr in new {
369− if new_attr.is_volatile() {
370− self.change_list
371− .emit_set_attribute(new_attr.name, new_attr.value);
372− } else {
373− for old_attr in old {
374− if old_attr.name == new_attr.name {
375− if old_attr.value != new_attr.value {
376− self.change_list
377− .emit_set_attribute(new_attr.name, new_attr.value);
378− }
379− continue 'outer;
380− }
381− }
382− self.change_list
383− .emit_set_attribute(new_attr.name, new_attr.value);
384− }
385− }
386−
387− 'outer2: for old_attr in old {
388− for new_attr in new {
389− if old_attr.name == new_attr.name {
390− continue 'outer2;
391− }
392− }
393− self.change_list.emit_remove_attribute(old_attr.name);
394− }
395− }
396−
397− fn diff_children<'a>(
398− &mut self,
399− registry: &mut EventsRegistry,
400− old: &'a [Node<'a>],
401− new: &'a [Node<'a>],
402− ) {
403− debug!(" updating children shared by old and new");
404−
405− let num_children_to_diff = cmp::min(new.len(), old.len());
406− let mut new_children = new.iter();
407− let mut old_children = old.iter();
408− let mut pushed = false;
409−
410− for (i, (new_child, old_child)) in new_children
411− .by_ref()
412− .zip(old_children.by_ref())
413− .take(num_children_to_diff)
414− .enumerate()
415− {
416− if i == 0 {
417− self.change_list.emit_push_first_child();
418− pushed = true;
419− } else {
420− debug_assert!(pushed);
421− self.change_list.emit_pop_push_next_sibling();
422− }
423−
424− self.diff(registry, old_child.clone(), new_child.clone());
425− }
426−
427− if old_children.next().is_some() {
428− debug!(" removing extra old children");
429− debug_assert!(new_children.next().is_none());
430− if !pushed {
431− self.change_list.emit_push_first_child();
432− } else {
433− self.change_list.emit_pop_push_next_sibling();
434− }
435− self.change_list.emit_remove_self_and_next_siblings();
436− pushed = false;
437− } else {
438− debug!(" creating new children");
439− for (i, new_child) in new_children.enumerate() {
440− if i == 0 && pushed {
441− self.change_list.emit_pop();
442− pushed = false;
443− }
444− self.create(registry, new_child.clone());
445− self.change_list.emit_append_child();
446− }
447− }
448−
449− debug!(" done updating children");
450− if pushed {
451− self.change_list.emit_pop();
452− }
453− }
454−
455− fn create<'a>(&mut self, registry: &mut EventsRegistry, node: Node<'a>) {
456− match node {
457− Node::Text(TextNode { text }) => {
458− self.change_list.emit_create_text_node(text);
459− }
460− Node::Element(ElementNode {
461− tag_name,
462− listeners,
463− attributes,
464− children,
465− }) => {
466− self.change_list.emit_create_element(tag_name);
467− for l in listeners {
468− unsafe {
469− registry.add(l);
470− }
471− self.change_list.emit_new_event_listener(l);
472− }
473− for attr in attributes {
474− self.change_list.emit_set_attribute(&attr.name, &attr.value);
475− }
476− for child in children {
477− self.create(registry, child.clone());
478− self.change_list.emit_append_child();
479− }
480− }
481− }
482− }
483327}
484328
485−fn request_animation_frame(f: &Closure<FnMut()>) {
329+fn request_animation_frame(f: &Closure<dyn FnMut()>) {
486330 web_sys::window()
487331 .expect_throw("should have a window")
488332 .request_animation_frame(f.as_ref().unchecked_ref())
@@ -499,7 +343,7 @@ where
499343 let f = Closure::wrap(Box::new(move || {
500344 *g.borrow_mut() = None;
501345 f();
502− }) as Box<FnMut()>);
346+ }) as Box<dyn FnMut()>);
503347 request_animation_frame(&f);
504348
505349 *h.borrow_mut() = Some(f);
@@ -530,25 +374,23 @@ impl VdomWeak {
530374 /// Replace the root rendering component with the new `root`.
531375 ///
532376 /// Returns a future that resolves to the *old* root component.
533− pub fn set_component(
377+ pub async fn set_component(
534378 self,
535379 root: Box<dyn RootRender>,
536− ) -> impl Future<Item = Box<dyn RootRender + 'static>, Error = VdomDroppedError> {
537− futures::future::ok(self.inner.upgrade())
538− .and_then(|inner| inner.ok_or(()))
539− .map_err(|_| VdomDroppedError {})
540− .and_then(|inner| {
541− let promise = js_sys::Promise::resolve(&JsValue::null());
542− JsFuture::from(promise)
543− .map_err(|_| VdomDroppedError {})
544− .and_then(move |_| {
545− let old = {
546− let mut exclusive = inner.exclusive.borrow_mut();
547− mem::replace(&mut *exclusive.component.as_mut().unwrap_throw(), root)
548− };
549− VdomWeak::new(&inner).render().map(|_| old)
550− })
551− })
380+ ) -> Result<Box<dyn RootRender + 'static>, VdomDroppedError> {
381+ let inner = self.inner.upgrade().ok_or(VdomDroppedError {})?;
382+
383+ // Wait for a new tick of the micro-task queue
384+ let _ = JsFuture::from(js_sys::Promise::resolve(&JsValue::null())).await;
385+
386+ let old = {
387+ let mut exclusive = inner.exclusive.borrow_mut();
388+ mem::replace(&mut *exclusive.component.as_mut().unwrap_throw(), root)
389+ };
390+
391+ VdomWeak::new(&inner).render().await?;
392+
393+ Ok(old)
552394 }
553395
554396 /// Execute `f` with a reference to this virtual DOM's root rendering
@@ -556,24 +398,18 @@ impl VdomWeak {
556398 ///
557399 /// To ensure exclusive access to the root rendering component, the
558400 /// invocation takes place on a new tick of the micro-task queue.
559− pub fn with_component<F, T>(&self, f: F) -> impl Future<Item = T, Error = VdomDroppedError>
401+ pub async fn with_component<F, T>(&self, f: F) -> Result<T, VdomDroppedError>
560402 where
561403 F: 'static + FnOnce(&mut dyn RootRender) -> T,
562404 {
563− futures::future::ok(self.inner.upgrade())
564− .and_then(|inner| inner.ok_or(()))
565− .map_err(|_| VdomDroppedError {})
566− .and_then(|inner| {
567− let mut f = Some(f);
568− let promise = js_sys::Promise::resolve(&JsValue::null());
569− JsFuture::from(promise)
570− .map_err(|_| VdomDroppedError {})
571− .map(move |_| {
572− let f = f.take().unwrap_throw();
573− let mut exclusive = inner.exclusive.borrow_mut();
574− f(exclusive.component_raw_mut())
575− })
576− })
405+ let inner = self.inner.upgrade().ok_or(VdomDroppedError {})?;
406+
407+ // Wait for a new tick of the micro-task queue
408+ let _ = JsFuture::from(js_sys::Promise::resolve(&JsValue::null())).await;
409+
410+ let mut exclusive = inner.exclusive.borrow_mut();
411+
412+ Ok(f(exclusive.component_raw_mut()))
577413 }
578414
579415 /// Schedule a render to occur during the next animation frame.
@@ -582,7 +418,12 @@ impl VdomWeak {
582418 /// `render` instead.
583419 pub fn schedule_render(&self) {
584420 debug!("VdomWeak::schedule_render");
585− wasm_bindgen_futures::spawn_local(self.render().map_err(|_| ()));
421+
422+ let future = self.render();
423+
424+ wasm_bindgen_futures::spawn_local(async move {
425+ let _ = future.await;
426+ });
586427 }
587428
588429 /// Schedule a render to occur during the next animation frame and return a
@@ -590,43 +431,46 @@ impl VdomWeak {
590431 ///
591432 /// If you don't want to do more things after the render completes, then use
592433 /// `schedule_render` instead of `render`.
593− pub fn render(&self) -> impl Future<Item = (), Error = VdomDroppedError> {
594− debug!("VdomWeak::render: initiating render in new animation frame");
595− futures::future::ok(self.inner.upgrade())
596− .and_then(|inner| inner.ok_or(()))
597− .map_err(|_| VdomDroppedError {})
598− .and_then(|inner| {
599− let promise = inner.shared.render_scheduled.take().unwrap_or_else(|| {
600− js_sys::Promise::new(&mut |resolve, reject| {
601− let vdom = VdomWeak {
602− inner: Rc::downgrade(&inner),
603− };
604− with_animation_frame(move || match vdom.inner.upgrade() {
605− None => {
606− warn!("VdomWeak::render: vdom unmounted before we could render");
607− let r = reject.call0(&JsValue::null());
608− debug_assert!(r.is_ok());
609− }
610− Some(inner) => {
611− let mut exclusive = inner.exclusive.borrow_mut();
612− exclusive.render();
613−
614− // We did the render, so take the promise away
615− // and let future `render` calls request new
616− // animation frames.
617− let _ = inner.shared.render_scheduled.take();
618−
619− debug!("VdomWeak::render: finished rendering");
620− let r = resolve.call0(&JsValue::null());
621− debug_assert!(r.is_ok());
622− }
623− });
624− })
625− });
626− inner.shared.render_scheduled.set(Some(promise.clone()));
627− JsFuture::from(promise)
628− .map(|_| ())
629− .map_err(|_| VdomDroppedError {})
630− })
434+ pub fn render(&self) -> impl Future<Output = Result<(), VdomDroppedError>> {
435+ let inner = self.inner.upgrade();
436+
437+ async move {
438+ let inner = inner.ok_or(VdomDroppedError {})?;
439+
440+ let promise = inner.shared.render_scheduled.take().unwrap_or_else(|| {
441+ js_sys::Promise::new(&mut |resolve, reject| {
442+ let vdom = VdomWeak {
443+ inner: Rc::downgrade(&inner),
444+ };
445+
446+ with_animation_frame(move || match vdom.inner.upgrade() {
447+ None => {
448+ warn!("VdomWeak::render: vdom unmounted before we could render");
449+ let r = reject.call0(&JsValue::null());
450+ debug_assert!(r.is_ok());
451+ }
452+ Some(inner) => {
453+ let mut exclusive = inner.exclusive.borrow_mut();
454+ exclusive.render();
455+
456+ // We did the render, so take the promise away
457+ // and let future `render` calls request new
458+ // animation frames.
459+ let _ = inner.shared.render_scheduled.take();
460+
461+ let r = resolve.call0(&JsValue::null());
462+ debug_assert!(r.is_ok());
463+ }
464+ });
465+ })
466+ });
467+
468+ inner.shared.render_scheduled.set(Some(promise.clone()));
469+
470+ JsFuture::from(promise)
471+ .await
472+ .map(drop)
473+ .map_err(|_| VdomDroppedError {})
474+ }
631475 }
632476}
tests/web/cached.jsnew file+11
@@ -0,0 +1,11 @@
1+let cloneNodeCount = 0;
2+
3+const origCloneNode = Node.prototype.cloneNode;
4+Node.prototype.cloneNode = function (...args) {
5+ cloneNodeCount += 1;
6+ return origCloneNode.apply(this, args);
7+};
8+
9+export function getCloneNodeCount() {
10+ return cloneNodeCount;
11+}
tests/web/cached.rs+239−51
@@ -1,11 +1,11 @@
1−use super::{assert_rendered, create_element, RenderFn};
2−use dodrio::{builder::*, bumpalo::Bump, Cached, Node, Render, Vdom};
3−use futures::prelude::*;
1+use super::{assert_rendered, before_after, create_element, RenderFn};
2+use dodrio::{builder::*, bumpalo, Cached, Node, Render, RenderContext, RootRender, Vdom};
43use std::cell::Cell;
54use std::rc::Rc;
6−use wasm_bindgen::*;
5+use wasm_bindgen::prelude::*;
76use wasm_bindgen_test::*;
87
8+#[derive(Default)]
99pub struct CountRenders {
1010 render_count: Cell<usize>,
1111}
@@ -18,21 +18,18 @@ impl CountRenders {
1818 }
1919}
2020
21−impl Render for CountRenders {
22− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
23− where
24− 'a: 'bump,
25− {
21+impl<'a> Render<'a> for CountRenders {
22+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
2623 let count = self.render_count.get() + 1;
2724 self.render_count.set(count);
2825
29− let s = bumpalo::format!(in bump, "{}", count);
26+ let s = bumpalo::format!(in cx.bump, "{}", count);
3027 text(s.into_bump_str())
3128 }
3229}
3330
34−#[wasm_bindgen_test(async)]
35−fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
31+#[wasm_bindgen_test]
32+async fn uses_cached_render() {
3633 use dodrio::builder::*;
3734
3835 let cached = Cached::new(CountRenders::new());
@@ -52,50 +49,241 @@ fn uses_cached_render() -> impl Future<Item = (), Error = JsValue> {
5249 let vdom6 = vdom0.clone();
5350 let vdom7 = vdom0.clone();
5451
52+ // We render, populate the cache, and get "1".
53+ vdom0.weak().render().await.unwrap();
5554 vdom0
5655 .weak()
57− // We render, populate the cache, and get "1".
58− .render()
59− .and_then(move |_| {
60− vdom0.weak().with_component(move |comp| {
61− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
62− assert_eq!(comp.render_count.get(), 1);
63− assert_rendered(&container1, &RenderFn(|_| text("1")));
64− })
56+ .with_component(move |comp| {
57+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
58+ assert_eq!(comp.render_count.get(), 1);
59+ assert_rendered(&container1, &RenderFn(|_| text("1")));
6560 })
66− // We re-render, re-use the cached node, and get "1" again.
67− .and_then(move |_| vdom1.weak().render())
68− .and_then(move |_| {
69− vdom2.weak().with_component(move |comp| {
70− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
71− assert_eq!(comp.render_count.get(), 1);
72− assert_rendered(&container2, &RenderFn(|_| text("1")));
73− })
61+ .await
62+ .unwrap();
63+
64+ // We re-render, re-use the cached node, and get "1" again.
65+ vdom1.weak().render().await.unwrap();
66+ vdom2
67+ .weak()
68+ .with_component(move |comp| {
69+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
70+ assert_eq!(comp.render_count.get(), 1);
71+ assert_rendered(&container2, &RenderFn(|_| text("1")));
7472 })
75− // We invalidate the cache, re-render, re-populate the cache, and should
76− // now have "2".
77− .and_then(move |_| {
78− vdom3.weak().with_component(move |comp| {
79− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
80− Cached::invalidate(comp);
81− })
73+ .await
74+ .unwrap();
75+
76+ // We invalidate the cache, re-render, re-populate the cache, and should now have "2".
77+ vdom3
78+ .weak()
79+ .with_component(move |comp| {
80+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
81+ Cached::invalidate(comp);
8282 })
83− .and_then(move |_| vdom4.weak().render())
84− .and_then(move |_| {
85− vdom5.weak().with_component(move |comp| {
86− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
87− assert_eq!(comp.render_count.get(), 2);
88− assert_rendered(&container3, &RenderFn(|_| text("2")));
89− })
83+ .await
84+ .unwrap();
85+ vdom4.weak().render().await.unwrap();
86+ vdom5
87+ .weak()
88+ .with_component(move |comp| {
89+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
90+ assert_eq!(comp.render_count.get(), 2);
91+ assert_rendered(&container3, &RenderFn(|_| text("2")));
9092 })
91− // We re-render, re-use the cached node, and get "2" again.
92− .and_then(move |_| vdom6.weak().render())
93− .and_then(move |_| {
94− vdom7.weak().with_component(move |comp| {
95− let comp = comp.unwrap_mut::<Cached<CountRenders>>();
96− assert_eq!(comp.render_count.get(), 2);
97− assert_rendered(&container4, &RenderFn(|_| text("2")));
98− })
93+ .await
94+ .unwrap();
95+
96+ // We re-render, re-use the cached node, and get "2" again.
97+ vdom6.weak().render().await.unwrap();
98+ vdom7
99+ .weak()
100+ .with_component(move |comp| {
101+ let comp = comp.unwrap_mut::<Cached<CountRenders>>();
102+ assert_eq!(comp.render_count.get(), 2);
103+ assert_rendered(&container4, &RenderFn(|_| text("2")));
99104 })
100− .map_err(|e| JsValue::from(e.to_string()))
105+ .await
106+ .unwrap();
107+}
108+
109+#[wasm_bindgen(module = "/tests/web/cached.js")]
110+extern "C" {
111+ #[wasm_bindgen(js_name = getCloneNodeCount)]
112+ fn get_clone_node_count() -> u32;
113+}
114+
115+#[wasm_bindgen_test]
116+async fn creating_cached_nodes_uses_clone_node() {
117+ use dodrio::builder::*;
118+
119+ let container = create_element("div");
120+
121+ // The initial vdom render should populate the cache and save a template.
122+ let vdom0 = Rc::new(Vdom::new(&container, Cached::new(CountRenders::new())));
123+ let vdom1 = vdom0.clone();
124+ let _vdom2 = vdom0.clone();
125+
126+ let clone_node_count_before = get_clone_node_count();
127+
128+ // Render something entirely different...
129+ vdom0
130+ .weak()
131+ .set_component(Box::new(RenderFn(|_| text("hi"))) as Box<dyn RootRender>)
132+ .await
133+ .unwrap();
134+
135+ // Re-render our cached node. Since we are creating it from scratch,
136+ // and already have a template from the earlier render, we should
137+ // call `cloneNode` here.
138+ vdom1
139+ .weak()
140+ .set_component(Box::new(Cached::new(CountRenders::new())) as Box<dyn RootRender>)
141+ .await
142+ .unwrap();
143+
144+ let clone_node_count_after = get_clone_node_count();
145+ assert_eq!(clone_node_count_after, clone_node_count_before + 1);
146+}
147+
148+struct Id(&'static str);
149+
150+impl Default for Id {
151+ fn default() -> Id {
152+ Id("")
153+ }
154+}
155+
156+impl<'a> Render<'a> for Id {
157+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
158+ text(self.0)
159+ }
160+}
161+
162+thread_local! {
163+ static WARM_CHEESE: Rc<Cached<Id>> = Rc::new(Cached::new(Id("cheese")));
164+ static WARM_CHEESIER: Rc<Cached<Id>> = Rc::new(Cached::new(Id("cheesier")));
165+}
166+
167+fn warm_cheese<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
168+ WARM_CHEESE.with(|c| {
169+ let _ = c.render(cx);
170+ c.render(cx)
171+ })
172+}
173+
174+fn warm_cheesier<'a>(cx: &mut RenderContext<'a>) -> Node<'a> {
175+ WARM_CHEESIER.with(|c| {
176+ let _ = c.render(cx);
177+ c.render(cx)
178+ })
179+}
180+
181+before_after! {
182+ cold_cache_and_not_cached {
183+ before(cx) {
184+ Cached::new(Id("cheese")).render(cx)
185+ }
186+ after(cx) {
187+ Id("ravioli").render(cx)
188+ }
189+ }
190+
191+ not_cached_and_cold_cache {
192+ before(cx) {
193+ Id("ravioli").render(cx)
194+ }
195+ after(cx) {
196+ Cached::new(Id("cheese")).render(cx)
197+ }
198+ }
199+
200+ cold_cache_and_cold_cache_same {
201+ before(cx) {
202+ Cached::new(Id("cheese")).render(cx)
203+ }
204+ after(cx) {
205+ Cached::new(Id("cheese")).render(cx)
206+ }
207+ }
208+
209+ cold_cache_and_cold_cache_different {
210+ before(cx) {
211+ Cached::new(Id("cheese")).render(cx)
212+ }
213+ after(cx) {
214+ Cached::new(Id("ravioli")).render(cx)
215+ }
216+ }
217+
218+ warm_cache_and_not_cached {
219+ before(cx) {
220+ warm_cheese(cx)
221+ }
222+ after(cx) {
223+ Id("ravioli").render(cx)
224+ }
225+ }
226+
227+ not_cached_and_warm_cache {
228+ before(cx) {
229+ Id("ravioli").render(cx)
230+ }
231+ after(cx) {
232+ warm_cheese(cx)
233+ }
234+ }
235+
236+ warm_cache_and_warm_cache_same {
237+ before(cx) {
238+ warm_cheese(cx)
239+ }
240+ after(cx) {
241+ warm_cheese(cx)
242+ }
243+ }
244+
245+ warm_cache_and_warm_cache_different {
246+ before(cx) {
247+ warm_cheese(cx)
248+ }
249+ after(cx) {
250+ warm_cheesier(cx)
251+ }
252+ }
253+
254+ cold_cache_and_warm_cache_same {
255+ before(cx) {
256+ Cached::new(Id("cheese")).render(cx)
257+ }
258+ after(cx) {
259+ warm_cheese(cx)
260+ }
261+ }
262+
263+ cold_cache_and_warm_cache_different {
264+ before(cx) {
265+ Cached::new(Id("cheese")).render(cx)
266+ }
267+ after(cx) {
268+ warm_cheesier(cx)
269+ }
270+ }
271+
272+ warm_cache_and_cold_cache_same {
273+ before(cx) {
274+ warm_cheese(cx)
275+ }
276+ after(cx) {
277+ Cached::new(Id("cheese")).render(cx)
278+ }
279+ }
280+
281+ warm_cache_and_cold_cache_different {
282+ before(cx) {
283+ warm_cheesier(cx)
284+ }
285+ after(cx) {
286+ Cached::new(Id("cheese")).render(cx)
287+ }
288+ }
101289}
tests/web/events.rs+58−156
@@ -1,7 +1,6 @@
11use super::create_element;
2−use bumpalo::Bump;
3−use dodrio::{Node, Render, Vdom};
4−use futures::Future;
2+use dodrio::{Node, Render, RenderContext, Vdom};
3+use futures::future::{select, Either};
54use std::cell::{Cell, RefCell};
65use std::rc::Rc;
76use wasm_bindgen::prelude::*;
@@ -10,7 +9,7 @@ use wasm_bindgen_test::*;
109
1110struct EventContainer {
1211 event: &'static str,
13− on_event: Box<FnMut()>,
12+ on_event: Box<dyn FnMut()>,
1413}
1514
1615impl EventContainer {
@@ -25,13 +24,10 @@ impl EventContainer {
2524 }
2625}
2726
28−impl Render for EventContainer {
29− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
30− where
31− 'a: 'bump,
32− {
27+impl<'a> Render<'a> for EventContainer {
28+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
3329 use dodrio::builder::*;
34− div(bump)
30+ div(&cx)
3531 .attr("id", "target")
3632 .on(self.event, |root, _scheduler, _event| {
3733 (root.unwrap_mut::<EventContainer>().on_event)();
@@ -48,14 +44,14 @@ fn target(container: &web_sys::Element) -> web_sys::HtmlElement {
4844 .unchecked_into()
4945}
5046
51−#[wasm_bindgen_test(async)]
52−fn click() -> impl Future<Item = (), Error = JsValue> {
47+#[wasm_bindgen_test]
48+async fn click() {
5349 let container = create_element("div");
5450
55− let (sender, receiver) = futures::sync::oneshot::channel();
51+ let (sender, receiver) = futures::channel::oneshot::channel();
5652 let mut sender = Some(sender);
5753
58− let vdom = Vdom::new(
54+ let _vdom = Vdom::new(
5955 &container,
6056 EventContainer::new("click", move || {
6157 sender
@@ -68,18 +64,14 @@ fn click() -> impl Future<Item = (), Error = JsValue> {
6864
6965 target(&container).click();
7066
71− receiver
72− .map(|_| {
73− drop(vdom);
74− })
75− .map_err(|e| JsValue::from(e.to_string()))
67+ receiver.await.unwrap();
7668}
7769
78−#[wasm_bindgen_test(async)]
79−fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
70+#[wasm_bindgen_test]
71+async fn updated_listener_is_called() {
8072 let container = create_element("div");
8173
82− let (first_sender, first_receiver) = futures::sync::oneshot::channel();
74+ let (first_sender, first_receiver) = futures::channel::oneshot::channel();
8375 let mut first_sender = Some(first_sender);
8476
8577 let vdom = Vdom::new(
@@ -93,7 +85,7 @@ fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
9385 }),
9486 );
9587
96− let (second_sender, second_receiver) = futures::sync::oneshot::channel();
88+ let (second_sender, second_receiver) = futures::channel::oneshot::channel();
9789 let mut second_sender = Some(second_sender);
9890
9991 vdom.weak()
@@ -104,25 +96,22 @@ fn updated_listener_is_called() -> impl Future<Item = (), Error = JsValue> {
10496 .send("second")
10597 .expect_throw("should not have dropped the receiver");
10698 })))
107− .map_err(|e| JsValue::from(e.to_string()))
108− .and_then(move |_| {
109− target(&container).click();
110−
111− first_receiver
112− .select(second_receiver)
113− .map(|(which, _other)| which)
114− .or_else(|(_error, other)| other)
115− .map(|which| {
116− assert_eq!(which, "second");
117− drop(vdom);
118− })
119− .map_err(|e| JsValue::from(e.to_string()))
120− })
99+ .await
100+ .unwrap();
101+
102+ target(&container).click();
103+
104+ match select(first_receiver, second_receiver).await {
105+ Either::Left((Ok(_), _)) => panic!(),
106+ Either::Left((Err(_), second)) => assert_eq!(second.await, Ok("second")),
107+ Either::Right((Ok(msg), _)) => assert_eq!(msg, "second"),
108+ Either::Right((Err(_), _)) => panic!(),
109+ }
121110}
122111
123112struct ListensOnlyOnFirstRender {
124113 count: Cell<usize>,
125− callback: Box<FnMut()>,
114+ callback: Box<dyn FnMut()>,
126115}
127116
128117impl ListensOnlyOnFirstRender {
@@ -137,17 +126,14 @@ impl ListensOnlyOnFirstRender {
137126 }
138127}
139128
140−impl Render for ListensOnlyOnFirstRender {
141− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
142− where
143− 'a: 'bump,
144− {
129+impl<'a> Render<'a> for ListensOnlyOnFirstRender {
130+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
145131 use dodrio::builder::*;
146132
147133 let count = self.count.get();
148134 self.count.set(count + 1);
149135
150− let mut elem = div(bump).attr("id", "target");
136+ let mut elem = div(&cx).attr("id", "target");
151137 if count == 0 {
152138 elem = elem.on("click", |root, _scheduler, _event| {
153139 (root.unwrap_mut::<ListensOnlyOnFirstRender>().callback)();
@@ -157,11 +143,11 @@ impl Render for ListensOnlyOnFirstRender {
157143 }
158144}
159145
160−#[wasm_bindgen_test(async)]
161−fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
146+#[wasm_bindgen_test]
147+async fn removed_listener_is_not_called() {
162148 let container = create_element("div");
163149
164− let (outer_sender, outer_receiver) = futures::sync::oneshot::channel();
150+ let (outer_sender, outer_receiver) = futures::channel::oneshot::channel();
165151 let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
166152 let outer_listener = Closure::wrap(Box::new(move |_| {
167153 outer_sender
@@ -170,13 +156,13 @@ fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
170156 .expect_throw("should only invoke outer_listener once")
171157 .send("outer")
172158 .expect_throw("should not have dropped receiver");
173− }) as Box<FnMut(web_sys::Event)>);
159+ }) as Box<dyn FnMut(web_sys::Event)>);
174160
175161 container
176162 .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
177163 .unwrap();
178164
179− let (vdom_sender, vdom_receiver) = futures::sync::oneshot::channel();
165+ let (vdom_sender, vdom_receiver) = futures::channel::oneshot::channel();
180166 let mut vdom_sender = Some(vdom_sender);
181167
182168 // Render a vdom with a listener into container.
@@ -191,33 +177,26 @@ fn removed_listener_is_not_called() -> impl Future<Item = (), Error = JsValue> {
191177 }),
192178 );
193179
194− vdom.weak()
195− // Re-render, so we aren't listening anymore.
196− .render()
197− .map_err(|e| JsValue::from(e.to_string()))
198− .and_then(move |_| {
199− target(&container).click();
200−
201− // We should get our container's event handler fired, and not the unmounted
202− // vdom's event handler.
203− outer_receiver
204− .select(vdom_receiver)
205− .map(|(which, _other)| which)
206− .or_else(|(_error, other)| other)
207− .map(|which| {
208− assert_eq!(which, "outer");
209− drop(outer_listener);
210− drop(vdom)
211− })
212− .map_err(|e| JsValue::from(e.to_string()))
213− })
180+ // Re-render, so we aren't listening anymore.
181+ vdom.weak().render().await.unwrap();
182+
183+ target(&container).click();
184+
185+ // We should get our container's event handler fired, and not the unmounted
186+ // vdom's event handler.
187+ match select(outer_receiver, vdom_receiver).await {
188+ Either::Left((Ok(msg), _)) => assert_eq!(msg, ("outer")),
189+ Either::Left((Err(_), _)) => panic!(),
190+ Either::Right((Ok(_), _)) => panic!(),
191+ Either::Right((Err(_), outer)) => assert_eq!(outer.await, Ok("outer")),
192+ }
214193}
215194
216−#[wasm_bindgen_test(async)]
217−fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsValue> {
195+#[wasm_bindgen_test]
196+async fn event_does_not_fire_after_unmounting() {
218197 let container = create_element("div");
219198
220− let (outer_sender, outer_receiver) = futures::sync::oneshot::channel();
199+ let (outer_sender, outer_receiver) = futures::channel::oneshot::channel();
221200 let outer_sender = Rc::new(RefCell::new(Some(outer_sender)));
222201 let outer_listener = Closure::wrap(Box::new(move |_| {
223202 outer_sender
@@ -226,13 +205,13 @@ fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsVa
226205 .expect_throw("should only invoke outer_listener once")
227206 .send("outer")
228207 .expect_throw("should not have dropped receiver");
229− }) as Box<FnMut(web_sys::Event)>);
208+ }) as Box<dyn FnMut(web_sys::Event)>);
230209
231210 container
232211 .add_event_listener_with_callback("click", outer_listener.as_ref().unchecked_ref())
233212 .unwrap();
234213
235− let (vdom_sender, vdom_receiver) = futures::sync::oneshot::channel();
214+ let (vdom_sender, vdom_receiver) = futures::channel::oneshot::channel();
236215 let mut vdom_sender = Some(vdom_sender);
237216
238217 // Render a vdom with a listener into container.
@@ -262,87 +241,10 @@ fn event_does_not_fire_after_unmounting() -> impl Future<Item = (), Error = JsVa
262241
263242 // We should get our container's event handler fired, and not the unmounted
264243 // vdom's event handler.
265− outer_receiver
266− .select(vdom_receiver)
267− .map(|(which, _other)| which)
268− .or_else(|(_error, other)| other)
269− .map(|which| {
270− assert_eq!(which, "outer");
271− drop(outer_listener);
272− })
273− .map_err(|e| JsValue::from(e.to_string()))
274−}
275−
276−struct CountRendersAndListen {
277− count: Cell<usize>,
278− callback: Box<FnMut(usize)>,
279−}
280−
281−impl CountRendersAndListen {
282− fn new<F>(callback: F) -> CountRendersAndListen
283− where
284− F: 'static + FnMut(usize),
285− {
286− CountRendersAndListen {
287− count: Cell::new(0),
288− callback: Box::new(callback),
289− }
290− }
291−}
292−
293−impl Render for CountRendersAndListen {
294− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
295− where
296− 'a: 'bump,
297− {
298− use dodrio::builder::*;
299−
300− let count = self.count.get() + 1;
301− self.count.set(count);
302−
303− button(bump)
304− .attr("id", "target")
305− .on("click", |comp, _vdom, _event| {
306− let comp: &mut _ = comp.unwrap_mut::<dodrio::Cached<CountRendersAndListen>>();
307− let comp: &mut CountRendersAndListen = comp;
308− let n = comp.count.get();
309− (comp.callback)(n);
310− })
311− .finish()
244+ match select(outer_receiver, vdom_receiver).await {
245+ Either::Left((Ok(msg), _)) => assert_eq!(msg, ("outer")),
246+ Either::Left((Err(_), _)) => panic!(),
247+ Either::Right((Ok(_), _)) => panic!(),
248+ Either::Right((Err(_), outer)) => assert_eq!(outer.await, Ok("outer")),
312249 }
313250}
314−
315−#[wasm_bindgen_test(async)]
316−fn cached_listeners_work_correctly() -> impl Future<Item = (), Error = JsValue> {
317− let container = create_element("div");
318−
319− let (sender, receiver) = futures::sync::oneshot::channel();
320− let mut sender = Some(sender);
321− let vdom = Vdom::new(
322− &container,
323− dodrio::Cached::new(CountRendersAndListen::new(move |n| {
324− sender.take().unwrap().send(n).unwrap();
325− })),
326− );
327−
328− let weak0 = vdom.weak();
329− let weak1 = vdom.weak();
330− let weak2 = vdom.weak();
331− vdom.weak()
332− // Re-render a few times, so we are using the cached version.
333− .render()
334− .and_then(move |_| weak0.render())
335− .and_then(move |_| weak1.render())
336− .and_then(move |_| weak2.render())
337− .map_err(|e| JsValue::from(e.to_string()))
338− .and_then(move |_| {
339− target(&container).click();
340−
341− receiver
342− .map(|n| {
343− assert_eq!(n, 1);
344− drop(vdom);
345− })
346− .map_err(|e| JsValue::from(e.to_string()))
347− })
348−}
tests/web/js_api.rs+37−47
@@ -1,8 +1,7 @@
11use super::{assert_rendered, create_element, RenderFn};
2−use dodrio::{builder::*, bumpalo::Bump, Node, Render, Vdom};
2+use dodrio::{builder::*, Node, Render, RenderContext, Vdom};
33use dodrio_js_api::JsRender;
4−use futures::prelude::*;
5−use futures::sync::oneshot;
4+use futures::channel::oneshot;
65use js_sys::Object;
76use std::rc::Rc;
87use wasm_bindgen::prelude::*;
@@ -14,7 +13,7 @@ pub struct WrapJs {
1413}
1514
1615impl WrapJs {
17− pub fn new() -> (WrapJs, oneshot::Receiver<()>, Closure<FnMut()>) {
16+ pub fn new() -> (WrapJs, oneshot::Receiver<()>, Closure<dyn FnMut()>) {
1817 let (sender, receiver) = oneshot::channel();
1918
2019 let on_after_render = Closure::wrap(Box::new({
@@ -26,7 +25,7 @@ impl WrapJs {
2625 .send(())
2726 .expect_throw("receiver should not have been dropped");
2827 }
29− }) as Box<FnMut()>);
28+ }) as Box<dyn FnMut()>);
3029
3130 let component = WrapJs {
3231 inner: JsRender::new(JsComponent::new(&on_after_render)),
@@ -36,14 +35,11 @@ impl WrapJs {
3635 }
3736}
3837
39−impl Render for WrapJs {
40− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
41− where
42− 'a: 'bump,
43− {
44− div(bump)
38+impl<'a> Render<'a> for WrapJs {
39+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
40+ div(&cx)
4541 .attr("class", "wrap-js")
46− .children([self.inner.render(bump)])
42+ .children([self.inner.render(cx)])
4743 .finish()
4844 }
4945}
@@ -55,7 +51,7 @@ extern "C" {
5551 type JsComponent;
5652
5753 #[wasm_bindgen(constructor)]
58− fn new(on_after_render: &Closure<FnMut()>) -> JsComponent;
54+ fn new(on_after_render: &Closure<dyn FnMut()>) -> JsComponent;
5955}
6056
6157fn eval_js_rendering_component() {
@@ -99,24 +95,24 @@ fn eval_js_rendering_component() {
9995 });
10096}
10197
102−#[wasm_bindgen_test(async)]
103−fn can_use_js_rendering_components() -> impl Future<Item = (), Error = JsValue> {
98+#[wasm_bindgen_test]
99+async fn can_use_js_rendering_components() {
104100 eval_js_rendering_component();
105101
106102 let container = create_element("div");
107− let (component, receiver, closure) = WrapJs::new();
103+ let (component, receiver, _closure) = WrapJs::new();
104+ let _vdom = Rc::new(Vdom::new(&container, component));
108105
109− let vdom = Rc::new(Vdom::new(&container, component));
110106 assert_rendered(
111107 &container,
112− &RenderFn(|bump| {
113− div(bump)
108+ &RenderFn(|cx| {
109+ div(&cx)
114110 .attr("class", "wrap-js")
115− .children([span(bump)
111+ .children([span(&cx)
116112 .attr("class", "js-component")
117113 .children([
118114 text("Here is some plain text"),
119− b(bump)
115+ b(&cx)
120116 .children([text("...and here is some bold text")])
121117 .finish(),
122118 text("0"),
@@ -134,30 +130,24 @@ fn can_use_js_rendering_components() -> impl Future<Item = (), Error = JsValue>
134130 .expect_throw("should be an `HTMLElement` object")
135131 .click();
136132
137− receiver
138− .map(move |_| {
139− assert_rendered(
140− &container,
141− &RenderFn(|bump| {
142− div(bump)
143− .attr("class", "wrap-js")
144− .children([span(bump)
145− .attr("class", "js-component")
146− .children([
147− text("Here is some plain text"),
148− b(bump)
149− .children([text("...and here is some bold text")])
150− .finish(),
151− text("1"),
152− ])
153− .finish()])
154− .finish()
155− }),
156− );
157−
158− drop(vdom);
159− drop(container);
160− drop(closure);
161− })
162− .map_err(|e| e.to_string().into())
133+ receiver.await.unwrap();
134+
135+ assert_rendered(
136+ &container,
137+ &RenderFn(|cx| {
138+ div(&cx)
139+ .attr("class", "wrap-js")
140+ .children([span(&cx)
141+ .attr("class", "js-component")
142+ .children([
143+ text("Here is some plain text"),
144+ b(&cx)
145+ .children([text("...and here is some bold text")])
146+ .finish(),
147+ text("1"),
148+ ])
149+ .finish()])
150+ .finish()
151+ }),
152+ );
163153}
tests/web/keyed.jsnew file+25
@@ -0,0 +1,25 @@
1+export function saveKeyedElements(container) {
2+ const saved = new Map;
3+ const children = container.querySelectorAll(".keyed");
4+ for (const child of children) {
5+ saved.set(child.id, child);
6+ }
7+ return saved;
8+}
9+
10+function assertEq(a, b, msg) {
11+ if (a !== b) {
12+ throw new Error(`assertEq failed: ${a} !== ${b}; ${msg}`);
13+ }
14+}
15+
16+export function checkKeyedElements(container, saved) {
17+ const children = container.querySelectorAll(".keyed");
18+ for (const child of children) {
19+ console.log(`checking child=${child.outerHTML}`);
20+ const original = saved.get(child.id);
21+ if (original) {
22+ assertEq(original, child, `did not preserve child with key=${child.id}`);
23+ }
24+ }
25+}
tests/web/keyed.rsnew file+269
@@ -0,0 +1,269 @@
1+use crate::{assert_rendered, create_element};
2+use dodrio::{builder::*, bumpalo, Node, Render, RenderContext, Vdom};
3+use log::*;
4+use std::rc::Rc;
5+use wasm_bindgen::prelude::*;
6+use wasm_bindgen_test::*;
7+
8+struct Keyed(u16);
9+
10+impl<'a> Render<'a> for Keyed {
11+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
12+ let key = bumpalo::format!(in cx.bump, "{}", self.0).into_bump_str();
13+ div(&cx)
14+ .attr("class", "keyed")
15+ .attr("id", key)
16+ .key(self.0 as u32)
17+ .finish()
18+ }
19+}
20+
21+fn keyed<'a, Keys>(cx: &mut RenderContext<'a>, keys: Keys) -> Node<'a>
22+where
23+ Keys: AsRef<[u16]>,
24+{
25+ let mut parent = div(&cx).attr("class", "parent");
26+
27+ for &k in keys.as_ref() {
28+ parent = parent.child(Keyed(k).render(cx));
29+ }
30+
31+ parent.finish()
32+}
33+
34+async fn assert_keyed<Before, After>(before: Before, after: After) -> Result<(), JsValue>
35+where
36+ Before: 'static + for<'a> Render<'a>,
37+ After: 'static + for<'a> Render<'a>,
38+{
39+ #[wasm_bindgen(module = "/tests/web/keyed.js")]
40+ extern "C" {
41+ #[wasm_bindgen(js_name = saveKeyedElements)]
42+ fn save_keyed_elements(container: &web_sys::Element) -> JsValue;
43+
44+ #[wasm_bindgen(js_name = checkKeyedElements)]
45+ fn check_keyed_elements(container: &web_sys::Element, saved: JsValue);
46+ }
47+
48+ let container = create_element("div");
49+
50+ let before = Rc::new(before);
51+ let after = Rc::new(after);
52+
53+ debug!("====== Rendering the *before* DOM into the physical DOM ======");
54+ let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
55+ let _vdom2 = vdom1.clone();
56+ let saved = save_keyed_elements(&container);
57+
58+ debug!("====== Checking the *before* DOM against the physical DOM ======");
59+ assert_rendered(&container, &before);
60+
61+ debug!("====== Rendering the *after* DOM into the physical DOM ======");
62+ let weak = vdom1.weak();
63+ weak.set_component(Box::new(after.clone()))
64+ .await
65+ .map_err(|e| e.to_string())?;
66+
67+ debug!("====== Checking the *after* DOM against the physical DOM ======");
68+ assert_rendered(&container, &after);
69+ check_keyed_elements(&container, saved);
70+
71+ Ok(())
72+}
73+
74+macro_rules! keyed_tests {
75+ ( $(
76+ $name:ident {
77+ before($before_cx:ident) {
78+ $( $before:tt )*
79+ }
80+ after($after_cx:ident) {
81+ $( $after:tt )*
82+ }
83+ }
84+ )* ) => {
85+ $(
86+ #[wasm_bindgen_test]
87+ async fn $name() {
88+ use crate::RenderFn;
89+ log::debug!("############### {} ###############", stringify!($name));
90+ assert_keyed(
91+ RenderFn(|$before_cx| { $( $before )* }),
92+ RenderFn(|$after_cx| { $( $after )* }),
93+ )
94+ .await
95+ .unwrap()
96+ }
97+ )*
98+ }
99+}
100+
101+keyed_tests! {
102+ same_order {
103+ before(cx) {
104+ keyed(cx, [1, 2, 3])
105+ }
106+ after(cx) {
107+ keyed(cx, [1, 2, 3])
108+ }
109+ }
110+
111+ same_order_append {
112+ before(cx) {
113+ keyed(cx, [1])
114+ }
115+ after(cx) {
116+ keyed(cx, [1, 2])
117+ }
118+ }
119+
120+ same_order_delete {
121+ before(cx) {
122+ keyed(cx, [1, 2, 3])
123+ }
124+ after(cx) {
125+ keyed(cx, [1, 2])
126+ }
127+ }
128+
129+ same_suffix {
130+ before(cx) {
131+ keyed(cx, [1, 2, 3])
132+ }
133+ after(cx) {
134+ keyed(cx, [4, 2, 3])
135+ }
136+ }
137+
138+ same_prefix_and_suffix_reorder_middle {
139+ before(cx) {
140+ keyed(cx, [1, 2, 3, 4])
141+ }
142+ after(cx) {
143+ keyed(cx, [1, 3, 2, 4])
144+ }
145+ }
146+
147+ same_prefix_and_suffix_new_middle {
148+ before(cx) {
149+ keyed(cx, [2, 3, 4, 5])
150+ }
151+ after(cx) {
152+ keyed(cx, [2, 7, 8, 5])
153+ }
154+ }
155+
156+ reverse_order {
157+ before(cx) {
158+ keyed(cx, [1, 2, 3])
159+ }
160+ after(cx) {
161+ keyed(cx, [3, 2, 1])
162+ }
163+ }
164+
165+ no_shared_keys {
166+ before(cx) {
167+ keyed(cx, [1, 2])
168+ }
169+ after(cx) {
170+ keyed(cx, [3, 4])
171+ }
172+ }
173+
174+ new_keys_in_middle {
175+ before(cx) {
176+ keyed(cx, [1, 2])
177+ }
178+ after(cx) {
179+ keyed(cx, [1, 3, 4, 2])
180+ }
181+ }
182+
183+ new_keys_at_start_and_end {
184+ before(cx) {
185+ keyed(cx, [1, 2, 3])
186+ }
187+ after(cx) {
188+ keyed(cx, [4, 1, 2, 3, 5])
189+ }
190+ }
191+
192+ delete_prefix {
193+ before(cx) {
194+ keyed(cx, [1, 2, 3])
195+ }
196+ after(cx) {
197+ keyed(cx, [2, 3])
198+ }
199+ }
200+
201+ delete_suffix {
202+ before(cx) {
203+ keyed(cx, [1, 2, 3])
204+ }
205+ after(cx) {
206+ keyed(cx, [1, 2])
207+ }
208+ }
209+
210+ delete_middle {
211+ before(cx) {
212+ keyed(cx, [1, 2, 3])
213+ }
214+ after(cx) {
215+ keyed(cx, [1, 3])
216+ }
217+ }
218+
219+ nested_keyed_children {
220+ before(cx) {
221+ ul(&cx)
222+ .children([
223+ li(&cx)
224+ .key(1)
225+ .children([
226+ keyed(cx, [2, 3, 4])
227+ ])
228+ .finish(),
229+ li(&cx)
230+ .key(5)
231+ .children([
232+ keyed(cx, [6, 7, 8])
233+ ])
234+ .finish(),
235+ li(&cx)
236+ .key(9)
237+ .children([
238+ keyed(cx, [10, 11, 12])
239+ ])
240+ .finish(),
241+ ])
242+ .finish()
243+ }
244+ after(cx) {
245+ ul(&cx)
246+ .children([
247+ li(&cx)
248+ .key(9)
249+ .children([
250+ keyed(cx, [12, 11, 10])
251+ ])
252+ .finish(),
253+ li(&cx)
254+ .key(5)
255+ .children([
256+ keyed(cx, [8, 7, 6])
257+ ])
258+ .finish(),
259+ li(&cx)
260+ .key(1)
261+ .children([
262+ keyed(cx, [4, 3, 2])
263+ ])
264+ .finish(),
265+ ])
266+ .finish()
267+ }
268+ }
269+}
tests/web/main.rs+60−37
@@ -1,11 +1,14 @@
11//! Test suite for the Web and headless browsers.
22
3−#![cfg(target_arch = "wasm32")]
3+#![cfg(all(feature = "xxx-unstable-internal-use-only", target_arch = "wasm32"))]
44
55use bumpalo::Bump;
6−use dodrio::{Attribute, Node, Render, Vdom};
7−use futures::prelude::*;
6+use dodrio::{
7+ Attribute, CachedSet, ElementNode, Node, NodeKind, Render, RenderContext, TextNode, Vdom,
8+};
9+use fxhash::FxHashMap;
810use log::*;
11+use std::cell::RefCell;
912use std::rc::Rc;
1013use wasm_bindgen::prelude::*;
1114use wasm_bindgen::JsCast;
@@ -16,6 +19,7 @@ wasm_bindgen_test_configure!(run_in_browser);
1619pub mod cached;
1720pub mod events;
1821pub mod js_api;
22+pub mod keyed;
1923pub mod render;
2024
2125pub fn window() -> web_sys::Window {
@@ -46,15 +50,20 @@ pub fn init_logging() {
4650
4751/// Assert that the `container` contains the physical DOM tree that matches
4852/// `r`'s rendered virtual DOM.
49−pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
53+pub fn assert_rendered<R: for<'a> Render<'a>>(container: &web_sys::Element, r: &R) {
5054 init_logging();
5155
56+ let cached_set = &RefCell::new(CachedSet::default());
5257 let bump = &Bump::new();
53− let node = r.render(bump);
58+ let templates = &mut FxHashMap::default();
59+ let cx = &mut RenderContext::new(bump, cached_set, templates);
60+ let node = r.render(cx);
5461 let child = container
5562 .first_child()
5663 .expect("container does not have anything rendered into it?");
57− check_node(&child, &node);
64+
65+ let cached_set = cached_set.borrow();
66+ check_node(&cached_set, &child, &node);
5867
5968 fn stringify_actual_node(n: &web_sys::Node) -> String {
6069 if let Some(el) = n.dyn_ref::<web_sys::Element>() {
@@ -64,12 +73,12 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
6473 }
6574 }
6675
67− fn check_node(actual: &web_sys::Node, expected: &Node) {
76+ fn check_node(cached_set: &CachedSet, actual: &web_sys::Node, expected: &Node) {
6877 debug!("check_render:");
6978 debug!(" actual = {}", stringify_actual_node(&actual));
7079 debug!(" expected = {:#?}", expected);
71− match expected {
72− Node::Text(text_node) => {
80+ match expected.kind {
81+ NodeKind::Text(TextNode { text }) => {
7382 assert_eq!(
7483 actual.node_name().to_uppercase(),
7584 "#TEXT",
@@ -77,21 +86,34 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
7786 );
7887 assert_eq!(
7988 actual.text_content().unwrap_or_default(),
80− text_node.text(),
89+ text,
8190 "actual.text_content() == expected.text()"
8291 );
8392 }
84− Node::Element(elem) => {
93+ NodeKind::Element(&ElementNode {
94+ tag_name,
95+ attributes,
96+ children,
97+ namespace,
98+ ..
99+ }) => {
85100 assert_eq!(
86101 actual.node_name().to_uppercase(),
87− elem.tag_name().to_uppercase(),
102+ tag_name.to_uppercase(),
88103 "actual.node_name() == expected.tag_name()"
89104 );
90105 let actual = actual
91106 .dyn_ref::<web_sys::Element>()
92107 .expect("`actual` should be an `Element`");
93− check_attributes(actual.attributes(), elem.attributes());
94− check_children(actual.child_nodes(), elem.children());
108+ check_attributes(actual.attributes(), attributes);
109+ check_children(cached_set, actual.child_nodes(), children);
110+ if let Some(namespace) = namespace {
111+ assert_eq!(actual.namespace_uri(), Some(namespace.into()))
112+ }
113+ }
114+ NodeKind::Cached(ref c) => {
115+ let (expected, _template) = cached_set.get(c.id);
116+ check_node(cached_set, actual, &expected);
95117 }
96118 }
97119 }
@@ -115,7 +137,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
115137 }
116138 }
117139
118− fn check_children(actual: web_sys::NodeList, expected: &[Node]) {
140+ fn check_children(cached_set: &CachedSet, actual: web_sys::NodeList, expected: &[Node]) {
119141 assert_eq!(
120142 actual.length(),
121143 expected.len() as u32,
@@ -123,7 +145,7 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
123145 );
124146 for (i, child) in expected.iter().enumerate() {
125147 let actual_child = actual.item(i as u32).unwrap();
126− check_node(&actual_child, child);
148+ check_node(cached_set, &actual_child, child);
127149 }
128150 }
129151}
@@ -131,27 +153,24 @@ pub fn assert_rendered<R: Render>(container: &web_sys::Element, r: &R) {
131153/// Use the function `F` to render.
132154pub struct RenderFn<F>(F)
133155where
134− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>;
156+ F: for<'a> Fn(&mut RenderContext<'a>) -> Node<'a>;
135157
136−impl<F> Render for RenderFn<F>
158+impl<'a, F> Render<'a> for RenderFn<F>
137159where
138− F: for<'bump> Fn(&'bump Bump) -> Node<'bump>,
160+ F: for<'b> Fn(&mut RenderContext<'b>) -> Node<'b>,
139161{
140− fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
141− where
142− 'a: 'bump,
143− {
144− (self.0)(bump)
162+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
163+ (self.0)(cx)
145164 }
146165}
147166
148167/// Assert that if we start by rendering the `before` virtual DOM tree into a
149168/// physical DOM tree, and then diff it with the `after` virtual DOM tree, then
150169/// the physical DOM tree correctly matches `after`.
151−pub fn assert_before_after<R, S>(before: R, after: S) -> impl Future<Item = (), Error = JsValue>
170+pub async fn assert_before_after<R, S>(before: R, after: S) -> Result<(), JsValue>
152171where
153− R: 'static + Render,
154− S: 'static + Render,
172+ R: 'static + for<'a> Render<'a>,
173+ S: 'static + for<'a> Render<'a>,
155174{
156175 let container = create_element("div");
157176
@@ -160,7 +179,7 @@ where
160179
161180 debug!("====== Rendering the *before* DOM into the physical DOM ======");
162181 let vdom1 = Rc::new(Vdom::new(&container, before.clone()));
163− let vdom2 = vdom1.clone();
182+ let _vdom2 = vdom1.clone();
164183
165184 debug!("====== Checking the *before* DOM against the physical DOM ======");
166185 assert_rendered(&container, &before);
@@ -168,15 +187,16 @@ where
168187 debug!("====== Rendering the *after* DOM into the physical DOM ======");
169188 let weak = vdom1.weak();
170189 weak.set_component(Box::new(after.clone()))
171− .map(move |_| {
172− debug!("====== Checking the *after* DOM against the physical DOM ======");
173− assert_rendered(&container, &after);
174− drop(vdom2);
175− })
176− .map_err(|e| JsValue::from(e.to_string()))
190+ .await
191+ .map_err(|e| e.to_string())?;
192+
193+ debug!("====== Checking the *after* DOM against the physical DOM ======");
194+ assert_rendered(&container, &after);
195+
196+ Ok(())
177197}
178198
179−/// A helper macro for decalaring a bunch of `assert_before_after` tests.
199+/// A helper macro for declaring a bunch of `assert_before_after` tests.
180200#[macro_export]
181201macro_rules! before_after {
182202 ( $(
@@ -190,13 +210,16 @@ macro_rules! before_after {
190210 }
191211 )* ) => {
192212 $(
193− #[wasm_bindgen_test(async)]
194− fn $name() -> impl Future<Item = (), Error = wasm_bindgen::JsValue> {
213+ #[wasm_bindgen_test]
214+ async fn $name() {
195215 use crate::{assert_before_after, RenderFn};
216+ log::debug!("############### {} ###############", stringify!($name));
196217 assert_before_after(
197218 RenderFn(|$before_bump| { $( $before )* }),
198219 RenderFn(|$after_bump| { $( $after )* })
199220 )
221+ .await
222+ .unwrap()
200223 }
201224 )*
202225 }
tests/web/render.rs+183−83
@@ -1,12 +1,12 @@
11use super::{assert_rendered, before_after, create_element, RenderFn};
2−use dodrio::{builder::*, Vdom};
3−use futures::prelude::*;
2+use dodrio::{builder::*, bumpalo::collections::String, Node, Render, RenderContext, Vdom};
43use std::rc::Rc;
4+use wasm_bindgen::{ JsCast};
55use wasm_bindgen_test::*;
66
77#[wasm_bindgen_test]
88fn render_initial_text() {
9− let hello = Rc::new(RenderFn(|_bump| text("hello")));
9+ let hello = Rc::new(RenderFn(|_cx| text("hello")));
1010
1111 let container = create_element("div");
1212 let _vdom = Vdom::new(&container, hello.clone());
@@ -15,10 +15,10 @@ fn render_initial_text() {
1515
1616#[wasm_bindgen_test]
1717fn render_initial_node() {
18− let hello = Rc::new(RenderFn(|bump| {
19− div(bump)
18+ let hello = Rc::new(RenderFn(|cx| {
19+ div(&cx)
2020 .attr("id", "hello-world")
21− .children([text("Hello "), span(bump).child(text("World!")).finish()])
21+ .children([text("Hello "), span(&cx).child(text("World!")).finish()])
2222 .finish()
2323 }));
2424
@@ -30,96 +30,183 @@ fn render_initial_node() {
3030#[wasm_bindgen_test]
3131fn container_is_emptied_upon_drop() {
3232 let container = create_element("div");
33− let vdom = Vdom::new(&container, RenderFn(|_bump| text("blah")));
33+ let vdom = Vdom::new(&container, RenderFn(|_cx| text("blah")));
3434 drop(vdom);
3535 assert!(container.first_child().is_none());
3636}
3737
38+/// Renders a child with a lifetime scoped to the RenderContext bump arena.
39+#[wasm_bindgen_test]
40+fn render_bump_scoped_node() {
41+ struct Child<'a> {
42+ name: &'a str,
43+ }
44+
45+ impl<'a> Render<'a> for Child<'a> {
46+ fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> {
47+ text(self.name)
48+ }
49+ }
50+
51+ struct Parent;
52+
53+ impl<'a> Render<'a> for Parent {
54+ fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
55+ let child_name = String::from_str_in("child", cx.bump).into_bump_str();
56+
57+ div(&cx)
58+ .children([Child { name: child_name }.render(cx)])
59+ .finish()
60+ }
61+ }
62+
63+ let parent = Rc::new(RenderFn(|cx| {
64+ Parent.render(cx)
65+ }));
66+
67+ let container = create_element("div");
68+ let _vdom = Vdom::new(&container, parent.clone());
69+
70+ assert_rendered(&container, &parent);
71+}
72+
73+/// Originally, dodrio would use the className property for SVGs.
74+///
75+/// This is problematic because when SVG elements are created, the className is flagged as a read
76+/// only property, so setting it causes an exception to be thrown. Here's an example of how this
77+/// happens:
78+///
79+/// let elem = web_sys::window()
80+/// .unwrap()
81+/// .document()
82+/// .unwrap()
83+/// .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg")
84+/// .unwrap();
85+///
86+/// elem.set_class_name("does-not-work");
87+///
88+/// -----------------------------------------------------------------------------------------------
89+///
90+/// wasm-bindgen: imported JS function that was not marked as `catch` threw an error:
91+/// setting getter-only property "className"
92+///
93+/// -----------------------------------------------------------------------------------------------
94+///
95+/// Now, dodrio passes the 'class' attribute of all namespaced elements into set_attribute. This
96+/// satisfies the restrictions on SVG and keeps the optimized path for non-namespaced elements
97+#[wasm_bindgen_test(async)]
98+async fn test_svg_set_class() {
99+ let container = create_element("div");
100+
101+
102+ let valid_svg = Rc::new(RenderFn(|cx| {
103+ ElementBuilder::new(cx.bump, "svg")
104+ .namespace(Some("http://www.w3.org/2000/svg"))
105+ .attr("class", "works")
106+ .finish()
107+ }));
108+
109+ let vdom = Vdom::new(&container, valid_svg.clone());
110+ let weak = vdom.weak();
111+
112+ weak.render().await.unwrap();
113+
114+ assert_eq!(
115+ "works",
116+ container.first_child()
117+ .expect("unable to get svg")
118+ .dyn_ref::<web_sys::Element>()
119+ .expect("svg should be an element")
120+ .get_attribute("class")
121+ .expect("unable to get 'class' of svg")
122+ );
123+}
124+
38125before_after! {
39126 same_text {
40− before(_bump) {
127+ before(_cx) {
41128 text("hello")
42129 }
43− after(_bump) {
130+ after(_cx) {
44131 text("hello")
45132 }
46133 }
47134
48135 update_text {
49− before(_bump) {
136+ before(_cx) {
50137 text("before")
51138 }
52− after(_bump) {
139+ after(_cx) {
53140 text("after")
54141 }
55142 }
56143
57144 replace_text_with_elem {
58− before(_bump) {
145+ before(_cx) {
59146 text("before")
60147 }
61− after(bump) {
62− div(bump).finish()
148+ after(cx) {
149+ div(&cx).finish()
63150 }
64151 }
65152
66153 replace_elem_with_text {
67− before(bump) {
68− div(bump).finish()
154+ before(cx) {
155+ div(&cx).finish()
69156 }
70− after(_bump) {
157+ after(_cx) {
71158 text("before")
72159 }
73160 }
74161
75162 same_elem {
76− before(bump) {
77− div(bump).finish()
163+ before(cx) {
164+ div(&cx).finish()
78165 }
79− after(bump) {
80− div(bump).finish()
166+ after(cx) {
167+ div(&cx).finish()
81168 }
82169 }
83170
84171 elems_with_different_tag_names {
85− before(bump) {
86− span(bump).finish()
172+ before(cx) {
173+ span(&cx).finish()
87174 }
88− after(bump) {
89− div(bump).finish()
175+ after(cx) {
176+ div(&cx).finish()
90177 }
91178 }
92179
93180 same_tag_name_update_attribute {
94− before(bump) {
95− div(bump).attr("value", "1").finish()
181+ before(cx) {
182+ div(&cx).attr("value", "1").finish()
96183 }
97− after(bump) {
98− div(bump).attr("value", "2").finish()
184+ after(cx) {
185+ div(&cx).attr("value", "2").finish()
99186 }
100187 }
101188
102189 same_tag_name_remove_attribute {
103− before(bump) {
104− div(bump).attr("value", "1").finish()
190+ before(cx) {
191+ div(&cx).attr("value", "1").finish()
105192 }
106− after(bump) {
107− div(bump).finish()
193+ after(cx) {
194+ div(&cx).finish()
108195 }
109196 }
110197
111198 same_tag_name_add_attribute {
112− before(bump) {
113− div(bump).finish()
199+ before(cx) {
200+ div(&cx).finish()
114201 }
115− after(bump) {
116− div(bump).attr("value", "2").finish()
202+ after(cx) {
203+ div(&cx).attr("value", "2").finish()
117204 }
118205 }
119206
120207 same_tag_name_many_attributes {
121− before(bump) {
122− div(bump)
208+ before(cx) {
209+ div(&cx)
123210 .attr("before-1", "1")
124211 .attr("shared-1", "1")
125212 .attr("modified-1", "1")
@@ -131,8 +218,8 @@ before_after! {
131218 .attr("modified-3", "3")
132219 .finish()
133220 }
134− after(bump) {
135− div(bump)
221+ after(cx) {
222+ div(&cx)
136223 .attr("after-1", "1")
137224 .attr("shared-1", "1")
138225 .attr("modified-1", "100")
@@ -147,97 +234,110 @@ before_after! {
147234 }
148235
149236 same_tag_same_children {
150− before(bump) {
151− div(bump).child(text("child")).finish()
237+ before(cx) {
238+ div(&cx).child(text("child")).finish()
152239 }
153− after(bump) {
154− div(bump).child(text("child")).finish()
240+ after(cx) {
241+ div(&cx).child(text("child")).finish()
155242 }
156243 }
157244
158245 same_tag_update_child {
159− before(bump) {
160− div(bump).child(text("before")).finish()
246+ before(cx) {
247+ div(&cx).child(text("before")).finish()
161248 }
162− after(bump) {
163− div(bump).child(text("after")).finish()
249+ after(cx) {
250+ div(&cx).child(text("after")).finish()
164251 }
165252 }
166253
167254 same_tag_add_child {
168− before(bump) {
169− div(bump).finish()
255+ before(cx) {
256+ div(&cx).finish()
170257 }
171− after(bump) {
172− div(bump).child(text("child")).finish()
258+ after(cx) {
259+ div(&cx).child(text("child")).finish()
173260 }
174261 }
175262
176263 same_tag_remove_child {
177− before(bump) {
178− div(bump).child(text("child")).finish()
264+ before(cx) {
265+ div(&cx).child(text("child")).finish()
179266 }
180− after(bump) {
181− div(bump).finish()
267+ after(cx) {
268+ div(&cx).finish()
182269 }
183270 }
184271
185272 same_tag_update_many_children {
186− before(bump) {
187− div(bump)
273+ before(cx) {
274+ div(&cx)
188275 .children([
189− div(bump).finish(),
190− span(bump).finish(),
191− p(bump).finish(),
276+ div(&cx).finish(),
277+ span(&cx).finish(),
278+ p(&cx).finish(),
192279 ])
193280 .finish()
194281 }
195− after(bump) {
196− div(bump)
282+ after(cx) {
283+ div(&cx)
197284 .children([
198− span(bump).finish(),
199− p(bump).finish(),
200− div(bump).finish(),
285+ span(&cx).finish(),
286+ p(&cx).finish(),
287+ div(&cx).finish(),
201288 ])
202289 .finish()
203290 }
204291 }
205292
206293 same_tag_remove_many_children {
207− before(bump) {
208− div(bump)
294+ before(cx) {
295+ div(&cx)
209296 .children([
210− div(bump).finish(),
211− span(bump).finish(),
212− p(bump).finish(),
297+ div(&cx).finish(),
298+ span(&cx).finish(),
299+ p(&cx).finish(),
213300 ])
214301 .finish()
215302 }
216− after(bump) {
217− div(bump)
303+ after(cx) {
304+ div(&cx)
218305 .children([
219− div(bump).finish(),
306+ div(&cx).finish(),
220307 ])
221308 .finish()
222309 }
223310 }
224311
225312 same_tag_add_many_children {
226− before(bump) {
227− div(bump)
313+ before(cx) {
314+ div(&cx)
228315 .children([
229− div(bump).finish(),
316+ div(&cx).finish(),
230317 ])
231318 .finish()
232319 }
233− after(bump) {
234− div(bump)
320+ after(cx) {
321+ div(&cx)
235322 .children([
236− div(bump).finish(),
237− span(bump).finish(),
238− p(bump).finish(),
323+ div(&cx).finish(),
324+ span(&cx).finish(),
325+ p(&cx).finish(),
239326 ])
240327 .finish()
241328 }
242329 }
330+
331+ same_tag_different_namespace {
332+ before(cx) {
333+ div(&cx)
334+ .namespace(Some("http://example.com"))
335+ .finish()
336+ }
337+ after(cx) {
338+ div(&cx)
339+ .namespace(Some("http://example.net"))
340+ .finish()
341+ }
342+ }
243343}