@miaskiewicz/turbo-dom 0.7.1 → 0.8.0

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 CHANGED
@@ -2791,7 +2791,7 @@ dependencies = [
2791
2791
 
2792
2792
  [[package]]
2793
2793
  name = "turbo-dom"
2794
- version = "0.7.1"
2794
+ version = "0.8.0"
2795
2795
  dependencies = [
2796
2796
  "cssparser",
2797
2797
  "html5ever",
@@ -2807,7 +2807,7 @@ dependencies = [
2807
2807
 
2808
2808
  [[package]]
2809
2809
  name = "turbo-dom-browser-env"
2810
- version = "0.7.1"
2810
+ version = "0.8.0"
2811
2811
  dependencies = [
2812
2812
  "cssparser",
2813
2813
  "lightningcss",
@@ -2825,7 +2825,7 @@ dependencies = [
2825
2825
 
2826
2826
  [[package]]
2827
2827
  name = "turbo-dom-layout"
2828
- version = "0.7.1"
2828
+ version = "0.8.0"
2829
2829
  dependencies = [
2830
2830
  "core-foundation",
2831
2831
  "core-text",
@@ -2849,7 +2849,7 @@ dependencies = [
2849
2849
 
2850
2850
  [[package]]
2851
2851
  name = "turbo-dom-parser"
2852
- version = "0.7.1"
2852
+ version = "0.8.0"
2853
2853
  dependencies = [
2854
2854
  "html5ever",
2855
2855
  "markup5ever_rcdom",
@@ -2865,7 +2865,7 @@ dependencies = [
2865
2865
 
2866
2866
  [[package]]
2867
2867
  name = "turbo-dom-raster"
2868
- version = "0.7.1"
2868
+ version = "0.8.0"
2869
2869
  dependencies = [
2870
2870
  "base64",
2871
2871
  "core-foundation",
@@ -2888,8 +2888,8 @@ dependencies = [
2888
2888
 
2889
2889
  [[package]]
2890
2890
  name = "turbo-dom-v8"
2891
- version = "150.4.0"
2892
- source = "git+https://github.com/miaskiewicz/turbo-dom?rev=e9b36c8733ef1cf1073f7053a8c5931381c978c5#e9b36c8733ef1cf1073f7053a8c5931381c978c5"
2891
+ version = "150.4.1"
2892
+ source = "git+https://github.com/miaskiewicz/turbo-dom?rev=a673f79e22006b85315670493800418932f2df31#a673f79e22006b85315670493800418932f2df31"
2893
2893
  dependencies = [
2894
2894
  "bindgen",
2895
2895
  "bitflags 2.12.1",
package/Cargo.toml CHANGED
@@ -43,7 +43,7 @@ manual_midpoint = "allow"
43
43
 
44
44
  [package]
45
45
  name = "turbo-dom-parser"
46
- version = "0.7.1"
46
+ version = "0.8.0"
47
47
  edition = "2021"
48
48
  description = "html5ever-backed native parser for turbo-dom (Layer 1)"
49
49
  # Ships to npm only (napi/wasm addon); never published to crates.io.
@@ -114,8 +114,10 @@ opt-level = 3
114
114
  inherits = "release"
115
115
  debug = true
116
116
 
117
- # Pre-publish only: resolve the generated turbo-dom-v8 fork from its candidate branch
118
- # (crates/turbo-dom-v8/scripts/push-candidate.sh). Remove once turbo-dom-v8 150.4.0 is
119
- # on crates.io (see PUBLISHING.md).
117
+ # Pre-publish only: resolve the unpublished turbo-dom-v8 150.4.1 (pins.json) from its
118
+ # candidate branch (crates/turbo-dom-v8/scripts/push-candidate.sh prints the rev). crates.io
119
+ # has 150.4.0; publish.yml publishes 150.4.1 first and then drops this table and re-locks
120
+ # from crates.io (`scripts/publish-crates.py drop-v8-patch`, PUBLISHING.md). Re-point it
121
+ # after every reviewed fork change; check-versions.py asserts the locked version.
120
122
  [patch.crates-io]
121
- turbo-dom-v8 = { git = "https://github.com/miaskiewicz/turbo-dom", rev = "e9b36c8733ef1cf1073f7053a8c5931381c978c5" }
123
+ turbo-dom-v8 = { git = "https://github.com/miaskiewicz/turbo-dom", rev = "a673f79e22006b85315670493800418932f2df31" }
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # turbo-dom
2
2
 
3
- Release **0.7.1**. turbo-dom is the shared browser-engine base of three projects:
3
+ Release **0.8.0**. turbo-dom is the shared browser-engine base of three projects:
4
4
 
5
5
  - **[turbo-test](https://github.com/miaskiewicz/turbo-test)**, a vitest-compatible test runner. It runs tests in V8 over this repository's DOM.
6
6
  - **[turbo-html2pdf](https://github.com/miaskiewicz/turbo-html2pdf)**, which renders HTML to PDF and uses this repository's layout engine.
7
7
  - **[turbo-surf](https://github.com/miaskiewicz/turbo-surf)**, a headless browser that uses the DOM, the V8 binding, the layout engine and the screenshot painter.
8
8
 
9
+ > Agents: start at [docs/AGENTS.md](docs/AGENTS.md) for the one-command workflows in `scripts/agent/`.
10
+
9
11
  turbo-dom is also a standalone npm package: a fast, spec-correct DOM environment for
10
12
  **vitest** and **jest**, usable in place of jsdom or happy-dom.
11
13
 
@@ -24,7 +26,7 @@ and the publish order.
24
26
  | [`turbo-dom-layout`](crates/turbo-dom-layout) | crates.io | 0.7.0 | HTML/CSS layout (block / inline / flex / grid / table), fonts, images, CSSOM-View geometry | turbo-html2pdf, turbo-surf |
25
27
  | [`turbo-dom-raster`](crates/turbo-dom-raster) | crates.io | 0.7.0 | synthetic screenshots (PNG / SVG), canvas op-log replay, WebGL readback | turbo-surf |
26
28
  | [`turbo-dom-browser-env`](crates/turbo-dom-browser-env) | crates.io | 0.7.0 | the `rtdom` <-> V8 browser environment (window, document, CSSOM, forms, events) | turbo-test, turbo-surf |
27
- | [`turbo-dom-v8`](crates/turbo-dom-v8) | crates.io | 150.4.0 | turbo-dom's rusty_v8 fork, generated from the upstream crate plus reviewed patches | turbo-dom-browser-env, turbo-surf |
29
+ | [`turbo-dom-v8`](crates/turbo-dom-v8) | crates.io | 150.4.1 | turbo-dom's rusty_v8 fork, generated from the upstream crate plus reviewed patches | turbo-dom-browser-env, turbo-surf |
28
30
 
29
31
  The root `turbo-dom-parser` crate is `publish = false`. It ships inside the npm package.
30
32
 
@@ -105,10 +107,10 @@ parseBuffer, parseFragment })` or set it as `globalThis.__TURBO_DOM_PARSER__`. T
105
107
 
106
108
  ```toml
107
109
  [dependencies]
108
- turbo-dom = "0.7"
109
- turbo-dom-layout = "0.7" # bundled fonts on by default
110
- turbo-dom-raster = "0.7"
111
- turbo-dom-browser-env = "0.7" # needs a V8 archive, see its README
110
+ turbo-dom = "0.8"
111
+ turbo-dom-layout = "0.8" # bundled fonts on by default
112
+ turbo-dom-raster = "0.8"
113
+ turbo-dom-browser-env = "0.8" # needs a V8 archive, see its README
112
114
  ```
113
115
 
114
116
  ```rust
@@ -134,6 +136,7 @@ Each crate's README covers its API: [turbo-dom](crates/turbo-dom/README.md),
134
136
  |---|---|---|---|
135
137
  | `turbo-dom-layout` | `bundled-fonts` | on | embeds OFL faces (Inter/Roboto, Liberation Serif/PT Serif, Fira Code/IBM Plex Mono) so a document lays out with no caller fonts |
136
138
  | | `hyphenation` | on | `hyphens: auto` for English (`en` / `en-*`, not en-GB) with the embedded en-US TeX patterns (~90 KB); off, `auto` acts as `manual` |
139
+ | | `hyphenation-languages` | on | `hyphens: auto` in every other language Chrome 156 on macOS hyphenates (en-GB, ca, cs, da, de, el, es, fi, fr, hr, hu, it, nb / no, nl, pl, pt, ro, ru, sk, sv, uk) with the TeX patterns (~3 MB, all of the `hyphenation` crate's); implies `hyphenation` |
137
140
  | | `svg` | off | routes `image/svg+xml` through the SVG rasterizer in the image pipeline (implies `svg-rasterize`) |
138
141
  | | `svg-rasterize` | off | the shared resvg rasterizer only (`svg::rasterize`) |
139
142
  | | `serde` | off | `Serialize` for the CSSOM-View metrics records |
@@ -170,8 +173,10 @@ upstream rusty_v8's features (for example `simdutf`).
170
173
  - **Open gaps (tracked as issues):**
171
174
  - glyph stems in raster screenshots lack Chrome's stem darkening, so glyph edges are
172
175
  up to ~0.5px thinner per side (#266).
173
- - Known difference, not tracked: `hyphens: auto` covers English only, and Chrome on
174
- macOS hyphenates with the system dictionary, whose points differ for some words.
176
+ - Known difference, not tracked: `hyphens: auto` breaks at the TeX patterns' points
177
+ (what Chrome uses on Linux, Windows and Android); Chrome on macOS hyphenates with the
178
+ system dictionary, whose points differ for a few words (en `international`, pl
179
+ `szczególnie`; `tests/fixtures/hyphens/words-chrome156.json` lists them).
175
180
  - **The npm runtime has no layout.** Geometry is synthetic: `getBoundingClientRect()`,
176
181
  `offsetWidth/Height`, `clientWidth/Height` and `scrollWidth/Height` return a non-zero
177
182
  size that is stable for a given DOM state (blocks fill the parent's width, the root
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Faster, more spec-correct DOM for test runners \u2014 native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
package/pkg/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "turbo-dom-parser",
3
3
  "description": "html5ever-backed native parser for turbo-dom (Layer 1)",
4
- "version": "0.7.1",
4
+ "version": "0.8.0",
5
5
  "files": [
6
6
  "turbo_dom_parser_bg.wasm",
7
7
  "turbo_dom_parser.js",
@@ -9,4 +9,4 @@
9
9
  ],
10
10
  "main": "turbo_dom_parser.js",
11
11
  "types": "turbo_dom_parser.d.ts"
12
- }
12
+ }
Binary file
@@ -2,7 +2,7 @@
2
2
  "name": "turbo-dom-parser",
3
3
  "type": "module",
4
4
  "description": "html5ever-backed native parser for turbo-dom (Layer 1)",
5
- "version": "0.7.1",
5
+ "version": "0.8.0",
6
6
  "files": [
7
7
  "turbo_dom_parser_bg.wasm",
8
8
  "turbo_dom_parser.js",
@@ -13,4 +13,4 @@
13
13
  "sideEffects": [
14
14
  "./snippets/*"
15
15
  ]
16
- }
16
+ }
Binary file
@@ -45,25 +45,31 @@ const BORDER_STYLES = new Set(['none', 'hidden', 'dotted', 'dashed', 'solid', 'd
45
45
  // declarations of the same longhand override naturally (cascade order preserved).
46
46
  function setProp(map, name, val) {
47
47
  map.set(name, val);
48
- if (name === 'margin' || name === 'padding') {
49
- const t = val.trim().split(/\s+/);
50
- const top = t[0], right = t[1] ?? top, bottom = t[2] ?? top, left = t[3] ?? right;
51
- map.set(name + '-top', top); map.set(name + '-right', right);
52
- map.set(name + '-bottom', bottom); map.set(name + '-left', left);
53
- } else if (name === 'border') {
54
- let width, style, color;
55
- for (const p of val.trim().split(/\s+/)) {
56
- if (BORDER_STYLES.has(p)) style = p;
57
- else if (/^(thin|medium|thick)$/.test(p) || /^[\d.]+(px|em|rem|%|pt|vh|vw)?$/.test(p)) width = p;
58
- else color = p;
59
- }
60
- if (width !== undefined) { map.set('border-width', width); for (const s of ['top', 'right', 'bottom', 'left']) map.set(`border-${s}-width`, width); }
61
- if (style !== undefined) map.set('border-style', style);
62
- if (color !== undefined) map.set('border-color', color);
63
- } else if (name === 'background' && !/\s/.test(val.trim())) {
48
+ if (name === 'margin' || name === 'padding') setBoxSides(map, name, val);
49
+ else if (name === 'border') setBorder(map, val);
50
+ else if (name === 'background' && !/\s/.test(val.trim())) {
64
51
  map.set('background-color', val.trim());
65
52
  }
66
53
  }
54
+ // `margin` / `padding`: the 1–4 value side expansion.
55
+ function setBoxSides(map, name, val) {
56
+ const t = val.trim().split(/\s+/);
57
+ const top = t[0], right = t[1] ?? top, bottom = t[2] ?? top, left = t[3] ?? right;
58
+ map.set(name + '-top', top); map.set(name + '-right', right);
59
+ map.set(name + '-bottom', bottom); map.set(name + '-left', left);
60
+ }
61
+ // `border`: width / style / color by the shape of each value.
62
+ function setBorder(map, val) {
63
+ let width, style, color;
64
+ for (const p of val.trim().split(/\s+/)) {
65
+ if (BORDER_STYLES.has(p)) style = p;
66
+ else if (/^(thin|medium|thick)$/.test(p) || /^[\d.]+(px|em|rem|%|pt|vh|vw)?$/.test(p)) width = p;
67
+ else color = p;
68
+ }
69
+ if (width !== undefined) { map.set('border-width', width); for (const s of ['top', 'right', 'bottom', 'left']) map.set(`border-${s}-width`, width); }
70
+ if (style !== undefined) map.set('border-style', style);
71
+ if (color !== undefined) map.set('border-color', color);
72
+ }
67
73
 
68
74
  function parseDecls(text, into) {
69
75
  const map = into || new Map();
@@ -95,21 +101,30 @@ function parseStylesheet(css, startOrder, rules) {
95
101
  if (css[j] === ';') { order++; i = j + 1; continue; }
96
102
  if (css[j] === '}') { i = j + 1; continue; }
97
103
  const sel = css.slice(i, j).trim();
98
- let depth = 1, k = j + 1;
99
- while (k < n && depth > 0) { const ch = css[k]; if (ch === '{') depth++; else if (ch === '}') depth--; k++; }
100
- if (sel && sel[0] !== '@') {
101
- const decls = parseDecls(css.slice(j + 1, k - 1));
102
- // the prelude is a selector LIST split at top-level commas only (`:is(a, b)` stays
103
- // whole); an invalid list drops the whole rule, as in a browser; each item carries its
104
- // Selectors 4 specificity (mirrors rtdom::cascade::parse_stylesheet)
105
- if (decls.size) for (const t of ruleSelectors(sel)) {
106
- rules.push({ selector: t, decls, spec: specificity(t), order: order++ });
107
- }
108
- } else { order++; }
104
+ const k = blockEnd(css, j + 1);
105
+ if (sel && sel[0] !== '@') order = pushRules(sel, parseDecls(css.slice(j + 1, k - 1)), order, rules);
106
+ else order++;
109
107
  i = k;
110
108
  }
111
109
  return order;
112
110
  }
111
+ // The index just past the `}` closing the block whose body starts at `k` (the input's
112
+ // end when unclosed).
113
+ function blockEnd(css, k) {
114
+ let depth = 1;
115
+ while (k < css.length && depth > 0) { const ch = css[k]; if (ch === '{') depth++; else if (ch === '}') depth--; k++; }
116
+ return k;
117
+ }
118
+ // The rules of a style rule with prelude `sel`; returns the next source order.
119
+ function pushRules(sel, decls, order, rules) {
120
+ // the prelude is a selector LIST split at top-level commas only (`:is(a, b)` stays
121
+ // whole); an invalid list drops the whole rule, as in a browser; each item carries its
122
+ // Selectors 4 specificity (mirrors rtdom::cascade::parse_stylesheet)
123
+ if (decls.size) for (const t of ruleSelectors(sel)) {
124
+ rules.push({ selector: t, decls, spec: specificity(t), order: order++ });
125
+ }
126
+ return order;
127
+ }
113
128
 
114
129
  function buildIndex(scope) {
115
130
  // scope is always a Document or ShadowRoot — both expose getElementsByTagName.
@@ -225,6 +240,29 @@ function makeProxy(map, v) {
225
240
  });
226
241
  }
227
242
 
243
+ // Encapsulation: an element inside a shadow tree resolves against that shadow root's own
244
+ // <style> rules, not the document's (and vice-versa). Gated on __hasShadow so the
245
+ // no-shadow path keeps resolving against doc.
246
+ function styleScope(doc, el) {
247
+ if (!doc.__hasShadow) return doc;
248
+ const root = el.getRootNode && el.getRootNode();
249
+ return root && root.__isShadowRoot ? root : doc;
250
+ }
251
+ // The shadow overlays (only once the document has a shadow root).
252
+ function collectShadowMatched(el, v, matched) {
253
+ // shadow host: overlay :host rules from its OWN shadow root's stylesheet
254
+ if (el.__shadow) collectMatched(el, getIndex(el.__shadow, v), 'host', matched);
255
+ // slotted light node: overlay ::slotted rules from the slot's shadow root
256
+ const slot = el.assignedSlot;
257
+ if (slot) { const sr = slot.getRootNode(); if (sr.__isShadowRoot) collectMatched(el, getIndex(sr, v), 'slotted', matched, slot); }
258
+ }
259
+ // The unset INHERITED properties of `map` from the parent's computed style `ps`.
260
+ function inheritFrom(map, ps) {
261
+ for (const prop of INHERITED) {
262
+ if (!map.has(prop)) { const pv = ps.getPropertyValue(prop); if (pv) map.set(prop, pv); }
263
+ }
264
+ }
265
+
228
266
  // getComputedStyle(el): resolves cascade, memoized per element on the current
229
267
  // Document.__version (any style/DOM mutation bumps it → cache auto-invalidates).
230
268
  export function makeGetComputedStyle() {
@@ -238,24 +276,9 @@ export function makeGetComputedStyle() {
238
276
  const map = new Map();
239
277
  if (!doc) { const p = makeProxy(map, v); el.__computedStyle = p; return p; }
240
278
 
241
- // Encapsulation: an element inside a shadow tree resolves against that
242
- // shadow root's own <style> rules, not the document's (and vice-versa).
243
- // Gated on __hasShadow so the no-shadow path keeps resolving against doc.
244
- let scope = doc, inShadow = false;
245
- if (doc.__hasShadow) {
246
- const root = el.getRootNode && el.getRootNode();
247
- if (root && root.__isShadowRoot) { scope = root; inShadow = true; }
248
- }
249
-
250
279
  const matched = [];
251
- collectMatched(el, getIndex(scope, v), 'normal', matched);
252
- if (doc.__hasShadow) {
253
- // shadow host: overlay :host rules from its OWN shadow root's stylesheet
254
- if (el.__shadow) collectMatched(el, getIndex(el.__shadow, v), 'host', matched);
255
- // slotted light node: overlay ::slotted rules from the slot's shadow root
256
- const slot = el.assignedSlot;
257
- if (slot) { const sr = slot.getRootNode(); if (sr.__isShadowRoot) collectMatched(el, getIndex(sr, v), 'slotted', matched, slot); }
258
- }
280
+ collectMatched(el, getIndex(styleScope(doc, el), v), 'normal', matched);
281
+ if (doc.__hasShadow) collectShadowMatched(el, v, matched);
259
282
  applyMatched(matched, map);
260
283
 
261
284
  // inline style wins over stylesheet rules
@@ -268,17 +291,10 @@ export function makeGetComputedStyle() {
268
291
  // shadow content. Honest partial: only the curated INHERITED set inherits, only
269
292
  // from values a REAL rule/inline actually set (pv truthy) — never an initial
270
293
  // value. Recursion is memoized per element/version and terminates at the root
271
- // (flattenedParent → null). Test-time only; hot paths never call gcs.
272
- // `inShadow` is unused now that light-DOM inherits too, but the flattened-parent
273
- // hop still crosses the host boundary correctly for shadow content.
274
- void inShadow;
294
+ // (flattenedParent → null). Test-time only; hot paths never call gcs. The
295
+ // flattened-parent hop crosses the host boundary for shadow content.
275
296
  const parent = flattenedParent(el);
276
- if (parent) {
277
- const ps = gcs(parent);
278
- for (const prop of INHERITED) {
279
- if (!map.has(prop)) { const pv = ps.getPropertyValue(prop); if (pv) map.set(prop, pv); }
280
- }
281
- }
297
+ if (parent) inheritFrom(map, gcs(parent));
282
298
 
283
299
  const proxy = makeProxy(map, v);
284
300
  el.__computedStyle = proxy;