├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── attrs.rs ├── lib.rs ├── node.rs ├── tokenizer.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .vscode 3 | www 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "anyhow" 5 | version = "1.0.26" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "brunhild" 10 | version = "0.6.1" 11 | dependencies = [ 12 | "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", 13 | "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", 15 | ] 16 | 17 | [[package]] 18 | name = "bumpalo" 19 | version = "3.1.2" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | 22 | [[package]] 23 | name = "cfg-if" 24 | version = "0.1.10" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | 27 | [[package]] 28 | name = "heck" 29 | version = "0.3.1" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | dependencies = [ 32 | "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 33 | ] 34 | 35 | [[package]] 36 | name = "js-sys" 37 | version = "0.3.35" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | dependencies = [ 40 | "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 41 | ] 42 | 43 | [[package]] 44 | name = "lazy_static" 45 | version = "1.4.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | 48 | [[package]] 49 | name = "log" 50 | version = "0.4.8" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | dependencies = [ 53 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 54 | ] 55 | 56 | [[package]] 57 | name = "memchr" 58 | version = "2.3.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | 61 | [[package]] 62 | name = "nom" 63 | version = "4.2.3" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | dependencies = [ 66 | "memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 67 | "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 68 | ] 69 | 70 | [[package]] 71 | name = "proc-macro2" 72 | version = "1.0.8" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | dependencies = [ 75 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 76 | ] 77 | 78 | [[package]] 79 | name = "quote" 80 | version = "1.0.2" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | dependencies = [ 83 | "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", 84 | ] 85 | 86 | [[package]] 87 | name = "sourcefile" 88 | version = "0.1.4" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | 91 | [[package]] 92 | name = "syn" 93 | version = "1.0.14" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | dependencies = [ 96 | "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", 97 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 98 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 99 | ] 100 | 101 | [[package]] 102 | name = "unicode-segmentation" 103 | version = "1.6.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | 106 | [[package]] 107 | name = "unicode-xid" 108 | version = "0.2.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | 111 | [[package]] 112 | name = "version_check" 113 | version = "0.1.5" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | 116 | [[package]] 117 | name = "wasm-bindgen" 118 | version = "0.2.58" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | dependencies = [ 121 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 122 | "wasm-bindgen-macro 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 123 | ] 124 | 125 | [[package]] 126 | name = "wasm-bindgen-backend" 127 | version = "0.2.58" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | dependencies = [ 130 | "bumpalo 3.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 131 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 132 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 133 | "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", 134 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 135 | "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", 136 | "wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 137 | ] 138 | 139 | [[package]] 140 | name = "wasm-bindgen-macro" 141 | version = "0.2.58" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | dependencies = [ 144 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 145 | "wasm-bindgen-macro-support 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 146 | ] 147 | 148 | [[package]] 149 | name = "wasm-bindgen-macro-support" 150 | version = "0.2.58" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | dependencies = [ 153 | "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", 154 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 155 | "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", 156 | "wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 157 | "wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 158 | ] 159 | 160 | [[package]] 161 | name = "wasm-bindgen-shared" 162 | version = "0.2.58" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | 165 | [[package]] 166 | name = "wasm-bindgen-webidl" 167 | version = "0.2.58" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | dependencies = [ 170 | "anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", 171 | "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", 172 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 173 | "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", 174 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 175 | "syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", 176 | "wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 177 | "weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", 178 | ] 179 | 180 | [[package]] 181 | name = "web-sys" 182 | version = "0.3.35" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | dependencies = [ 185 | "anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)", 186 | "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", 187 | "sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 188 | "wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 189 | "wasm-bindgen-webidl 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)", 190 | ] 191 | 192 | [[package]] 193 | name = "weedle" 194 | version = "0.10.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | dependencies = [ 197 | "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 198 | ] 199 | 200 | [metadata] 201 | "checksum anyhow 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)" = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" 202 | "checksum bumpalo 3.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fb8038c1ddc0a5f73787b130f4cc75151e96ed33e417fde765eb5a81e3532f4" 203 | "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 204 | "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 205 | "checksum js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9" 206 | "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 207 | "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 208 | "checksum memchr 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" 209 | "checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" 210 | "checksum proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" 211 | "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 212 | "checksum sourcefile 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf77cb82ba8453b42b6ae1d692e4cdc92f9a47beaf89a847c8be83f4e328ad3" 213 | "checksum syn 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" 214 | "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 215 | "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 216 | "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 217 | "checksum wasm-bindgen 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "5205e9afdf42282b192e2310a5b463a6d1c1d774e30dc3c791ac37ab42d2616c" 218 | "checksum wasm-bindgen-backend 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "11cdb95816290b525b32587d76419facd99662a07e59d3cdb560488a819d9a45" 219 | "checksum wasm-bindgen-macro 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "574094772ce6921576fb6f2e3f7497b8a76273b6db092be18fc48a082de09dc3" 220 | "checksum wasm-bindgen-macro-support 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "e85031354f25eaebe78bb7db1c3d86140312a911a106b2e29f9cc440ce3e7668" 221 | "checksum wasm-bindgen-shared 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "f5e7e61fc929f4c0dddb748b102ebf9f632e2b8d739f2016542b4de2965a9601" 222 | "checksum wasm-bindgen-webidl 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "ef012a0d93fc0432df126a8eaf547b2dce25a8ce9212e1d3cbeef5c11157975d" 223 | "checksum web-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "aaf97caf6aa8c2b1dac90faf0db529d9d63c93846cca4911856f78a83cebf53b" 224 | "checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" 225 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brunhild" 3 | version = "0.6.1" 4 | authors = ["bakape "] 5 | edition = "2018" 6 | description = "experimental compressive Rust virtual DOM library" 7 | repository = "https://github.com/bakape/brunhild.git" 8 | license = "MIT" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [dependencies] 14 | js-sys = "0.3.1" 15 | wasm-bindgen = "0.2.54" 16 | 17 | [dependencies.web-sys] 18 | version = "0.3.31" 19 | features = [ 20 | 'Document', 21 | 'Window', 22 | 'HtmlElement', 23 | 'Element', 24 | 'Node', 25 | ] 26 | 27 | [profile.release] 28 | opt-level = 3 29 | debug = false 30 | rpath = false 31 | lto = true 32 | debug-assertions = false 33 | codegen-units = 1 34 | panic = 'abort' 35 | incremental = false 36 | overflow-checks = false 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 bakape 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # brunhild 2 | experimental compressive Rust virtual DOM library 3 | 4 | Brunhild aims to provide a minimalistic fast virtual DOM implementation for use 5 | as is or for building higher level (for example, view-based) libraries and 6 | frameworks. 7 | 8 | Brunhild's core principle is reduction of allocations and indirection by 9 | internally converting string values to integers, that reference a value in 10 | either a static lookup table of common HTML strings or dynamically populated 11 | global table. This enables most value comparisons and building of element Node 12 | trees to be done much more cheaply. 13 | 14 | Brunhild is mostly referenceless in relation to the DOM. Many virtual DOM 15 | libraries create one to one Node <-> DOM Element mappings on Node construction. 16 | Brunhild only performs this, when a DOM Element mutation is required. This 17 | allows to cheaply patch in large subtree changes as HTML strings, reducing FFI 18 | overhead. This is achieved by setting DOM Element IDs and storing those 19 | efficiently as integers on the Node. As a result brunhild does not support 20 | setting the ID attribute by the library user. Please use classes instead for 21 | such purposes. 22 | -------------------------------------------------------------------------------- /src/attrs.rs: -------------------------------------------------------------------------------- 1 | use super::tokenizer; 2 | use super::util; 3 | 4 | use std::collections::BTreeMap; 5 | use std::fmt; 6 | use wasm_bindgen::JsValue; 7 | 8 | // Attribute keys that have limited set of values and thus can have their 9 | // values tokenized. 10 | // Sorted for binary search. 11 | // 12 | // Sourced from: 13 | // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes 14 | static TOKENIZABLE_VALUES: [&'static str; 35] = [ 15 | "async", 16 | "autocapitalize", 17 | "autocomplete", 18 | "autofocus", 19 | "autoplay", 20 | "checked", 21 | "class", 22 | "contenteditable", 23 | "controls", 24 | "crossorigin", 25 | "decoding", 26 | "defer", 27 | "dir", 28 | "disabled", 29 | "draggable", 30 | "dropzone", 31 | "hidden", 32 | "language", 33 | "loop", 34 | "method", 35 | "multiple", 36 | "muted", 37 | "novalidate", 38 | "open", 39 | "preload", 40 | "readonly", 41 | "referrerpolicy", 42 | "required", 43 | "reversed", 44 | "sandbox", 45 | "selected", 46 | "spellcheck", 47 | "translate", 48 | "type", 49 | "wrap", 50 | ]; 51 | 52 | // Compressed attribute storage with manipulation functions 53 | #[derive(Default, Debug)] 54 | pub struct Attrs(BTreeMap); 55 | 56 | // Contains a value stored in one of 2 storage methods for attribute values 57 | #[derive(PartialEq, Eq, Debug)] 58 | enum Value { 59 | // Tokenized string value 60 | StringToken(u16), 61 | 62 | // Untokenized string. Used to store values too dynamic to benefit from 63 | // tokenization in most use cases. 64 | Untokenized(String), 65 | } 66 | 67 | impl Attrs { 68 | // Create empty attribute map 69 | // TODO: Make generic with Into 70 | #[inline] 71 | pub fn new(arr: &[(&str, &str)]) -> Self { 72 | Self( 73 | arr.iter() 74 | .map(|(key, val)| { 75 | ( 76 | tokenizer::tokenize(key), 77 | if *val == "" { 78 | Value::StringToken(0) 79 | } else { 80 | match TOKENIZABLE_VALUES.binary_search(&key) { 81 | Ok(_) => { 82 | Value::StringToken(tokenizer::tokenize(val)) 83 | } 84 | _ => Value::Untokenized(String::from(*val)), 85 | } 86 | }, 87 | ) 88 | }) 89 | .collect(), 90 | ) 91 | } 92 | 93 | // Diff and patch attributes against new set and write changes to the DOM 94 | pub fn patch( 95 | &mut self, 96 | el: &mut util::LazyElement, 97 | new: Attrs, 98 | ) -> Result<(), JsValue> { 99 | // Attributes removed 100 | let mut to_remove = Vec::::new(); 101 | for k in self.0.keys() { 102 | if new.0.contains_key(k) { 103 | continue; 104 | } 105 | 106 | to_remove.push(*k); 107 | match el.get() { 108 | Ok(el) => { 109 | tokenizer::get_value(*k, |key| el.remove_attribute(key)) 110 | } 111 | Err(e) => Err(e), 112 | }?; 113 | } 114 | for k in to_remove { 115 | self.0.remove(&k); 116 | } 117 | 118 | // Attributes added or changed 119 | for (k, v) in new.0.into_iter() { 120 | let mut set = |k: u16, v: &Value| -> Result<(), JsValue> { 121 | match el.get() { 122 | Ok(el) => tokenizer::get_value(k, |key| match v { 123 | Value::StringToken(v) => { 124 | tokenizer::get_value(*v, |value| { 125 | el.set_attribute(key, value) 126 | }) 127 | } 128 | Value::Untokenized(value) => { 129 | el.set_attribute(key, value) 130 | } 131 | }), 132 | Err(e) => Err(e), 133 | } 134 | }; 135 | match self.0.get_mut(&k) { 136 | Some(old_v) => { 137 | if v != *old_v { 138 | set(k, &v)?; 139 | *old_v = v; 140 | } 141 | } 142 | None => { 143 | set(k, &v)?; 144 | self.0.insert(k, v); 145 | } 146 | } 147 | } 148 | 149 | Ok(()) 150 | } 151 | } 152 | 153 | impl util::WriteHTMLTo for Attrs { 154 | fn write_html_to(&mut self, w: &mut W) -> fmt::Result { 155 | for (k, v) in self.0.iter() { 156 | tokenizer::get_value(*k, |s| write!(w, " {}", s))?; 157 | match v { 158 | Value::StringToken(v) => { 159 | if *v != 0 { 160 | tokenizer::get_value(*v, |s| write!(w, "=\"{}\"", s))?; 161 | } 162 | } 163 | Value::Untokenized(s) => { 164 | write!(w, "=\"{}\"", s)?; 165 | } 166 | }; 167 | } 168 | Ok(()) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod attrs; 2 | mod node; 3 | mod tokenizer; 4 | mod util; 5 | 6 | pub use node::{ElementOptions, Node, TextOptions}; 7 | -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | use super::attrs::Attrs; 2 | use super::tokenizer; 3 | use super::util; 4 | use super::util::WriteHTMLTo; 5 | use std::collections::HashMap; 6 | use std::fmt; 7 | use wasm_bindgen::JsValue; 8 | 9 | // Creates a new element node 10 | #[macro_export] 11 | macro_rules! element { 12 | ($tag:expr) => { 13 | $crate::element!{ $tag, &[], vec![]} 14 | }; 15 | ($tag:expr, {$($key:expr => $val:expr,)+}) => { 16 | $crate::element!{ $tag, {$($key => $val,)+}, vec![] } 17 | }; 18 | ($tag:expr, {$($key:expr => $val:expr),+}) => { 19 | $crate::element!{ $tag, {$($key => $val,)+}, vec![] } 20 | }; 21 | ($tag:expr, {$($key:expr => $val:expr,)+}, [$($child:expr,)+]) => { 22 | $crate::element!{ $tag, {$($key => $val,)+}, vec![$($child,)+] } 23 | }; 24 | ($tag:expr, {$($key:expr => $val:expr),+}, [$($child:expr,)+]) => { 25 | $crate::element!{ $tag, {$($key => $val,)+}, vec![$($child,)+] } 26 | }; 27 | ($tag:expr, {$($key:expr => $val:expr),+}, [$($child:expr),+]) => { 28 | $crate::element!{ $tag, {$($key => $val,)+}, vec![$($child,)+] } 29 | }; 30 | ($tag:expr, {$($key:expr => $val:expr),+}, $children:expr) => { 31 | $crate::element!{ $tag, {$($key => $val,)+}, $children } 32 | }; 33 | ($tag:expr, {$($key:expr => $val:expr,)+}, $children:expr) => { 34 | $crate::element!{ 35 | $tag, 36 | &[$(($key.as_ref(), $val.as_ref()),)+], 37 | $children 38 | } 39 | }; 40 | ($tag:expr, $attrs:expr, $children:expr) => { 41 | $crate::Node::with_children( 42 | &$crate::ElementOptions { 43 | tag: $tag.as_ref(), 44 | attrs: $attrs, 45 | ..Default::default() 46 | }, 47 | $children 48 | ) 49 | }; 50 | } 51 | 52 | // Creates a new text node 53 | #[macro_export] 54 | macro_rules! text { 55 | ($text:expr) => { 56 | $crate::Node::text(&TextOptions { 57 | text: $text.as_ref(), 58 | ..Default::default() 59 | }) 60 | }; 61 | } 62 | 63 | // Creates a new text node with HTML escaping 64 | #[macro_export] 65 | macro_rules! escaped { 66 | ($text:expr) => { 67 | $crate::Node::text(&TextOptions { 68 | text: $text.as_ref(), 69 | escape: true, 70 | ..Default::default() 71 | }) 72 | }; 73 | } 74 | 75 | // Internal contents of a text Node or Element 76 | #[derive(Debug)] 77 | enum NodeContents { 78 | Text(String), 79 | Element(ElementContents), 80 | } 81 | 82 | impl Default for NodeContents { 83 | fn default() -> Self { 84 | NodeContents::Element(Default::default()) 85 | } 86 | } 87 | 88 | // Internal contents of an Element 89 | #[derive(Debug)] 90 | struct ElementContents { 91 | // Token for the node's tag 92 | tag: u16, 93 | 94 | // Node attributes, excluding "id" and "class". 95 | // "id" is used internally for node addressing and can not be set. 96 | // to set "class" used the dedicated methods. 97 | attrs: Attrs, 98 | 99 | // Children of Node 100 | children: Vec, 101 | } 102 | 103 | impl Default for ElementContents { 104 | fn default() -> Self { 105 | Self { 106 | tag: tokenizer::tokenize("div"), 107 | attrs: Default::default(), 108 | children: Default::default(), 109 | } 110 | } 111 | } 112 | 113 | // Node used for constructing DOM trees for applying patches and representing 114 | // the browser DOM state. 115 | #[derive(Default, Debug)] 116 | pub struct Node { 117 | // ID of DOM element the node is representing. Can be 0 in nodes not yet 118 | // patched into the DOM. 119 | id: u64, 120 | 121 | // Key used to identify the same node, during potentially destructive 122 | // patching. Only set, if this node requires persistance, like maintaining 123 | // user input focus or selections. 124 | key: Option, 125 | 126 | contents: NodeContents, 127 | 128 | // Lazy getter for corresponding JS Element object 129 | element: util::LazyElement, 130 | } 131 | 132 | // Options for constructing an Element Node. This struct has separate lifetimes 133 | // for each field, so that some of these can have static lifetimes and thus not 134 | // require runtime allocation. 135 | #[derive(Debug)] 136 | pub struct ElementOptions<'t, 'a> { 137 | // Element HTML tag 138 | pub tag: &'t str, 139 | 140 | // Kee used to identify the same node, during potentially destructive 141 | // patching. Only set, if this node requires persistance, like maintaining 142 | // user input focus or selections. 143 | pub key: Option, 144 | 145 | // List of element attributes 146 | pub attrs: &'a [(&'a str, &'a str)], 147 | } 148 | 149 | impl<'t, 'a> Default for ElementOptions<'t, 'a> { 150 | fn default() -> Self { 151 | Self { 152 | tag: "div", 153 | key: None, 154 | attrs: &[], 155 | } 156 | } 157 | } 158 | 159 | // Options for constructing a text Node 160 | #[derive(Debug)] 161 | pub struct TextOptions<'a> { 162 | // HTML-escape inner text 163 | pub escape: bool, 164 | 165 | // Element text content 166 | pub text: &'a str, 167 | 168 | // Kee used to identify the same node, during potentially destructive 169 | // patching. Only set, if this node requires persistance, like maintaining 170 | // user input focus or selections. 171 | pub key: Option, 172 | } 173 | 174 | impl<'a> Default for TextOptions<'a> { 175 | fn default() -> Self { 176 | Self { 177 | escape: true, 178 | text: "", 179 | key: None, 180 | } 181 | } 182 | } 183 | 184 | impl Node { 185 | // Create an Element Node 186 | #[inline] 187 | pub fn element(opts: &ElementOptions) -> Self { 188 | Self::with_children(opts, Vec::new()) 189 | } 190 | 191 | // Create an Element Node with children 192 | #[inline] 193 | pub fn with_children(opts: &ElementOptions, children: Vec) -> Self { 194 | Self { 195 | contents: NodeContents::Element(ElementContents { 196 | tag: tokenizer::tokenize(opts.tag), 197 | attrs: super::attrs::Attrs::new(opts.attrs), 198 | children: children, 199 | }), 200 | key: opts.key, 201 | ..Default::default() 202 | } 203 | } 204 | 205 | // Create a text Node with set inner content 206 | #[inline] 207 | pub fn text(opts: &TextOptions) -> Self { 208 | Self { 209 | contents: NodeContents::Text(if opts.escape { 210 | util::html_escape(opts.text.into()) 211 | } else { 212 | opts.text.into() 213 | }), 214 | key: opts.key, 215 | ..Default::default() 216 | } 217 | } 218 | 219 | // Mount Node as passed Element. Sets the element's ID attribute. 220 | pub fn mount_as(&mut self, el: &web_sys::Element) -> Result<(), JsValue> { 221 | el.set_outer_html(&self.html()?); 222 | Ok(()) 223 | } 224 | 225 | // Mount Node as last child of parent 226 | pub fn mount_append_to( 227 | &mut self, 228 | parent: &web_sys::Element, 229 | ) -> Result<(), JsValue> { 230 | self.mount(parent, "beforeend") 231 | } 232 | 233 | // Mount Node as first child of parent 234 | pub fn mount_prepend_to( 235 | &mut self, 236 | parent: &web_sys::Element, 237 | ) -> Result<(), JsValue> { 238 | self.mount(parent, "afterbegin") 239 | } 240 | 241 | // Mount Node after as previous sibling of parent 242 | pub fn mount_before( 243 | &mut self, 244 | parent: &web_sys::Element, 245 | ) -> Result<(), JsValue> { 246 | self.mount(parent, "beforebegin") 247 | } 248 | 249 | // Mount Node after as next sibling of parent 250 | pub fn mount_after( 251 | &mut self, 252 | parent: &web_sys::Element, 253 | ) -> Result<(), JsValue> { 254 | self.mount(parent, "afterend") 255 | } 256 | 257 | fn mount( 258 | &mut self, 259 | parent: &web_sys::Element, 260 | mode: &str, 261 | ) -> Result<(), JsValue> { 262 | parent.insert_adjacent_html(mode, &self.html()?) 263 | } 264 | 265 | // Return the DOM element ID of node 266 | pub fn element_id(&self) -> String { 267 | format!("bh-{}", self.id) 268 | } 269 | 270 | // Patch possibly changed subtree into self and apply changes to the DOM. 271 | // Node must be already mounted. 272 | pub fn patch(&mut self, new: Node) -> Result<(), JsValue> { 273 | if self.id == 0 { 274 | return Err("node not mounted yet".into()); 275 | } 276 | 277 | // Check, if nodes are considered similar enough to be merged and not 278 | // replaced destructively 279 | if self.key != new.key 280 | || match &self.contents { 281 | NodeContents::Text(_) => match &new.contents { 282 | NodeContents::Element(_) => true, 283 | NodeContents::Text(_) => false, 284 | }, 285 | NodeContents::Element(cont) => match &new.contents { 286 | NodeContents::Text(_) => true, 287 | NodeContents::Element(new_cont) => new_cont.tag != cont.tag, 288 | }, 289 | } { 290 | return Node::replace_node(self, new); 291 | } 292 | 293 | self.key = new.key; 294 | match &mut self.contents { 295 | NodeContents::Text(ref mut old_text) => { 296 | if let NodeContents::Text(new_text) = &new.contents { 297 | if old_text != new_text { 298 | *old_text = new_text.clone(); 299 | self.element.get()?.set_text_content(Some(old_text)); 300 | } 301 | } 302 | } 303 | NodeContents::Element(ref mut old_cont) => { 304 | if let NodeContents::Element(new_cont) = new.contents { 305 | old_cont.attrs.patch(&mut self.element, new_cont.attrs)?; 306 | 307 | Node::patch_children( 308 | &mut self.element, 309 | &mut old_cont.children, 310 | new_cont.children, 311 | )?; 312 | } 313 | } 314 | }; 315 | Ok(()) 316 | } 317 | 318 | // Completely replace old node and its subtree with new one 319 | fn replace_node(&mut self, new: Node) -> Result<(), JsValue> { 320 | self.key = new.key; 321 | self.contents = new.contents; 322 | self.element.get()?.set_outer_html(&self.html()?); 323 | Ok(()) 324 | } 325 | 326 | // Diff and patch 2 child lists 327 | fn patch_children( 328 | parent: &mut util::LazyElement, 329 | old: &mut Vec, 330 | new: Vec, 331 | ) -> Result<(), JsValue> { 332 | let mut old_it = old.iter_mut().peekable(); 333 | let mut new_it = new.into_iter().peekable(); 334 | let mut i = 0; 335 | 336 | // First patch all matching children. Most of the time child lists will 337 | // match, so this is the hottest loop. 338 | loop { 339 | let old_ch = old_it.peek(); 340 | let new_ch = new_it.peek(); 341 | if let Some(old_ch) = old_ch { 342 | if let Some(new_ch) = new_ch { 343 | if (old_ch.key.is_some() || new_ch.key.is_some()) 344 | && old_ch.key != new_ch.key 345 | { 346 | return Node::patch_children_by_key( 347 | parent, old, i, new_it, 348 | ); 349 | } 350 | 351 | old_it.next().unwrap().patch(new_it.next().unwrap())?; 352 | i += 1; 353 | continue; 354 | } 355 | } 356 | break; 357 | } 358 | 359 | // Handle mismatched node counts using appends or deletes 360 | if new_it.peek().is_some() { 361 | // Append new nodes to end 362 | 363 | let mut w = util::Appender::new(); 364 | old.reserve(new_it.size_hint().0); 365 | for mut new_ch in new_it { 366 | new_ch.write_html_to(&mut w).map_err(util::cast_error)?; 367 | old.push(new_ch); 368 | } 369 | parent.get()?.insert_adjacent_html("beforeend", &w.dump())?; 370 | } else if old_it.peek().is_some() { 371 | // Remove nodes from end 372 | 373 | for old_ch in old_it { 374 | old_ch.element.get()?.remove(); 375 | } 376 | old.truncate(i); 377 | } 378 | 379 | Ok(()) 380 | } 381 | 382 | // Match and patch nodes by key, if any 383 | fn patch_children_by_key( 384 | parent: &mut util::LazyElement, 385 | old: &mut Vec, 386 | mut i: usize, 387 | new_it: std::iter::Peekable>, 388 | ) -> Result<(), JsValue> { 389 | // Map old children by key 390 | let mut old_by_key = HashMap::::new(); 391 | let mut to_remove = Vec::::new(); 392 | for ch in old.split_off(i) { 393 | match ch.key { 394 | Some(k) => { 395 | old_by_key.insert(k, ch); 396 | } 397 | None => { 398 | to_remove.push(ch); 399 | } 400 | } 401 | } 402 | 403 | // Insert new HTML into the DOM efficiently in buffered chunks 404 | old.reserve(new_it.size_hint().0); 405 | let mut w = util::Appender::new(); 406 | let mut buffered = 0; 407 | 408 | let flush = |w: &mut util::Appender, 409 | i: &mut usize, 410 | buffered: &mut usize, 411 | old: &mut Vec, 412 | parent: &mut util::LazyElement| 413 | -> Result<(), JsValue> { 414 | if *buffered == 0 { 415 | return Ok(()); 416 | } 417 | 418 | let html = w.dump(); 419 | w.clear(); 420 | if *i == 0 { 421 | parent.get()?.insert_adjacent_html("afterbegin", &html)?; 422 | } else { 423 | old[*i] 424 | .element 425 | .get()? 426 | .insert_adjacent_html("afterend", &html)?; 427 | } 428 | *i += *buffered; 429 | *buffered = 0; 430 | 431 | Ok(()) 432 | }; 433 | 434 | for mut new_ch in new_it { 435 | if let Some(k) = new_ch.key { 436 | if let Some(mut old_ch) = old_by_key.remove(&k) { 437 | flush(&mut w, &mut i, &mut buffered, old, parent)?; 438 | 439 | let el = old_ch.element.get()?; 440 | if i == 0 { 441 | parent 442 | .get()? 443 | .insert_adjacent_element("afterbegin", &el)?; 444 | } else { 445 | old[i] 446 | .element 447 | .get()? 448 | .insert_adjacent_element("afterend", &el)?; 449 | } 450 | old_ch.patch(new_ch)?; 451 | old.push(old_ch); 452 | i += 1; 453 | continue; 454 | } 455 | } 456 | new_ch.write_html_to(&mut w).map_err(util::cast_error)?; 457 | old.push(new_ch); 458 | buffered += 1; 459 | } 460 | flush(&mut w, &mut i, &mut buffered, old, parent)?; 461 | 462 | // Remove any unmatched old children 463 | for mut ch in to_remove 464 | .into_iter() 465 | .chain(old_by_key.into_iter().map(|(_, v)| v)) 466 | { 467 | ch.element.get()?.remove(); 468 | } 469 | 470 | Ok(()) 471 | } 472 | 473 | // Set new element ID on self 474 | fn new_id(&mut self) { 475 | use std::sync::atomic::{AtomicU64, Ordering}; 476 | 477 | static COUNTER: AtomicU64 = AtomicU64::new(1); 478 | self.id = COUNTER.fetch_add(1, Ordering::Relaxed); 479 | } 480 | 481 | // Ensure Node has an element ID set 482 | fn ensure_id(&mut self) { 483 | if self.id == 0 { 484 | self.new_id(); 485 | self.element.id = self.id; 486 | } 487 | } 488 | 489 | // Format element and subtree as HTML 490 | pub fn html(&mut self) -> Result { 491 | let mut w = util::Appender::new(); 492 | if let Err(e) = self.write_html_to(&mut w) { 493 | return Err(util::cast_error(e)); 494 | } 495 | Ok(w.dump()) 496 | } 497 | } 498 | 499 | impl util::WriteHTMLTo for Node { 500 | fn write_html_to(&mut self, w: &mut W) -> fmt::Result { 501 | self.ensure_id(); 502 | 503 | match &mut self.contents { 504 | NodeContents::Text(ref text) => { 505 | write!(w, "{}", self.id, text) 506 | } 507 | NodeContents::Element(ref mut cont) => { 508 | let id = self.id; 509 | tokenizer::get_value(cont.tag, |tag| { 510 | write!(w, "<{} id=\"bh-{}\"", tag, id) 511 | })?; 512 | cont.attrs.write_html_to(w)?; 513 | w.write_char('>')?; 514 | 515 | match cont.tag { 516 | //
,
and must not be closed. 517 | // Some browsers will interpret that as 2 tags. 518 | 36 | 124 | 282 => { 519 | return Ok(()); 520 | } 521 | _ => { 522 | for ch in cont.children.iter_mut() { 523 | ch.write_html_to(w)?; 524 | } 525 | } 526 | }; 527 | 528 | tokenizer::get_value(cont.tag, |tag| write!(w, "", tag)) 529 | } 530 | } 531 | } 532 | } 533 | 534 | #[cfg(test)] 535 | type TestResult = std::result::Result<(), JsValue>; 536 | 537 | #[cfg(test)] 538 | macro_rules! assert_html { 539 | ($node:expr, $fmt:expr, $($arg:expr),*) => {{ 540 | let res = $node.html()?; // Must execute first 541 | assert_eq!(res, format!($fmt $(, $arg)*)); 542 | }}; 543 | } 544 | 545 | #[test] 546 | fn only_tag() -> TestResult { 547 | let mut node = element!("span"); 548 | assert_html!(node, "", node.id); 549 | Ok(()) 550 | } 551 | 552 | #[test] 553 | fn element_node() -> TestResult { 554 | let mut node = element!( 555 | "span", 556 | { 557 | "loooooooooooooooooooooooooooooooooooooooooooooooooooooong" => 558 | "caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat", 559 | "classes" => "class1 class2", 560 | "disabled" => "", 561 | "width" => "64", 562 | } 563 | ); 564 | assert_html!( 565 | node, 566 | "", 567 | node.id 568 | ); 569 | Ok(()) 570 | } 571 | 572 | #[test] 573 | fn element_node_with_children() -> TestResult { 574 | let mut node = element!( 575 | "span", 576 | { 577 | "disabled" => "", 578 | "width" => "64", 579 | }, 580 | [ 581 | element!( 582 | "span", 583 | { 584 | "class" => "foo", 585 | } 586 | ), 587 | ] 588 | ); 589 | assert_html!( 590 | node, 591 | r#""#, 592 | node.id, 593 | if let NodeContents::Element(el) = &node.contents { 594 | el.children[0].id 595 | } else { 596 | 0 597 | } 598 | ); 599 | Ok(()) 600 | } 601 | 602 | #[test] 603 | fn element_node_with_children_vec() -> TestResult { 604 | let mut node = element!( 605 | "span", 606 | { 607 | "disabled" => "", 608 | "width" => "64", 609 | }, 610 | vec![ 611 | element!( 612 | "span", 613 | { "class" => "foo" } 614 | ), 615 | ] 616 | ); 617 | assert_html!( 618 | node, 619 | r#""#, 620 | node.id, 621 | if let NodeContents::Element(el) = &node.contents { 622 | el.children[0].id 623 | } else { 624 | 0 625 | } 626 | ); 627 | Ok(()) 628 | } 629 | 630 | #[test] 631 | fn text_node() -> TestResult { 632 | let mut node = escaped!(""); 633 | match &node.contents { 634 | NodeContents::Text(t) => assert_eq!(t, "<span>"), 635 | _ => assert!(false), 636 | }; 637 | assert_html!(node, r#"<span>"#, node.id); 638 | Ok(()) 639 | } 640 | -------------------------------------------------------------------------------- /src/tokenizer.rs: -------------------------------------------------------------------------------- 1 | use super::util; 2 | use std::cell::RefCell; 3 | use std::fmt; 4 | 5 | /* 6 | Sorted list of predefined HTML tags and attributes to reduce allocations and 7 | need for map checks. 8 | 9 | Sourced from: 10 | https://developer.mozilla.org/en-US/docs/Web/HTML/Element 11 | https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes 12 | 13 | NOTE: Some functions hard-code indexes into this. Do not change lightly. 14 | constexpr when? 15 | */ 16 | static PREDEFINED: [&'static str; 285] = [ 17 | "a", 18 | "abbr", 19 | "accept", 20 | "accept-charset", 21 | "accesskey", 22 | "acronym", 23 | "action", 24 | "address", 25 | "align", 26 | "allow", 27 | "alt", 28 | "applet", 29 | "applet", 30 | "area", 31 | "article", 32 | "aside", 33 | "async", 34 | "audio", 35 | "autocapitalize", 36 | "autocomplete", 37 | "autofocus", 38 | "autoplay", 39 | "b", 40 | "background", 41 | "base", 42 | "basefont", 43 | "bdi", 44 | "bdo", 45 | "bgcolor", 46 | "bgsound", 47 | "big", 48 | "blink", 49 | "blockquote", 50 | "body", 51 | "border", 52 | "br", 53 | "buffered", 54 | "button", 55 | "canvas", 56 | "caption", 57 | "center", 58 | "challenge", 59 | "charset", 60 | "checked", 61 | "cite", 62 | "cite", 63 | "class", 64 | "code", 65 | "code", 66 | "codebase", 67 | "col", 68 | "colgroup", 69 | "color", 70 | "cols", 71 | "colspan", 72 | "command", 73 | "content", 74 | "content", 75 | "content", 76 | "contenteditable", 77 | "contextmenu", 78 | "controls", 79 | "coords", 80 | "crossorigin", 81 | "csp", 82 | "data", 83 | "data", 84 | "data-*", 85 | "datalist", 86 | "datetime", 87 | "dd", 88 | "decoding", 89 | "default", 90 | "defer", 91 | "del", 92 | "details", 93 | "dfn", 94 | "dialog", 95 | "dir", 96 | "dir", 97 | "dir", 98 | "dirname", 99 | "disabled", 100 | "div", 101 | "dl", 102 | "download", 103 | "draggable", 104 | "dropzone", 105 | "dt", 106 | "element", 107 | "element", 108 | "em", 109 | "embed", 110 | "enctype", 111 | "enterkeyhint", 112 | "fieldset", 113 | "figcaption", 114 | "figure", 115 | "font", 116 | "footer", 117 | "for", 118 | "form", 119 | "form", 120 | "formaction", 121 | "formenctype", 122 | "formmethod", 123 | "formnovalidate", 124 | "formtarget", 125 | "frame", 126 | "frameset", 127 | "h1", 128 | "h2", 129 | "h3", 130 | "h4", 131 | "h5", 132 | "h6", 133 | "head", 134 | "header", 135 | "headers", 136 | "height", 137 | "hgroup", 138 | "hidden", 139 | "high", 140 | "hr", 141 | "href", 142 | "hreflang", 143 | "html", 144 | "http-equiv", 145 | "i", 146 | "icon", 147 | "id", 148 | "iframe", 149 | "image", 150 | "img", 151 | "importance", 152 | "input", 153 | "inputmode", 154 | "ins", 155 | "integrity", 156 | "intrinsicsize", 157 | "isindex", 158 | "ismap", 159 | "itemprop", 160 | "kbd", 161 | "keygen", 162 | "keytype", 163 | "kind", 164 | "label", 165 | "label", 166 | "lang", 167 | "language", 168 | "legend", 169 | "li", 170 | "link", 171 | "list", 172 | "listing", 173 | "loading", 174 | "loop", 175 | "low", 176 | "main", 177 | "main", 178 | "manifest", 179 | "map", 180 | "mark", 181 | "marquee", 182 | "max", 183 | "maxlength", 184 | "media", 185 | "menu", 186 | "menuitem", 187 | "menuitem", 188 | "meta", 189 | "meter", 190 | "method", 191 | "min", 192 | "minlength", 193 | "multicol", 194 | "multiple", 195 | "muted", 196 | "name", 197 | "nav", 198 | "nextid", 199 | "nobr", 200 | "noembed", 201 | "noembed", 202 | "noframes", 203 | "noscript", 204 | "novalidate", 205 | "object", 206 | "ol", 207 | "open", 208 | "optgroup", 209 | "optimum", 210 | "option", 211 | "output", 212 | "p", 213 | "param", 214 | "pattern", 215 | "picture", 216 | "ping", 217 | "placeholder", 218 | "plaintext", 219 | "poster", 220 | "pre", 221 | "preload", 222 | "progress", 223 | "q", 224 | "radiogroup", 225 | "rb", 226 | "readonly", 227 | "referrerpolicy", 228 | "rel", 229 | "required", 230 | "reversed", 231 | "rows", 232 | "rowspan", 233 | "rp", 234 | "rt", 235 | "rtc", 236 | "ruby", 237 | "s", 238 | "samp", 239 | "sandbox", 240 | "scope", 241 | "scoped", 242 | "script", 243 | "section", 244 | "select", 245 | "selected", 246 | "shadow", 247 | "shadow", 248 | "shape", 249 | "size", 250 | "sizes", 251 | "slot", 252 | "slot", 253 | "small", 254 | "source", 255 | "spacer", 256 | "span", 257 | "span", 258 | "spellcheck", 259 | "src", 260 | "srcdoc", 261 | "srclang", 262 | "srcset", 263 | "start", 264 | "step", 265 | "strike", 266 | "strong", 267 | "style", 268 | "style", 269 | "sub", 270 | "summary", 271 | "summary", 272 | "sup", 273 | "tabindex", 274 | "table", 275 | "target", 276 | "tbody", 277 | "td", 278 | "template", 279 | "textarea", 280 | "tfoot", 281 | "th", 282 | "thead", 283 | "time", 284 | "title", 285 | "title", 286 | "tr", 287 | "track", 288 | "translate", 289 | "tt", 290 | "tt", 291 | "type", 292 | "u", 293 | "ul", 294 | "usemap", 295 | "value", 296 | "var", 297 | "video", 298 | "wbr", 299 | "width", 300 | "wrap", 301 | "xmp", 302 | ]; 303 | 304 | thread_local! { 305 | static REGISTRY: RefCell = RefCell::new(Registry::new()); 306 | } 307 | 308 | // Storage for small (len <= 15) strings without allocating extra heap memory 309 | #[derive(Default, PartialEq, Eq, Hash, Clone)] 310 | struct ArrayString { 311 | length: u8, 312 | arr: [u8; 15], 313 | } 314 | 315 | impl ArrayString { 316 | fn new(s: &str) -> Self { 317 | let mut arr: [u8; 15] = Default::default(); 318 | for (i, ch) in s.chars().enumerate() { 319 | arr[i] = ch as u8; 320 | } 321 | Self { 322 | length: s.len() as u8, 323 | arr: arr, 324 | } 325 | } 326 | } 327 | 328 | impl AsRef for ArrayString { 329 | fn as_ref(&self) -> &str { 330 | std::str::from_utf8(&self.arr[..self.length as usize]).unwrap() 331 | } 332 | } 333 | 334 | impl util::WriteHTMLTo for String { 335 | fn write_html_to(&mut self, w: &mut W) -> fmt::Result { 336 | w.write_str(&self) 337 | } 338 | } 339 | 340 | // Contains id->string and string->id mappings 341 | #[derive(Default)] 342 | struct Registry { 343 | id_gen: util::IDGenerator, 344 | small: util::TokenMap, 345 | large: util::TokenMap, 346 | } 347 | 348 | impl Registry { 349 | fn new() -> Self { 350 | Self { 351 | id_gen: util::IDGenerator::new(PREDEFINED.len() as u16), 352 | ..Default::default() 353 | } 354 | } 355 | 356 | // Convert string to token 357 | fn tokenize(&mut self, s: &str) -> u16 { 358 | match s.len() { 359 | 0 => 0, // Don't store empty strings 360 | 1..=15 => { 361 | let v = ArrayString::new(s); 362 | match self.small.get_token(&v) { 363 | Some(t) => *t, 364 | None => { 365 | let t = self.id_gen.new_id(false); 366 | self.small.insert(t, v); 367 | t 368 | } 369 | } 370 | } 371 | _ => { 372 | let v = String::from(s); 373 | match self.large.get_token(&v) { 374 | Some(t) => *t, 375 | None => { 376 | let t = self.id_gen.new_id(true); 377 | self.large.insert(t, v); 378 | t 379 | } 380 | } 381 | } 382 | } 383 | } 384 | 385 | /// Lookup string by token 386 | fn get_value(&self, k: u16) -> &str { 387 | if k == 0 { 388 | "" 389 | } else if k <= PREDEFINED.len() as u16 { 390 | PREDEFINED[k as usize - 1] 391 | } else if util::IDGenerator::is_flagged(k) { 392 | self.large.get_value(k).as_ref() 393 | } else { 394 | self.small.get_value(k).as_ref() 395 | } 396 | } 397 | } 398 | 399 | // Convert string to token 400 | #[inline] 401 | pub fn tokenize(s: &str) -> u16 { 402 | if let Ok(i) = PREDEFINED.binary_search(&s) { 403 | return i as u16 + 1; 404 | } 405 | util::with_global_mut(®ISTRY, |r| r.tokenize(s)) 406 | } 407 | 408 | // Lookup value by token and pass it to f 409 | pub fn get_value(k: u16, f: F) -> R 410 | where 411 | F: FnOnce(&str) -> R, 412 | { 413 | util::with_global(®ISTRY, |r| f(r.get_value(k))) 414 | } 415 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::{Borrow, BorrowMut}; 2 | use std::collections::HashMap; 3 | 4 | use std::fmt; 5 | use std::fmt::Display; 6 | use std::hash::Hash; 7 | use wasm_bindgen::JsValue; 8 | use web_sys; 9 | 10 | // Efficient append-only string builder for reducing reallocations 11 | pub struct Appender { 12 | i: usize, 13 | buffers: Vec, 14 | } 15 | 16 | impl Appender { 17 | pub fn new() -> Self { 18 | return Appender { 19 | i: 0, 20 | buffers: vec![String::with_capacity(64)], 21 | }; 22 | } 23 | 24 | fn current(&mut self) -> &mut String { 25 | &mut self.buffers[self.i] 26 | } 27 | 28 | fn assert_cap(&mut self, append_size: usize) { 29 | let buf = self.current(); 30 | let cap = buf.capacity(); 31 | if buf.len() + append_size > cap { 32 | if self.i == self.buffers.len() - 1 { 33 | self.buffers.push(String::with_capacity(cap * 2)); 34 | } else { 35 | self.i += 1; 36 | } 37 | } 38 | } 39 | 40 | // Clear all contents, but keep allocated memory for reuse 41 | pub fn clear(&mut self) { 42 | self.i = 0; 43 | for b in self.buffers.iter_mut() { 44 | b.clear() 45 | } 46 | } 47 | 48 | // Dump all partial buffers into whole string 49 | pub fn dump(&mut self) -> String { 50 | self.buffers.concat() 51 | } 52 | } 53 | 54 | impl fmt::Write for Appender { 55 | fn write_str(&mut self, s: &str) -> fmt::Result { 56 | self.assert_cap(s.len()); 57 | self.current().write_str(s) 58 | } 59 | 60 | fn write_char(&mut self, c: char) -> fmt::Result { 61 | self.assert_cap(1); 62 | self.current().write_char(c) 63 | } 64 | } 65 | 66 | // Lazily retrieves an element by its ID 67 | #[derive(Default, Debug)] 68 | pub struct LazyElement { 69 | pub id: u64, 70 | pub element: Option, 71 | } 72 | 73 | impl LazyElement { 74 | // Retrieve JS element reference or cached value 75 | pub fn get(&mut self) -> Result { 76 | match &mut self.element { 77 | Some(el) => Ok(el.clone()), 78 | None => { 79 | match document().get_element_by_id(&format!("bh-{}", self.id)) { 80 | Some(el) => { 81 | self.element = Some(el.clone()); 82 | Ok(el) 83 | } 84 | None => { 85 | Err(format!("element not found: bh-{}", self.id).into()) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | // Run function with global variable immutable access 94 | pub fn with_global( 95 | global: &'static std::thread::LocalKey>, 96 | func: F, 97 | ) -> R 98 | where 99 | F: FnOnce(&G) -> R, 100 | { 101 | global.with(|r| func(r.borrow().borrow())) 102 | } 103 | 104 | // Run function with global variable mutable access 105 | pub fn with_global_mut( 106 | global: &'static std::thread::LocalKey>, 107 | func: F, 108 | ) -> R 109 | where 110 | F: FnOnce(&mut G) -> R, 111 | { 112 | global.with(|r| func(r.borrow_mut().borrow_mut())) 113 | } 114 | 115 | // Bidirectional lookup map for with no key (or value) removal 116 | #[derive(Default)] 117 | pub struct TokenMap { 118 | forward: HashMap, 119 | inverted: HashMap, 120 | } 121 | 122 | impl TokenMap { 123 | // Get key token for a value, if it is in the map 124 | pub fn get_token(&self, value: &T) -> Option<&u16> { 125 | self.inverted.get(value) 126 | } 127 | 128 | // Get a reference to value from token, if it is in the map 129 | pub fn get_value(&self, token: u16) -> &T { 130 | self.forward.get(&token).expect("unset token lookup") 131 | } 132 | 133 | // Insert new token and value into map 134 | pub fn insert(&mut self, token: u16, value: T) { 135 | self.forward.insert(token, value.clone()); 136 | self.inverted.insert(value, token); 137 | } 138 | } 139 | 140 | // Generates u16 IDs with optional highest bit flagging 141 | #[derive(Default)] 142 | pub struct IDGenerator { 143 | counter: u16, 144 | } 145 | 146 | impl IDGenerator { 147 | pub fn new(start_from: u16) -> Self { 148 | Self { 149 | counter: start_from, 150 | } 151 | } 152 | 153 | // Create new ID with optional highest bit flagging 154 | pub fn new_id(&mut self, flag_highest: bool) -> u16 { 155 | self.counter += 1; 156 | let mut id = self.counter; 157 | if flag_highest { 158 | id |= 1 << 15; 159 | } 160 | return id; 161 | } 162 | 163 | // Shorthand for checking highest bit being flagged 164 | #[inline] 165 | pub fn is_flagged(id: u16) -> bool { 166 | return id & (1 << 15) != 0; 167 | } 168 | } 169 | 170 | // HTML-Escape a string 171 | pub fn html_escape(s: &str) -> String { 172 | let mut escaped = String::with_capacity(s.len()); 173 | for ch in s.chars() { 174 | match ch { 175 | '&' => escaped += "&", 176 | '\'' => { 177 | escaped += "'"; // "'" is shorter than "'" 178 | } 179 | '<' => { 180 | escaped += "<"; 181 | } 182 | '>' => { 183 | escaped += ">"; 184 | } 185 | '"' => { 186 | escaped += """; // """ is shorter than """ 187 | } 188 | _ => { 189 | escaped.push(ch); 190 | } 191 | }; 192 | } 193 | escaped 194 | } 195 | 196 | // Get JS window global 197 | pub fn window() -> web_sys::Window { 198 | web_sys::window().expect("no window global") 199 | } 200 | 201 | // Get page document 202 | pub fn document() -> web_sys::Document { 203 | window().document().expect("no document on window") 204 | } 205 | 206 | // Cast Rust error to JSValue to be thrown as exception 207 | pub fn cast_error(e: T) -> JsValue { 208 | JsValue::from(format!("{}", e)) 209 | } 210 | 211 | // Able to write itself as HTML to w 212 | pub trait WriteHTMLTo { 213 | fn write_html_to(&mut self, w: &mut W) -> fmt::Result; 214 | } 215 | --------------------------------------------------------------------------------