@miaskiewicz/turbo-dom 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +588 -0
- package/Cargo.toml +34 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/build.rs +4 -0
- package/index.d.ts +62 -0
- package/index.js +318 -0
- package/package.json +121 -0
- package/src/core.rs +492 -0
- package/src/environment/install.mjs +34 -0
- package/src/environment/jest.cjs +40 -0
- package/src/environment/vitest.mjs +31 -0
- package/src/lib.rs +161 -0
- package/src/runtime/buffer.mjs +35 -0
- package/src/runtime/collections.mjs +50 -0
- package/src/runtime/dom.mjs +863 -0
- package/src/runtime/events.mjs +213 -0
- package/src/runtime/html-serialize.mjs +72 -0
- package/src/runtime/index.mjs +46 -0
- package/src/runtime/selectors.mjs +239 -0
- package/src/runtime/stubs.mjs +148 -0
- package/src/runtime/window.mjs +168 -0
- package/turbo-dom-parser.darwin-arm64.node +0 -0
- package/turbo-dom-parser.linux-arm64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-musl.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
package/src/lib.rs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//! turbo-dom parser — Layer 1.
|
|
2
|
+
//! One Rust core (`core`), two interchangeable front-ends selected by feature:
|
|
3
|
+
//! * `napi-bind` → native Node addon (default, fast path)
|
|
4
|
+
//! * `wasm-bind` → wasm32 fallback (StackBlitz / WebContainers / locked-down CI)
|
|
5
|
+
//! Both expose the same logical API: parse(html) -> tree, parseFragment(html) -> tree.
|
|
6
|
+
|
|
7
|
+
pub mod core;
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// napi-rs front-end (native addon)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
#[cfg(feature = "napi-bind")]
|
|
13
|
+
mod napi_front {
|
|
14
|
+
use crate::core;
|
|
15
|
+
use napi_derive::napi;
|
|
16
|
+
|
|
17
|
+
/// Marshaled node returned to JS. Mirrors `core::Node` as a napi object so the
|
|
18
|
+
/// whole tree crosses the boundary in one return value (full-marshaling mode).
|
|
19
|
+
#[napi(object)]
|
|
20
|
+
pub struct JsNode {
|
|
21
|
+
pub node_type: u8,
|
|
22
|
+
pub name: String,
|
|
23
|
+
pub value: String,
|
|
24
|
+
pub namespace: String,
|
|
25
|
+
pub public_id: String,
|
|
26
|
+
pub system_id: String,
|
|
27
|
+
pub attrs: Vec<JsAttr>,
|
|
28
|
+
pub children: Vec<JsNode>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[napi(object)]
|
|
32
|
+
pub struct JsAttr {
|
|
33
|
+
pub name: String,
|
|
34
|
+
pub value: String,
|
|
35
|
+
pub prefix: String,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl From<core::Node> for JsNode {
|
|
39
|
+
fn from(n: core::Node) -> Self {
|
|
40
|
+
JsNode {
|
|
41
|
+
node_type: n.node_type,
|
|
42
|
+
name: n.name,
|
|
43
|
+
value: n.value,
|
|
44
|
+
namespace: n.namespace,
|
|
45
|
+
public_id: n.public_id,
|
|
46
|
+
system_id: n.system_id,
|
|
47
|
+
attrs: n
|
|
48
|
+
.attrs
|
|
49
|
+
.into_iter()
|
|
50
|
+
.map(|a| JsAttr { name: a.name, value: a.value, prefix: a.prefix })
|
|
51
|
+
.collect(),
|
|
52
|
+
children: n.children.into_iter().map(JsNode::from).collect(),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Parse a full HTML document. Boundary crossed exactly once.
|
|
58
|
+
#[napi]
|
|
59
|
+
pub fn parse(html: String) -> JsNode {
|
|
60
|
+
core::parse_html_document(&html).into()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Parse-only: returns node count, builds no JS tree. For isolating raw parse
|
|
64
|
+
/// cost from tree-build + boundary marshaling in benchmarks.
|
|
65
|
+
#[napi(js_name = "parseRaw")]
|
|
66
|
+
pub fn parse_raw(html: String) -> u32 {
|
|
67
|
+
core::parse_html_document_count(&html)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
use napi::bindgen_prelude::{Int32Array, Uint16Array, Uint32Array, Uint8Array};
|
|
71
|
+
|
|
72
|
+
/// SoA flat buffer: structure as typed arrays, crossed once. JS inflates node
|
|
73
|
+
/// objects lazily from this — no eager full-tree allocation. The fast path.
|
|
74
|
+
#[napi(object)]
|
|
75
|
+
pub struct JsSoa {
|
|
76
|
+
pub node_type: Uint8Array,
|
|
77
|
+
pub ns: Uint8Array,
|
|
78
|
+
pub tag_id: Uint32Array,
|
|
79
|
+
pub parent: Int32Array,
|
|
80
|
+
pub first_child: Int32Array,
|
|
81
|
+
pub next_sib: Int32Array,
|
|
82
|
+
pub text_id: Int32Array,
|
|
83
|
+
pub pub_id: Int32Array,
|
|
84
|
+
pub sys_id: Int32Array,
|
|
85
|
+
pub attr_start: Int32Array,
|
|
86
|
+
pub attr_count: Uint16Array,
|
|
87
|
+
pub attr_name_id: Uint32Array,
|
|
88
|
+
pub attr_value: Vec<String>,
|
|
89
|
+
pub attr_prefix_id: Uint32Array,
|
|
90
|
+
pub tag_names: Vec<String>,
|
|
91
|
+
pub attr_names: Vec<String>,
|
|
92
|
+
pub attr_prefixes: Vec<String>,
|
|
93
|
+
pub strings: Vec<String>,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl From<core::Soa> for JsSoa {
|
|
97
|
+
fn from(s: core::Soa) -> Self {
|
|
98
|
+
JsSoa {
|
|
99
|
+
node_type: Uint8Array::new(s.node_type),
|
|
100
|
+
ns: Uint8Array::new(s.ns),
|
|
101
|
+
tag_id: Uint32Array::new(s.tag_id),
|
|
102
|
+
parent: Int32Array::new(s.parent),
|
|
103
|
+
first_child: Int32Array::new(s.first_child),
|
|
104
|
+
next_sib: Int32Array::new(s.next_sib),
|
|
105
|
+
text_id: Int32Array::new(s.text_id),
|
|
106
|
+
pub_id: Int32Array::new(s.pub_id),
|
|
107
|
+
sys_id: Int32Array::new(s.sys_id),
|
|
108
|
+
attr_start: Int32Array::new(s.attr_start),
|
|
109
|
+
attr_count: Uint16Array::new(s.attr_count),
|
|
110
|
+
attr_name_id: Uint32Array::new(s.attr_name_id),
|
|
111
|
+
attr_value: s.attr_value,
|
|
112
|
+
attr_prefix_id: Uint32Array::new(s.attr_prefix_id),
|
|
113
|
+
tag_names: s.tag_names,
|
|
114
|
+
attr_names: s.attr_names,
|
|
115
|
+
attr_prefixes: s.attr_prefixes,
|
|
116
|
+
strings: s.strings,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Parse a document into the SoA flat buffer (the fast runtime path).
|
|
122
|
+
#[napi(js_name = "parseBuffer")]
|
|
123
|
+
pub fn parse_buffer(html: String) -> JsSoa {
|
|
124
|
+
core::parse_html_soa(&html).into()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Parse an HTML fragment (innerHTML-style). `context` is the context element:
|
|
128
|
+
/// e.g. "body" (default), "td", or namespaced "svg path" / "math ms".
|
|
129
|
+
#[napi(js_name = "parseFragment")]
|
|
130
|
+
pub fn parse_fragment(html: String, context: Option<String>) -> JsNode {
|
|
131
|
+
core::parse_html_fragment_context(&html, context.as_deref().unwrap_or("")).into()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// wasm-bindgen front-end (fallback)
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
#[cfg(feature = "wasm-bind")]
|
|
139
|
+
mod wasm_front {
|
|
140
|
+
use crate::core;
|
|
141
|
+
use wasm_bindgen::prelude::*;
|
|
142
|
+
|
|
143
|
+
/// Same contract as the native addon, marshaled via serde → JS object.
|
|
144
|
+
#[wasm_bindgen]
|
|
145
|
+
pub fn parse(html: &str) -> Result<JsValue, JsValue> {
|
|
146
|
+
let tree = core::parse_html_document(html);
|
|
147
|
+
serde_wasm_bindgen::to_value(&tree).map_err(|e| JsValue::from_str(&e.to_string()))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[wasm_bindgen(js_name = "parseFragment")]
|
|
151
|
+
pub fn parse_fragment(html: &str, context: Option<String>) -> Result<JsValue, JsValue> {
|
|
152
|
+
let tree = core::parse_html_fragment_context(html, context.as_deref().unwrap_or(""));
|
|
153
|
+
serde_wasm_bindgen::to_value(&tree).map_err(|e| JsValue::from_str(&e.to_string()))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[wasm_bindgen(js_name = "parseBuffer")]
|
|
157
|
+
pub fn parse_buffer(html: &str) -> Result<JsValue, JsValue> {
|
|
158
|
+
let soa = core::parse_html_soa(html);
|
|
159
|
+
serde_wasm_bindgen::to_value(&soa).map_err(|e| JsValue::from_str(&e.to_string()))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SoA buffer accessor. Reads tree structure straight from the typed arrays the
|
|
2
|
+
// native parser produced — no node objects allocated until something asks.
|
|
3
|
+
|
|
4
|
+
const NS_SHORT = ['', 'svg', 'math'];
|
|
5
|
+
|
|
6
|
+
export class Buffer {
|
|
7
|
+
constructor(soa) {
|
|
8
|
+
this.soa = soa;
|
|
9
|
+
this.length = soa.nodeType.length;
|
|
10
|
+
}
|
|
11
|
+
nodeType(i) { return this.soa.nodeType[i]; }
|
|
12
|
+
ns(i) { return NS_SHORT[this.soa.ns[i]] || ''; }
|
|
13
|
+
tagName(i) { return this.soa.tagNames[this.soa.tagId[i]]; }
|
|
14
|
+
parent(i) { return this.soa.parent[i]; }
|
|
15
|
+
firstChild(i) { return this.soa.firstChild[i]; }
|
|
16
|
+
nextSib(i) { return this.soa.nextSib[i]; }
|
|
17
|
+
text(i) { const t = this.soa.textId[i]; return t < 0 ? '' : this.soa.strings[t]; }
|
|
18
|
+
publicId(i) { const t = this.soa.pubId[i]; return t < 0 ? '' : this.soa.strings[t]; }
|
|
19
|
+
systemId(i) { const t = this.soa.sysId[i]; return t < 0 ? '' : this.soa.strings[t]; }
|
|
20
|
+
attrs(i) {
|
|
21
|
+
const start = this.soa.attrStart[i];
|
|
22
|
+
if (start < 0) return [];
|
|
23
|
+
const count = this.soa.attrCount[i];
|
|
24
|
+
const out = new Array(count);
|
|
25
|
+
const { attrNameId, attrNames, attrValue, attrPrefixId, attrPrefixes } = this.soa;
|
|
26
|
+
for (let k = 0; k < count; k++) {
|
|
27
|
+
out[k] = {
|
|
28
|
+
name: attrNames[attrNameId[start + k]],
|
|
29
|
+
value: attrValue[start + k],
|
|
30
|
+
prefix: attrPrefixes[attrPrefixId[start + k]] || '',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Live collections. Each reads its backing array on every access, so they
|
|
2
|
+
// reflect mutations immediately — the exact place happy-dom bleeds liveness bugs.
|
|
3
|
+
|
|
4
|
+
function makeLive(getArray, extra = {}) {
|
|
5
|
+
const target = function () {};
|
|
6
|
+
return new Proxy(target, {
|
|
7
|
+
get(_t, key) {
|
|
8
|
+
const arr = getArray();
|
|
9
|
+
if (key === 'length') return arr.length;
|
|
10
|
+
if (key === 'item') return (i) => arr[i] ?? null;
|
|
11
|
+
if (key === 'forEach') return (cb, thisArg) => arr.forEach(cb, thisArg);
|
|
12
|
+
if (key === 'entries') return () => arr.entries();
|
|
13
|
+
if (key === 'keys') return () => arr.keys();
|
|
14
|
+
if (key === 'values') return () => arr[Symbol.iterator]();
|
|
15
|
+
if (key === Symbol.iterator) return () => arr[Symbol.iterator]();
|
|
16
|
+
if (key === 'toString') return () => '[object NodeList]';
|
|
17
|
+
if (key in extra) return extra[key](arr);
|
|
18
|
+
if (typeof key === 'string' && /^\d+$/.test(key)) return arr[Number(key)] ?? undefined;
|
|
19
|
+
return undefined;
|
|
20
|
+
},
|
|
21
|
+
has(_t, key) {
|
|
22
|
+
const arr = getArray();
|
|
23
|
+
if (typeof key === 'string' && /^\d+$/.test(key)) return Number(key) < arr.length;
|
|
24
|
+
return key === 'length' || key === 'item' || key === 'forEach' || key in extra;
|
|
25
|
+
},
|
|
26
|
+
ownKeys() {
|
|
27
|
+
const arr = getArray();
|
|
28
|
+
return [...arr.keys()].map(String).concat('length');
|
|
29
|
+
},
|
|
30
|
+
getOwnPropertyDescriptor(_t, key) {
|
|
31
|
+
const arr = getArray();
|
|
32
|
+
if (key === 'length') return { configurable: true, enumerable: false, value: arr.length };
|
|
33
|
+
if (typeof key === 'string' && /^\d+$/.test(key) && Number(key) < arr.length) {
|
|
34
|
+
return { configurable: true, enumerable: true, value: arr[Number(key)] };
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function liveNodeList(getArray) {
|
|
42
|
+
return makeLive(getArray);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function liveHTMLCollection(getArray) {
|
|
46
|
+
return makeLive(getArray, {
|
|
47
|
+
namedItem: (arr) => (name) =>
|
|
48
|
+
arr.find((el) => el.getAttribute('id') === name || el.getAttribute('name') === name) ?? null,
|
|
49
|
+
});
|
|
50
|
+
}
|