diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index af64d758..c2288c7d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -45,15 +45,6 @@ jobs:
# native MTP --coverage flag is ignored in this mode). The collector
# ships with the MSTest meta-package, so no extra dependency is needed.
run: dotnet test --no-build -c Release --logger "trx;LogFileName=test.trx" --collect:"Code Coverage;Format=cobertura"
- - name: Test (Jint alt-engine — Engine suite under STARLING_JS_ENGINE=jint)
- # The default test run above exercises the in-house Starling.Js backend.
- # Jint is a runtime-selectable alternative (a temporary compat crutch);
- # re-run the engine integration suite against it so the alt backend can
- # never silently regress. The Jint backend's own unit tests
- # (Starling.Bindings.Jint.Tests) already ran in the default step.
- env:
- STARLING_JS_ENGINE: jint
- run: dotnet test tests/Starling.Engine.Tests/Starling.Engine.Tests.csproj --no-build -c Release --logger "trx;LogFileName=test-jint.trx"
- name: Test (golden-image)
run: dotnet test --no-build -c Release --filter Category=GoldenImage
- name: Upload coverage to Codecov
@@ -117,7 +108,6 @@ jobs:
src/Starling.Js/ \
src/Starling.Js.Hosting/ \
src/Starling.Bindings/ \
- src/Starling.Bindings.Jint/ \
src/Starling.Loop/ \
src/Starling.Engine/
diff --git a/AGENTS.md b/AGENTS.md
index 850276a2..736d8a7e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -163,16 +163,14 @@ Runtime selection flags go after `aspire run --` and are forwarded to both GUI
and headless resources:
```bash
-aspire run -- --jint --imagesharp # Jint JS backend + CPU paint
+aspire run -- --starling --imagesharp # Starling JS backend + CPU paint
aspire run -- --starling --gpu # Starling JS backend + WebGPU paint
```
Flags win over `STARLING_JS_ENGINE` and `STARLING_PAINT_BACKEND`.
-The default JS engine is **Starling**. Pass `--jint` (or set
-`STARLING_JS_ENGINE=jint`) to use the Jint backend instead. Binding / DOM /
-JS-OM work should target `src/Starling.Bindings` and its matching tests unless
-the task is explicitly about the Jint backend.
+The JS engine is **Starling** — the only backend. Binding / DOM / JS-OM work
+targets `src/Starling.Bindings` and its matching tests.
The default HTML parser is the **Starling parser**. The Aspire resources and the
desktop `Starling.Gui` startup both default to it. Pass `--anglesharp-html` (or
diff --git a/Directory.Build.props b/Directory.Build.props
index c88de09c..cd242449 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,8 +8,7 @@
net11 and the preview language version. So Starling.Bindings and every
project that consumes it move to net11. Projects below Bindings (Dom, Js,
Common, and so on) stay on net10 and are referenced downlevel, which the
- runtime allows. The Jint backend does not reference Bindings, so it stays
- on net10 too. Dom.Tests stays on net10, so the core DOM tests run
+ runtime allows. Dom.Tests stays on net10, so the core DOM tests run
unchanged.
The Aspire host (Starling.AppHost) stays on net10: its Aspire.AppHost.Sdk
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 54dd5920..8b684209 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -83,13 +83,6 @@
-
-
@@ -15,7 +14,6 @@
-
diff --git a/bench/Starling.JsEngineBench/StarlingFeatureBench.cs b/bench/Starling.JsEngineBench/StarlingFeatureBench.cs
index afb40878..82e5801b 100644
--- a/bench/Starling.JsEngineBench/StarlingFeatureBench.cs
+++ b/bench/Starling.JsEngineBench/StarlingFeatureBench.cs
@@ -1,6 +1,5 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
-using Jint;
using Starling.Js.Bytecode;
using Starling.Js.Parse;
using Starling.Js.Runtime;
@@ -9,10 +8,9 @@ namespace Starling.JsEngineBench;
///
/// Starling-authored microbenchmarks, one per engine optimization, run through
-/// the SAME Starling-vs-Jint harness as
-/// (cold + prepared, Jint as the baseline). Where the vendored dromaeo suite
-/// conflates many things, each case here isolates one piece of work so its
-/// standing against Jint is visible and trackable on its own:
+/// the SAME harness as (cold + prepared). Where
+/// the vendored dromaeo suite conflates many things, each case here isolates
+/// one piece of work so its cost is visible and trackable on its own:
///
/// - calls — call-argument and frame-stack pooling.
/// - prop-read-mono — monomorphic own-property read inline cache.
@@ -35,7 +33,7 @@ namespace Starling.JsEngineBench;
[RankColumn]
public class StarlingFeatureBench
{
- // Strict-mode parity with EngineComparisonBench's prelude (no dromaeo stubs
+ // Strict-mode parity with ScriptSuiteBench's prelude (no dromaeo stubs
// needed — these scripts are self-contained).
private const string Prelude = "\"use strict\";\n";
@@ -109,8 +107,8 @@ public class StarlingFeatureBench
"for (var i = 0; i < 100000; i++) { s += str.split(/,/).length; }\n" +
"s;",
- // Regex global replace: the @@replace path (still slower than Jint —
- // tracks the next regex lever).
+ // Regex global replace: the @@replace path (the known remaining regex
+ // gap — tracks the next regex lever).
["regex-replace"] =
"var str = 'the quick brown fox jumps over';\n" +
"var s = 0;\n" +
@@ -129,39 +127,26 @@ public class StarlingFeatureBench
public static IEnumerable Cases => Scripts.Keys;
private string _src = "";
- private Chunk? _starlingChunk;
- private Prepared _jintPrepared;
+ private Chunk? _chunk;
[GlobalSetup]
public void Setup()
{
_src = Prelude + Scripts[Case];
- _starlingChunk = JsCompiler.CompileForEval(new JsParser(_src).ParseProgram());
- _jintPrepared = Engine.PrepareScript(_src, strict: true);
+ _chunk = JsCompiler.CompileForEval(new JsParser(_src).ParseProgram());
- // Fail fast if a case throws on either engine, so a broken script never
- // masquerades as a (mis)measured benchmark.
- RunStarling();
- RunJint();
+ // Fail fast if a case throws, so a broken script never masquerades as a
+ // (mis)measured benchmark.
+ RunCold();
}
[Benchmark(Baseline = true)]
- public Engine Jint() => RunJint();
-
- [Benchmark]
- public Engine Jint_ParsedScript() =>
- new Engine(o => o.Strict = true).Execute(_jintPrepared);
-
- [Benchmark]
- public JsValue Starling() => RunStarling();
+ public JsValue Starling() => RunCold();
[Benchmark]
public JsValue Starling_Prepared() =>
- new JsVm(new JsRuntime()).Run(_starlingChunk!);
+ new JsVm(new JsRuntime()).Run(_chunk!);
- private JsValue RunStarling() =>
+ private JsValue RunCold() =>
new JsVm(new JsRuntime()).Run(JsCompiler.CompileForEval(new JsParser(_src).ParseProgram()));
-
- private Engine RunJint() =>
- new Engine(o => o.Strict = true).Execute(_src);
}
diff --git a/bench/Starling.JsEngineBench/StarlingScalingBench.cs b/bench/Starling.JsEngineBench/StarlingScalingBench.cs
index dc23359e..8dcbba59 100644
--- a/bench/Starling.JsEngineBench/StarlingScalingBench.cs
+++ b/bench/Starling.JsEngineBench/StarlingScalingBench.cs
@@ -1,5 +1,4 @@
using BenchmarkDotNet.Attributes;
-using Jint;
using Starling.Js.Bytecode;
using Starling.Js.Parse;
using Starling.Js.Runtime;
@@ -8,22 +7,20 @@ namespace Starling.JsEngineBench;
///
/// Scaling sweep: ONE representative workload run at an increasing iteration
-/// count N, so the engines' fixed cost (parse + compile + realm
+/// count N, so the engine's fixed cost (parse + compile + realm
/// bootstrap) and marginal per-iteration dispatch cost separate out as
/// N grows. Where pins each feature at a
/// single large N, this one answers "one-off vs medium vs lots-and-lots".
///
-/// Read the table down an engine's column as N increases:
+/// Read the table down a column as N increases:
///
-/// - N = 1 — dominated by FIXED cost (one-off latency). A bytecode VM
-/// pays a compile step a tree-walker skips, so Jint is expected to win here.
-/// - N large — dominated by MARGINAL cost (steady-state throughput).
-/// This is where bytecode + inline caches are expected to pull ahead — IF
-/// per-instruction dispatch is cheaper than re-walking the AST. A crossover
-/// exists only if Starling's marginal cost is below Jint's.
+/// - N = 1 — dominated by FIXED cost (one-off latency): the parse +
+/// compile + bootstrap a bytecode VM pays before the first instruction.
+/// - N large — dominated by MARGINAL cost (steady-state throughput),
+/// where bytecode + inline caches earn their keep.
///
///
-/// Decompose per engine from the curve:
+/// Decompose from the curve:
/// fixed ≈ time at N=1;
/// marginal ≈ (t(N_hi) − t(N_lo)) / (N_hi − N_lo).
///
@@ -35,9 +32,9 @@ namespace Starling.JsEngineBench;
/// The body is a monomorphic property + prototype-method + call workload — the
/// shape of code a bytecode VM with inline caches is supposed to be strongest
/// on, and (unlike the regex cases) no shared System.Text call dilutes the
-/// engines' own dispatch. Both Warm and Cold build a fresh engine/realm per op,
+/// engine's own dispatch. Both Warm and Cold build a fresh runtime/realm per op,
/// so realm bootstrap stays part of the fixed cost (intentional — it is part of
-/// one-off latency). Uses a short job (4 methods × 4 N); run with
+/// one-off latency). Uses a short job (2 methods × 4 N); run with
/// --filter '*StarlingScalingBench*'.
///
[ShortRunJob]
@@ -63,28 +60,19 @@ private static string Build(int n) =>
private string _src = "";
private Chunk? _chunk;
- private Prepared _jintPrepared;
[GlobalSetup]
public void Setup()
{
_src = Build(N);
_chunk = JsCompiler.CompileForEval(new JsParser(_src).ParseProgram());
- _jintPrepared = Engine.PrepareScript(_src, strict: true);
- // Fail fast if a script throws on either engine.
- Jint_Warm();
+ // Fail fast if the script throws.
Starling_Warm();
}
- // Baseline: warm Jint (pre-parsed AST), the natural steady-state reference.
+ // Baseline: warm (pre-compiled chunk), the natural steady-state reference.
[Benchmark(Baseline = true)]
- public Engine Jint_Warm() => new Engine(o => o.Strict = true).Execute(_jintPrepared);
-
- [Benchmark]
- public Engine Jint_Cold() => new Engine(o => o.Strict = true).Execute(_src);
-
- [Benchmark]
public JsValue Starling_Warm() => new JsVm(new JsRuntime()).Run(_chunk!);
[Benchmark]
diff --git a/bench/engine-comparison.md b/bench/engine-comparison.md
index 8ca2db91..7fd045c3 100644
--- a/bench/engine-comparison.md
+++ b/bench/engine-comparison.md
@@ -1,12 +1,11 @@
-# JS engine comparison: Starling vs Jint
+# JS engine script-suite results
-Where does the Starling JS engine land against Jint? This runs Jint's own
-"EngineComparison" benchmark on our engine. Same 19 scripts, same strict mode,
-ranked on one machine so the numbers compare fairly.
+How fast is the Starling JS engine on real script workloads? This runs the 19
+vendored scripts under `bench/Starling.JsEngineBench/Scripts/` (dromaeo loops,
+base64, a 34 KB linq library, regular expressions, `eval`-heavy code) in strict
+mode, ranked on one machine.
-The benchmark lives in `bench/Starling.JsEngineBench`. The scripts under
-`Scripts/` are copied straight from Jint's suite (see `Scripts/README.md` for the
-source commit).
+The benchmark lives in `bench/Starling.JsEngineBench` (`ScriptSuiteBench`).
## How to run
@@ -18,138 +17,63 @@ dotnet run -c Release --project bench/Starling.JsEngineBench -- --filter '*'
dotnet run -c Release --project bench/Starling.JsEngineBench -- --job dry --filter '*'
```
-Each engine runs two ways, matching Jint's own split:
+Each script runs two ways:
-- **cold** — parse, compile, and run every time. Read `Starling` against `Jint`.
-- **prepared** — compile once, then run on a fresh runtime each time. Read
- `Starling_Prepared` against `Jint_ParsedScript`.
+- **cold** (`Starling`) — parse, compile, and run every time.
+- **prepared** (`Starling_Prepared`) — compile once, then run on a fresh
+ runtime each time.
+
+The full table with error bars and per-generation garbage-collector counts is
+written to `BenchmarkDotNet.Artifacts/results/` under whatever folder you run
+the command from. That folder is gitignored.
## What we found
Starling ran **all 19 scripts with zero failures** — modern syntax, `eval`,
-`new Function`, regular expressions, and the 34 KB `linq-js` library. For a young
-engine that is the headline. Jint passes everything too, so this is a clean
-head-to-head.
-
-On speed, Starling is the slower engine on every script. The gap is small on
-some, huge on others:
-
-- **Close (1.2x–2x):** the dromaeo 3d-cube and object-string scripts. Tight
- arithmetic and string building. Starling's bytecode interpreter keeps up well
- here.
-- **Middle (3x–7x):** core-eval, base64, stopwatch, linq, plain evaluation. The
- common case.
-- **Two blow-ups:** the regular-expression scripts run about **120x slower** —
- almost 13 seconds against Jint's 0.1 second. This is the Starling
- regular-expression engine, not the interpreter. It is the single biggest thing
- to fix.
-
-One bright spot: on `linq-js`, prepared Starling (182 µs) beats **cold** Jint
-(1,000 µs). Reuse the compiled script and Starling does fine on real library code.
-
-The bigger weakness is memory. Starling allocates far more than Jint on most
-scripts. The range is wide: nearly even on the object-string scripts (1.1x), up
-to 346x on the regular-expression scripts. A no-JIT bytecode interpreter does
-more boxing and short-lived allocation per operation, and it shows. This is the
-other thing to chase, because high allocation drives garbage-collector pauses
-that hurt frame time in the browser.
-
-(JIT = just-in-time compiling to native code. Jint stays an interpreter too, so
-the gap here is about allocation, not native codegen.)
-
-## Local results (this machine)
-
-Apple M3 Max, macOS 26.3, .NET 10.0.8 Arm64. BenchmarkDotNet 0.14.0, ShortRun (3
-launches, 3 warmup, 3 iterations). Times are mean per run.
-
-- **cold x** is `Starling` time divided by `Jint` time.
-- **prep x** is `Starling_Prepared` time divided by `Jint_ParsedScript` time.
-- **Alloc** is `Starling` bytes divided by `Jint` bytes.
-
-Lower is better for all three. Ranked from smallest cold gap to largest.
-
-| Script | Jint | Starling | cold x | prep x | Alloc |
-|---|--:|--:|--:|--:|--:|
-| dromaeo-3d-cube.js | 17.3 ms | 20.8 ms | 1.2x | 1.7x | 11x |
-| dromaeo-object-string.js | 104 ms | 169 ms | 1.6x | 1.6x | 1.1x |
-| dromaeo-object-string-modern.js | 106 ms | 180 ms | 1.7x | 1.7x | 1.2x |
-| dromaeo-3d-cube-modern.js | 12.1 ms | 24.2 ms | 2.0x | 1.8x | 11x |
-| stopwatch-modern.js | 172 ms | 577 ms | 3.4x | 3.3x | 286x |
-| linq-js.js | 1.00 ms | 3.66 ms | 3.7x | 3.3x | 4.0x |
-| dromaeo-core-eval-modern.js | 1.93 ms | 7.44 ms | 3.9x | 3.9x | 95x |
-| dromaeo-core-eval.js | 1.79 ms | 7.23 ms | 4.0x | 4.1x | 95x |
-| dromaeo-string-base64.js | 20.5 ms | 91.0 ms | 4.4x | 4.5x | 335x |
-| dromaeo-string-base64-modern.js | 23.4 ms | 102 ms | 4.4x | 4.0x | 338x |
-| stopwatch.js | 155 ms | 746 ms | 4.8x | 4.9x | 284x |
-| evaluation-modern.js | 17 µs | 111 µs | 6.5x | 16x | 14x |
-| evaluation.js | 16 µs | 113 µs | 6.9x | 18x | 14x |
-| array-stress.js | 3.08 ms | 22.1 ms | 7.2x | 6.4x | 65x |
-| dromaeo-object-array-modern.js | 15.0 ms | 164 ms | 10.9x | 11.0x | 128x |
-| dromaeo-object-array.js | 13.8 ms | 162 ms | 11.8x | 12.0x | 126x |
-| minimal.js | 7.5 µs | 97 µs | 13.0x | 31x | 18x |
-| dromaeo-object-regexp-modern.js | 110 ms | **12.5 s** | **114x** | 130x | 346x |
-| dromaeo-object-regexp.js | 104 ms | **12.9 s** | **124x** | 163x | 344x |
-
-Two notes on prepared mode:
-
-- On the tiny scripts (`minimal`, `evaluation`) prepared mode looks *worse*, not
- better. Jint's parsed-script path drops to a few microseconds, while Starling
- still pays a fixed per-run setup cost. The ratio grows because Jint's
- denominator shrank, not because Starling slowed down.
-- `linq-js` is the win. Prepared Starling (182 µs) beats cold Jint (1,000 µs).
- Against Jint's own prepared path (55 µs) it is still 3.3x behind, but the
- compiled-artifact reuse clearly pays off on real library code.
-
-The full table with error bars and per-generation garbage-collector counts is
-written to `BenchmarkDotNet.Artifacts/results/` under whatever folder you run the
-command from. That folder is gitignored.
-
-## Published reference numbers (the four-engine field)
-
-We only run Starling and Jint locally, so those two columns above are the
-trustworthy same-machine pair. The other three engines in Jint's suite —
-NiL.JS, Jurassic, and YantraJS — are not run here. The numbers below are Jint's
-own published table, copied verbatim for reference.
-
-**Different hardware. Treat as a rough guide, not a same-machine ranking.**
-Jint's board ran on an AMD Ryzen 9 5950X under Windows 11 with .NET 10.0.7
-(BenchmarkDotNet 0.15.8, last updated 2026-05-10). Our local board is an Apple
-M3 Max under macOS. Even the Jint column differs from ours for that reason — for
-example regexp reads 135 ms there against 104 ms here.
-
-Cold `Jint` mean per run, base (non-`modern`) scripts:
-
-| Script | Jint | NiL.JS | Jurassic | YantraJS |
-|---|--:|--:|--:|--:|
-| minimal | 2.7 µs | 2.8 µs | 2,305 µs | 153 µs |
-| evaluation | 15 µs | 26 µs | 2,110 µs | 156 µs |
-| linq-js | 1.20 ms | 3.97 ms | 36.2 ms | 0.34 ms |
-| dromaeo-core-eval | 2.46 ms | 1.23 ms | 17.1 ms | 4.54 ms |
-| array-stress | 3.60 ms | 4.85 ms | 9.15 ms | 15.3 ms |
-| dromaeo-3d-cube | 12.4 ms | 6.19 ms | 55.1 ms | 3.00 ms |
-| dromaeo-object-array | 18.5 ms | 52.2 ms | 35.4 ms | 65.7 ms |
-| dromaeo-string-base64 | 26.9 ms | 25.9 ms | 47.1 ms | 43.0 ms |
-| dromaeo-object-string | 155 ms | 128 ms | 205 ms | 173 ms |
-| stopwatch | 195 ms | 132 ms | 142 ms | 63.4 ms |
-| dromaeo-object-regexp | 135 ms | 528 ms | 678 ms | 1,060 ms |
-
-The pattern from that board: Jint and NiL.JS trade the top spot on most scripts.
-YantraJS wins the graphics-heavy 3d-cube and the stopwatch loops, but allocates
-enormous amounts of memory (over 1 GB on object-array, against Jint's 10 MB).
-Jurassic is slow to start and weak on `eval`-style scripts.
-
-One thing stands out for our roadmap: even the *slowest* published engine on
-regexp, YantraJS at about 1.06 seconds, is still roughly 12x faster than
-Starling's 12.9 seconds. The regular-expression gap is not a Starling-vs-Jint
-problem. It is last place against the whole field.
-
-## Where this puts Starling
-
-Slotting Starling in by its ratio to Jint: on the close and middle scripts (1.2x
-to 7x), Starling lands near Jurassic's tier — behind Jint and NiL.JS. On the
-regular-expression scripts it would sit dead last by a wide margin until the
-Starling regular-expression engine is fixed. On `linq-js` with a prepared
-script, Starling is genuinely competitive.
-
-So: a solid mid-pack interpreter that already runs everything, with two clear
-work items — the regular-expression engine, and allocation per operation.
+`new Function`, regular expressions, and the 34 KB `linq-js` library. For a
+young engine that is the headline.
+
+Timings on Apple M3 Max, macOS 26.3, .NET 10.0.8 Arm64 (BenchmarkDotNet,
+ShortRun; mean per cold run):
+
+| Script | Starling |
+|---|--:|
+| minimal.js | 97 µs |
+| evaluation.js | 113 µs |
+| evaluation-modern.js | 111 µs |
+| linq-js.js | 3.66 ms |
+| dromaeo-core-eval.js | 7.23 ms |
+| dromaeo-core-eval-modern.js | 7.44 ms |
+| dromaeo-3d-cube.js | 20.8 ms |
+| array-stress.js | 22.1 ms |
+| dromaeo-3d-cube-modern.js | 24.2 ms |
+| dromaeo-string-base64.js | 91.0 ms |
+| dromaeo-string-base64-modern.js | 102 ms |
+| dromaeo-object-array.js | 162 ms |
+| dromaeo-object-array-modern.js | 164 ms |
+| dromaeo-object-string.js | 169 ms |
+| dromaeo-object-string-modern.js | 180 ms |
+| stopwatch-modern.js | 577 ms |
+| stopwatch.js | 746 ms |
+| dromaeo-object-regexp.js | **12.9 s** |
+| dromaeo-object-regexp-modern.js | **12.5 s** |
+
+Two clear work items fall out of the ranking:
+
+1. **The regular-expression engine.** The two regexp scripts take about 13
+ seconds while everything else stays under a second. This is the
+ regular-expression engine, not the interpreter, and it is the single biggest
+ thing to fix.
+2. **Allocation per operation.** A no-JIT bytecode interpreter does more boxing
+ and short-lived allocation per operation, and the memory columns show it —
+ worst on the regexp and stopwatch scripts. High allocation drives
+ garbage-collector pauses that hurt frame time in the browser.
+
+One bright spot: on `linq-js`, the prepared path (182 µs) shows the
+compiled-artifact reuse paying off on real library code — a ~20x drop from the
+cold run.
+
+A note on prepared mode: on the tiny scripts (`minimal`, `evaluation`) prepared
+mode saves little, because a fixed per-run setup cost (fresh runtime + realm
+bootstrap) dominates. The `bootstrap` case in `StarlingFeatureBench` tracks that
+cost in isolation.
diff --git a/bench/html-parser-comparison.md b/bench/html-parser-comparison.md
index 79f6331c..07ebde5a 100644
--- a/bench/html-parser-comparison.md
+++ b/bench/html-parser-comparison.md
@@ -3,8 +3,8 @@
Where does the Starling HTML parser land against a mature, pure-managed
reference parser? This runs Starling.Html and [AngleSharp](https://github.com/AngleSharp/AngleSharp)
on the same pages, on one machine, ranked together so the numbers compare
-fairly. It is the HTML-parsing counterpart of `engine-comparison.md`, which does
-the same for the Starling JS engine against Jint.
+fairly. It is the HTML-parsing counterpart of `engine-comparison.md`, which
+measures the Starling JS engine on its script suite.
The benchmark lives in `bench/Starling.HtmlParserBench`. AngleSharp is a
dev-only dependency there. No engine project references it, so the managed-first
diff --git a/browser-plan/00_INDEX.md b/browser-plan/00_INDEX.md
index 0eb0cb51..d7d58477 100644
--- a/browser-plan/00_INDEX.md
+++ b/browser-plan/00_INDEX.md
@@ -26,7 +26,7 @@
| Native code | None. Pure managed. `System.Security.Cryptography` BCL primitives are allowed; everything above the primitive layer is hand-written. | user |
| UI | Avalonia 12 (stable 12.0.x, released Apr 2026; targets .NET 10 directly; .NET 8+ only) | user |
| Rasterization | `SixLabors.ImageSharp` 3.x + `SixLabors.ImageSharp.Drawing` 2.x + `SixLabors.Fonts` 2.x | user |
-| JS engine | Hand-written in C#. Jint and Acornima may be consulted as references but are **not dependencies**. | user |
+| JS engine | The Starling JS engine, written from scratch in C#. No third-party JS engine dependencies. | user |
| Networking | Hand-written from `System.Net.Sockets` up. No `HttpClient`, no `SslStream`. | user |
| Process model | Single-process for v1. Ladybird-style multi-process sandboxing deferred to v2. | this plan |
| Cross-platform | Windows + macOS + Linux from day one. No platform branches without an `OPEN QUESTION`. | user |
diff --git a/browser-plan/09_JS_ENGINE.md b/browser-plan/09_JS_ENGINE.md
index 50a3c4ed..b30ff5c7 100644
--- a/browser-plan/09_JS_ENGINE.md
+++ b/browser-plan/09_JS_ENGINE.md
@@ -7,76 +7,37 @@
## Goal posture
-**Hand-write everything.** Reference implementations to read (not copy):
+**Write everything in-house.** Reference implementations to read (not copy):
- `Acornima` (C# port of Acorn) — clean ESTree-style parser.
-- `Jint` (C#) — interpreter, env records, intrinsics.
- `Boa` (Rust) — modern register VM design.
- `LibJS` (Ladybird) — spec-faithful bytecode VM.
These are read for **structure** and **algorithm**, not copied. Our shape is closer to LibJS: AST → bytecode → register VM.
-## Alternative engine backend (Jint)
-
-The browser can run on a second, runtime-selectable JS engine: **[Jint](https://github.com/sebastienros/jint)**,
-a pure-managed C# ECMAScript interpreter. It is a **temporary compatibility
-crutch** — it lets Starling render real-world pages at near-full ECMAScript
-conformance *today* while the in-house `Starling.Js` engine climbs toward its
-own conformance target. It is meant to be removed once `Starling.Js` is good
-enough; the architecture is built so removal is a one-step deletion.
-
-- **Why Jint fits the interop policy.** Jint is pure-managed (its only dependency
- is the managed Acornima parser; no `runtimes/` native assets, no P/Invoke), so
- it satisfies the managed-first rule exactly like BouncyCastle. Both new
- projects are in the CI interop-seam allowlist.
-- **Selection.** Set `STARLING_JS_ENGINE=jint` (default `starling`). The selector
- (`Starling.Engine/JsEngineSelector.cs`) mirrors `PaintBackendSelector`: lazy,
- default-on-unset, loud-fail on an unknown value.
-- **Conformance delta (measured on tc39/test262 `language`, identical corpus).**
- Jint ≈ **99.6%** vs `Starling.Js` ≈ **81%** — about a 19-point web-compat gap
- that the crutch closes while the in-house engine catches up. Run both numbers
- yourself via the `Conformance_pass_rate` / `Jint_conformance_pass_rate` tests
- in `tests/Starling.Js.Test262.Tests` (corpus fetched by `tools/fetch-test262.sh`).
-
-### Architecture — the narrow seam
-
-The engine-neutral shared asset is `Starling.Dom` (the real DOM); both engines
-wrap *the same* Dom nodes, only the marshalling differs. So the abstraction lives
-at the `Starling.Engine` ↔ JS boundary, **not** at the `JsValue`/`JsObject`
-level — the ~956 existing `Starling.Bindings` call sites are untouched.
+## Engine backend seam
+
+The Starling JS engine is the only JS backend. The engine still talks to it
+through a narrow, engine-neutral seam so the browser's orchestration never
+depends on engine internals.
+
+The engine-neutral shared asset is `Starling.Dom` (the real DOM); the backend
+wraps the same Dom nodes. The abstraction lives at the `Starling.Engine` ↔ JS
+boundary, **not** at the `JsValue`/`JsObject` level.
- `src/Starling.Js.Hosting` — the seam: `IScriptEngineFactory`, `IScriptSession`,
`ScriptSessionOptions`, `ScriptThrow`, and the shared `ILayoutHost`. Depends
- only on Dom/Net/Common/Url; references neither engine.
-- `src/Starling.Bindings` — hosts the default **Starling.Js** backend
+ only on Dom/Net/Common/Url; references no JS engine.
+- `src/Starling.Bindings` — hosts the **Starling.Js** backend
(`StarlingScriptSession`) over the existing `JsRuntime` path.
-- `src/Starling.Bindings.Jint` — the **Jint** backend: `JintScriptSession` plus a
- full, idiomatic re-exposure of the Web-API surface over Jint interop
- (Node/Element/Document, EventTarget/Event, Window/Storage/History/Performance,
- timers/rAF + event-loop pump, fetch, XMLHttpRequest, observers/crypto/cookies,
- ES modules). References Jint + the seam + Dom/Net/Css/Html/Common/Url — **not**
- `Starling.Js` or `Starling.Bindings`.
`Starling.Engine` keeps all orchestration (script ordering, DOMContentLoaded/load
-timing, the async pump) and talks only to `IScriptSession`. Both backends pass
-the same engine integration suite (`tests/Starling.Engine.Tests`, 151/151) and
-CI runs that suite under both engines.
-
-The full design contract and the work-package history are in
-[`tasks/jint/DESIGN.md`](../tasks/jint/DESIGN.md) and
-[`tasks/jint/TRACKER.md`](../tasks/jint/TRACKER.md).
-
-### Removal checklist (when `Starling.Js` is good enough)
-
-1. Delete `src/Starling.Bindings.Jint/` and `tests/Starling.Bindings.Jint.Tests/`.
-2. Remove the `jint` arm from `Starling.Engine/JsEngineSelector.cs` (and its
- `Starling.Bindings.Jint` project reference); the env var then only accepts
- `starling`.
-3. Remove the `Jint` `PackageVersion` from `Directory.Packages.props`.
-4. Remove the Jint test262 harness (`Jint*Test262*` in
- `tests/Starling.Js.Test262.Tests`) and the Jint files from `Starling.slnx`,
- the CI interop allowlist, and the `STARLING_JS_ENGINE=jint` CI step.
-5. **Keep** `src/Starling.Js.Hosting` (the seam) and `ILayoutHost` there — the
- seam is a clean abstraction worth retaining even with a single engine.
+timing, the async pump) and talks only to `IScriptSession`. Selection is
+`STARLING_JS_ENGINE` (default and only accepted value: `starling`); the selector
+(`Starling.Engine/JsEngineSelector.cs`) mirrors `PaintBackendSelector`: lazy,
+default-on-unset, loud-fail on an unknown value.
+
+The seam and `ILayoutHost` stay even with a single engine — it is a clean
+abstraction worth retaining.
## Spec refs
@@ -625,4 +586,4 @@ Use [Test262](https://github.com/tc39/test262). Subset selection: language featu
- [ ] `for-of` over a generator yields the expected sequence.
- [ ] `Proxy` traps for `get`/`set`/`has`/`deleteProperty`/`ownKeys` fire correctly.
- [ ] Stack overflow surfaces as a `RangeError` with a meaningful trace, not as a C# `StackOverflowException`.
-- [ ] No `DllImport`, no `Jint` import, no `Microsoft.JScript`. `grep -rn 'DllImport\|Jint\|JScript' src/Starling.Js/` is empty.
+- [ ] No `DllImport`, no third-party JS engine import. `grep -rn 'DllImport\|JScript' src/Starling.Js/` is empty.
diff --git a/browser-plan/anglesharp-backend-plan.md b/browser-plan/anglesharp-backend-plan.md
index fa02a0ef..f7c93cba 100644
--- a/browser-plan/anglesharp-backend-plan.md
+++ b/browser-plan/anglesharp-backend-plan.md
@@ -1,8 +1,8 @@
# Plan: AngleSharp as a swappable HTML-parser backend
This is the agreed plan for adding AngleSharp as an opt-in, off-by-default HTML
-parser, picked at runtime. It mirrors how the JS engine already switches between
-the Starling engine and Jint. The Starling parser stays the default.
+parser, picked at runtime. It mirrors the JS-engine backend seam. The Starling
+parser stays the default.
Read this top to bottom. It is self-contained, so a fresh session can pick it up
and build it without the chat history.
@@ -57,7 +57,6 @@ except the new backend project plus the selector wiring. It is deletable.
- `src/Starling.Bindings/NodeBindings.cs` — `innerHTML` / `outerHTML` /
`insertAdjacentHTML` (helper around line 2325-2328, callers near 581, 601,
1115+).
-- `src/Starling.Bindings.Jint/NodeBindings.cs` — the Jint mirror (around 1209).
- `src/Starling.Shell.Native/*` — demo and window render call sites
(`NativePresentDemo.cs:63`, `NativeBrowserWindow.cs:186` and neighbors).
@@ -65,14 +64,12 @@ except the new backend project plus the selector wiring. It is deletable.
- Seam interface lives in `src/Starling.Js.Hosting` (`IScriptEngineFactory`,
`IScriptSession`). It depends only on `Starling.Dom` and `Starling.Common`.
-- Backends: `src/Starling.Bindings` (Starling) and
- `src/Starling.Bindings.Jint` (Jint, references only the seam plus the Jint
- package). Each provides a factory.
+- Backend: `src/Starling.Bindings` (Starling) provides the factory.
- Selector: `src/Starling.Engine/JsEngineSelector.cs` reads `STARLING_JS_ENGINE`
- once, caches the choice, and builds the factory. It lives in `Starling.Engine`
- because that is the only project that references both backends.
-- Flags: `src/Starling.AppHost/AppHost.cs` maps `--jint` / `--starling` with a
- reusable `SelectFlag` helper, strips them before Aspire, and forwards the
+ once, caches the choice, and builds the factory. It lives in `Starling.Engine`,
+ where the backend assembly is referenced.
+- Flags: `src/Starling.AppHost/AppHost.cs` maps `--starling` with a
+ reusable `SelectFlag` helper, strips it before Aspire, and forwards the
choice as an environment variable. `src/Starling.Gui/Program.cs` defaults the
env var when it is unset. Flag beats env var beats default.
- `Starling.Dom` already grants `InternalsVisibleTo` to `Starling.Html` and
@@ -129,8 +126,7 @@ startup.
- Add an `HtmlTemplateElement` (or equivalent) whose `Content` is a
`DocumentFragment`, following the DOM standard for `template.content`.
-- Wire `template.content` in both `src/Starling.Bindings/NodeBindings.cs` and
- `src/Starling.Bindings.Jint/NodeBindings.cs`.
+- Wire `template.content` in `src/Starling.Bindings/NodeBindings.cs`.
- Update the Starling parser's `` handling to place template children
into the content fragment.
- Tests for the content-fragment semantics. This is core-DOM work that both
@@ -164,15 +160,15 @@ startup.
(`starling` default, `anglesharp`). It sets `HtmlParsing.Backend` at startup.
This is the only project that references the AngleSharp backend.
- Flags `--anglesharp-html` / `--starling-html` in `AppHost.cs` and the env-var
- default in `Gui/Program.cs`, matching the Jint pattern.
+ default in `Gui/Program.cs`, matching the JS-engine flag pattern.
- Differential test project `tests/Starling.Html.AngleSharp.Tests`: parse the
html5lib fixtures and our snapshot pages through both backends and assert the
serialized Starling DOM matches. Any diff is either an adapter bug or a real
Starling-parser spec gap. Both are worth finding.
- Add an `AngleSharp+copy` column to the existing `HtmlParserBench` so we measure
the real in-engine cost, not the raw AngleSharp number.
-- Short `STARLING_HTML_PARSER` note in `AGENTS.md`, matching the Jint write-up,
- and a `tasks/` work package per the repo workflow.
+- Short `STARLING_HTML_PARSER` note in `AGENTS.md` and a `tasks/` work package
+ per the repo workflow.
## Performance note (important, do not skip)
@@ -186,6 +182,6 @@ The `AngleSharp+copy` benchmark column in Phase 4 exists to keep this honest.
## How this honors the locked "own engine" decision
`browser-plan/00_INDEX.md` locks "own engine, no Chromium/Gecko/WebKit reuse."
-This stays true the same way Jint does: AngleSharp is opt-in, off by default, and
+This stays true: AngleSharp is opt-in, off by default, and
deletable by removing one project and one selector arm. The Starling parser
remains the default and the thing we keep building.
diff --git a/src/Starling.AppHost/AppHost.cs b/src/Starling.AppHost/AppHost.cs
index 79ac3e22..3ae921f5 100644
--- a/src/Starling.AppHost/AppHost.cs
+++ b/src/Starling.AppHost/AppHost.cs
@@ -3,13 +3,12 @@
// Run with:
//
// aspire run # defaults: --starling --imagesharp-gpu
-// aspire run -- --jint # Jint JS backend
// aspire run -- --starling --imagesharp # Starling + CPU paint backend
//
// Runtime-selection flags (everything after `aspire run --` lands in args) are
// parsed here and forwarded to BOTH the gui and headless resources:
//
-// --starling | --jint JS engine (default: --starling)
+// --starling JS engine (default: --starling)
// --imagesharp | --imagesharp-gpu paint backend (default: --imagesharp-gpu)
// --starling-html | --anglesharp-html HTML parser (default: --starling-html)
//
@@ -117,11 +116,10 @@ static string LocateRepoRoot()
return string.IsNullOrWhiteSpace(value) ? null : value;
}
-// --starling | --jint -> STARLING_JS_ENGINE value (null if no flag given).
+// --starling -> STARLING_JS_ENGINE value (null if no flag given).
static string? SelectJsEngine(string[] args) => SelectFlag(
args, "JS engine",
- ("--starling", "starling"),
- ("--jint", "jint"));
+ ("--starling", "starling"));
// --imagesharp/--cpu | --imagesharp-gpu/--imagesharp-webgpu/--gpu -> STARLING_PAINT_BACKEND.
static string? SelectPaintBackend(string[] args) => SelectFlag(
@@ -139,7 +137,7 @@ static string LocateRepoRoot()
("--anglesharp-html", "anglesharp"));
// Scan args for any of the given flag->value mappings; return the selected value
-// (null if none present). Throws on conflicting selections (e.g. --jint --starling).
+// (null if none present). Throws on conflicting selections (e.g. --imagesharp --imagesharp-gpu).
static string? SelectFlag(string[] args, string label, params (string Flag, string Value)[] mappings)
{
string? selected = null;
@@ -169,7 +167,7 @@ static string LocateRepoRoot()
// True for any flag SelectJsEngine / SelectPaintBackend recognize, so it can be
// stripped before the args reach Aspire's command-line configuration provider.
static bool IsStarlingSelectionFlag(string arg) => arg is
- "--starling" or "--jint"
+ "--starling"
or "--imagesharp" or "--cpu"
or "--imagesharp-gpu" or "--imagesharp-webgpu" or "--gpu"
or "--starling-html" or "--anglesharp-html";
diff --git a/src/Starling.Bindings.Jint/AnimationFrameBinding.cs b/src/Starling.Bindings.Jint/AnimationFrameBinding.cs
deleted file mode 100644
index 16b8a536..00000000
--- a/src/Starling.Bindings.Jint/AnimationFrameBinding.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using Jint.Native;
-using Jint.Runtime;
-using Microsoft.Extensions.Logging;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// HTML §"run the animation frame callbacks" for the Jint backend — installs
-/// requestAnimationFrame / cancelAnimationFrame on the global,
-/// routing through so rAF shares the
-/// simulated clock with the timers and a rAF-bootstrapped page settles on the
-/// same .
-///
-///
-/// Mirrors Starling.Bindings/AnimationFrameBinding.cs: each frame the loop
-/// snapshots the rAF queue and dispatches every pending callback with the same
-/// DOMHighResTimeStamp (CSS Animations 1 §3.5); callbacks scheduled
-/// during the drain land on the next frame. Errors out of a
-/// callback errors are logged so the loop keeps firing the remaining callbacks in the frame.
-///
-internal static class AnimationFrameBinding
-{
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- var engine = ctx.Engine;
- var loop = ctx.Loop;
-
- JintInterop.DefineMethod(engine, engine.Global, "requestAnimationFrame", (_, args) =>
- {
- if (args.Length == 0 || args[0] is not global::Jint.Native.Function.Function)
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError,
- "requestAnimationFrame argument is not callable");
- }
-
- var handler = args[0];
- var id = loop.RequestAnimationFrame(timestamp => InvokeCallback(ctx, handler, timestamp));
- return JintInterop.Num(id);
- }, 1);
-
- JintInterop.DefineMethod(engine, engine.Global, "cancelAnimationFrame", (_, args) =>
- {
- if (TryCoerceId(args, out var id))
- {
- loop.CancelAnimationFrame(id);
- }
-
- return JsValue.Undefined;
- }, 1);
- }
-
- private static bool TryCoerceId(JsValue[] args, out int id)
- {
- id = 0;
- if (args.Length == 0)
- {
- return false;
- }
-
- var n = TypeConverter.ToNumber(args[0]);
- if (double.IsNaN(n) || double.IsInfinity(n))
- {
- return false;
- }
-
- if (n < int.MinValue || n > int.MaxValue)
- {
- return false;
- }
-
- id = (int)n;
- return true;
- }
-
- private static void InvokeCallback(JintBackendContext ctx, JsValue handler, double timestamp)
- {
- var jsLog = ctx.LoggerFactory.CreateLogger("Starling.engine.js");
- try
- {
- ctx.Engine.Invoke(handler, JsValue.Undefined, new JsValue[] { JintInterop.Num(timestamp) });
- ctx.Engine.Advanced.ProcessTasks();
- }
- catch (JavaScriptException ex)
- {
- AnimationFrameBindingLog.UncaughtInAnimationFrame(jsLog,
- JintInterop.DescribeError(ex.Error, ex.Message));
- }
- catch (Exception ex)
- {
- AnimationFrameBindingLog.UncaughtInAnimationFrame(jsLog, ex.Message);
- }
- }
-}
-
-internal static partial class AnimationFrameBindingLog
-{
- [LoggerMessage(Level = LogLevel.Warning, Message = "Uncaught (in animation frame) {Detail}")]
- public static partial void UncaughtInAnimationFrame(ILogger logger, string detail);
-}
diff --git a/src/Starling.Bindings.Jint/AttrBinding.cs b/src/Starling.Bindings.Jint/AttrBinding.cs
deleted file mode 100644
index b35ca8bc..00000000
--- a/src/Starling.Bindings.Jint/AttrBinding.cs
+++ /dev/null
@@ -1,325 +0,0 @@
-using System.Globalization;
-using Jint;
-using Jint.Native;
-using Jint.Native.Object;
-using Jint.Native.Symbol;
-using Jint.Runtime;
-using Jint.Runtime.Descriptors;
-using Jint.Runtime.Interop;
-using Starling.Dom;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// DOM §4.9 Attr / NamedNodeMap for the Jint backend, mirroring the canonical
-/// backend's JsNamedNodeMapObject + Element Attr-node methods. Before this,
-/// element.attributes returned a plain snapshot array of {name,value}
-/// and there were no Attr-node methods. This installs a real NamedNodeMap
-/// interface, makes element.attributes a live exotic map of wrapped
-/// s, and adds getAttributeNode/setAttributeNode/
-/// … plus document.createAttribute(NS). The Attr interface itself is
-/// installed by (its prototype slot + global).
-///
-internal static class AttrBinding
-{
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- var engine = ctx.Engine;
- var elProto = ctx.Wrappers.ElementPrototype;
- var docProto = ctx.Wrappers.DocumentPrototype;
- if (elProto is null || docProto is null)
- {
- return;
- }
-
- // ---- NamedNodeMap.prototype --------------------------------------------
- var proto = new JsObject(engine);
- ctx.Wrappers.NamedNodeMapPrototype = proto;
- JintInterop.DefineAccessor(engine, proto, "length",
- (t, _) => JintInterop.Num(t is JintNamedNodeMapObject m ? m.Length : 0));
- JintInterop.DefineMethod(engine, proto, "item",
- (t, a) => t is JintNamedNodeMapObject m && a.Length > 0
- ? m.WrapAttr(m.GetItem((int)TypeConverter.ToNumber(a[0]))) : JsValue.Null, 1);
- JintInterop.DefineMethod(engine, proto, "getNamedItem",
- (t, a) => t is JintNamedNodeMapObject m && a.Length > 0
- ? m.WrapAttr(m.Element.Attributes.GetNamedItem(TypeConverter.ToString(a[0]))) : JsValue.Null, 1);
- JintInterop.DefineMethod(engine, proto, "getNamedItemNS",
- (t, a) => t is JintNamedNodeMapObject m && a.Length >= 2
- ? m.WrapAttr(m.Element.Attributes.GetNamedItemNS(a[0].IsNull() || a[0].IsUndefined() ? null : TypeConverter.ToString(a[0]), TypeConverter.ToString(a[1])))
- : JsValue.Null, 2);
- JintInterop.DefineMethod(engine, proto, "setNamedItem", (t, a) =>
- {
- if (t is not JintNamedNodeMapObject m)
- {
- return JsValue.Null;
- }
-
- if (a.Length == 0 || ctx.Wrappers.Unwrap(a[0]) is not AttrNode attr)
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError, "setNamedItem requires an Attr argument");
- }
-
- return m.WrapAttr(m.Element.Attributes.SetNamedItem(attr));
- }, 1);
- JintInterop.DefineMethod(engine, proto, "setNamedItemNS", (t, a) =>
- {
- if (t is not JintNamedNodeMapObject m)
- {
- return JsValue.Null;
- }
-
- if (a.Length == 0 || ctx.Wrappers.Unwrap(a[0]) is not AttrNode attr)
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError, "setNamedItemNS requires an Attr argument");
- }
-
- return m.WrapAttr(m.Element.Attributes.SetNamedItemNS(attr));
- }, 1);
- JintInterop.DefineMethod(engine, proto, "removeNamedItem", (t, a) =>
- {
- if (t is not JintNamedNodeMapObject m)
- {
- return JsValue.Null;
- }
-
- var name = a.Length > 0 ? TypeConverter.ToString(a[0]) : "";
- var removed = m.Element.Attributes.GetNamedItem(name)
- ?? throw DomExceptionBinding.Throw(ctx, "NotFoundError", "The node was not found.");
- m.Element.Attributes.RemoveNamedItem(name);
- return m.WrapAttr(removed);
- }, 1);
- JintInterop.DefineMethod(engine, proto, "removeNamedItemNS", (t, a) =>
- {
- if (t is not JintNamedNodeMapObject m)
- {
- return JsValue.Null;
- }
-
- var ns = a.Length > 0 && !a[0].IsNull() && !a[0].IsUndefined() ? TypeConverter.ToString(a[0]) : null;
- var local = a.Length > 1 ? TypeConverter.ToString(a[1]) : "";
- var removed = m.Element.Attributes.GetNamedItemNS(ns, local)
- ?? throw DomExceptionBinding.Throw(ctx, "NotFoundError", "The node was not found.");
- m.Element.Attributes.RemoveNamedItemNS(ns, local);
- return m.WrapAttr(removed);
- }, 2);
- WireInterface(engine, proto, "NamedNodeMap");
-
- // ---- Element Attr-node methods -----------------------------------------
- JintInterop.DefineMethod(engine, elProto, "getAttributeNode", (t, a) =>
- {
- if (ctx.Wrappers.UnwrapElement(t) is not { } e || a.Length == 0)
- {
- return JsValue.Null;
- }
-
- var attr = e.Attributes.GetNamedItem(TypeConverter.ToString(a[0]));
- return attr is null ? JsValue.Null : ctx.Wrappers.Wrap(attr);
- }, 1);
- JintInterop.DefineMethod(engine, elProto, "getAttributeNodeNS", (t, a) =>
- {
- if (ctx.Wrappers.UnwrapElement(t) is not { } e || a.Length < 2)
- {
- return JsValue.Null;
- }
-
- var ns = a[0].IsNull() || a[0].IsUndefined() ? null : TypeConverter.ToString(a[0]);
- var attr = e.Attributes.GetNamedItemNS(ns, TypeConverter.ToString(a[1]));
- return attr is null ? JsValue.Null : ctx.Wrappers.Wrap(attr);
- }, 2);
- JintInterop.DefineMethod(engine, elProto, "setAttributeNode", (t, a) =>
- {
- if (ctx.Wrappers.UnwrapElement(t) is not { } e || a.Length == 0)
- {
- return JsValue.Null;
- }
-
- if (ctx.Wrappers.Unwrap(a[0]) is not AttrNode attr)
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError, "setAttributeNode requires an Attr argument");
- }
-
- var old = e.Attributes.SetNamedItem(attr);
- return old is null ? JsValue.Null : ctx.Wrappers.Wrap(old);
- }, 1);
- JintInterop.DefineMethod(engine, elProto, "setAttributeNodeNS", (t, a) =>
- {
- if (ctx.Wrappers.UnwrapElement(t) is not { } e || a.Length == 0)
- {
- return JsValue.Null;
- }
-
- if (ctx.Wrappers.Unwrap(a[0]) is not AttrNode attr)
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError, "setAttributeNodeNS requires an Attr argument");
- }
-
- var old = e.Attributes.SetNamedItemNS(attr);
- return old is null ? JsValue.Null : ctx.Wrappers.Wrap(old);
- }, 1);
- JintInterop.DefineMethod(engine, elProto, "removeAttributeNode", (t, a) =>
- {
- if (ctx.Wrappers.UnwrapElement(t) is not { } e || a.Length == 0)
- {
- return JsValue.Null;
- }
-
- if (ctx.Wrappers.Unwrap(a[0]) is not AttrNode attr)
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError, "removeAttributeNode requires an Attr argument");
- }
-
- var found = e.Attributes.GetNamedItem(attr.Name);
- if (found is null || !ReferenceEquals(found, attr))
- {
- throw DomExceptionBinding.Throw(ctx, "NotFoundError", "The node was not found.");
- }
-
- e.Attributes.RemoveNamedItem(attr.Name);
- return ctx.Wrappers.Wrap(attr);
- }, 1);
-
- // ---- document.createAttribute ------------------------------------------
- // NOTE: createAttributeNS needs AttrNode.CreateNamespaced, which is internal
- // to Starling.Dom and not visible to this assembly — tracked under Tier 4.
- JintInterop.DefineMethod(engine, docProto, "createAttribute", (_, a) =>
- {
- var name = a.Length > 0 ? TypeConverter.ToString(a[0]) : "";
- if (name.Length == 0)
- {
- throw DomExceptionBinding.Throw(ctx, "InvalidCharacterError", "createAttribute: empty name");
- }
-
- return ctx.Wrappers.Wrap(new AttrNode(name.ToLowerInvariant()));
- }, 1);
- }
-
- /// Build the live NamedNodeMap exotic backing
- /// element.attributes.
- public static JsValue WrapAttributes(JintBackendContext ctx, Element element)
- => new JintNamedNodeMapObject(ctx, element);
-
- private static void WireInterface(global::Jint.Engine engine, ObjectInstance proto, string name)
- {
- var ctor = new ClrFunction(engine, name,
- (_, _) => throw new JavaScriptException(engine.Intrinsics.TypeError, "Illegal constructor"), 0, PropertyFlag.Configurable);
- ctor.Set("prototype", proto);
- JintInterop.DefineDataProp(proto, "constructor", ctor, writable: true, enumerable: false, configurable: true);
- proto.DefineOwnProperty(GlobalSymbolRegistry.ToStringTag,
- new PropertyDescriptor(JintInterop.Str(name), writable: false, enumerable: false, configurable: true));
- JintInterop.DefineDataProp(engine.Global, name, ctor, writable: true, enumerable: false, configurable: true);
- }
-}
-
-/// Live NamedNodeMap (DOM §4.9.1) backing element.attributes:
-/// integer indices and attribute names resolve to wrapped s
-/// against the element's live attribute list. Methods (item/getNamedItem/…) are
-/// inherited from %NamedNodeMapPrototype% and are never shadowed by an attribute
-/// of the same name.
-internal sealed class JintNamedNodeMapObject : ObjectInstance
-{
- private readonly JintBackendContext _ctx;
- public Element Element { get; }
-
- public JintNamedNodeMapObject(JintBackendContext ctx, Element element) : base(ctx.Engine)
- {
- _ctx = ctx;
- Element = element;
- if (ctx.Wrappers.NamedNodeMapPrototype is { } p)
- {
- Prototype = p;
- }
- }
-
- public int Length => Element.Attributes.Count;
-
- public AttrNode? GetItem(int index)
- => index >= 0 && index < Element.Attributes.Count ? Element.Attributes[index] : null;
-
- public JsValue WrapAttr(AttrNode? attr) => attr is null ? JsValue.Null : _ctx.Wrappers.Wrap(attr);
-
- private bool IsOnPrototype(JsValue name)
- {
- for (var p = Prototype; p is not null; p = p.Prototype)
- {
- if (p.GetOwnProperty(name) != PropertyDescriptor.Undefined)
- {
- return true;
- }
- }
-
- return false;
- }
-
- public override JsValue Get(JsValue property, JsValue receiver)
- {
- if (property.IsString())
- {
- var name = property.AsString();
- if (CollectionIndex.TryIndex(name, out var i))
- {
- return GetItem(i) is { } a ? _ctx.Wrappers.Wrap(a) : JsValue.Undefined;
- }
-
- if (!IsOnPrototype(property) && Element.Attributes.GetNamedItem(name) is { } attr)
- {
- return _ctx.Wrappers.Wrap(attr);
- }
- }
- return base.Get(property, receiver);
- }
-
- public override PropertyDescriptor GetOwnProperty(JsValue property)
- {
- if (property.IsString())
- {
- var name = property.AsString();
- if (CollectionIndex.TryIndex(name, out var i))
- {
- if (GetItem(i) is { } a)
- {
- return new PropertyDescriptor(_ctx.Wrappers.Wrap(a), writable: false, enumerable: true, configurable: true);
- }
- }
- else if (base.GetOwnProperty(property) == PropertyDescriptor.Undefined && !IsOnPrototype(property)
- && Element.Attributes.GetNamedItem(name) is { } attr)
- {
- return new PropertyDescriptor(_ctx.Wrappers.Wrap(attr), writable: false, enumerable: true, configurable: true);
- }
- }
- return base.GetOwnProperty(property);
- }
-
- public override bool HasProperty(JsValue property)
- {
- if (property.IsString())
- {
- var name = property.AsString();
- if (CollectionIndex.TryIndex(name, out var i))
- {
- return i < Element.Attributes.Count;
- }
-
- if (!IsOnPrototype(property) && Element.Attributes.GetNamedItem(name) is not null)
- {
- return true;
- }
- }
- return base.HasProperty(property);
- }
-
- public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol)
- {
- var keys = new List();
- if ((types & Types.String) != 0)
- {
- for (var i = 0; i < Element.Attributes.Count; i++)
- {
- keys.Add(JintInterop.Str(i.ToString(CultureInfo.InvariantCulture)));
- }
- }
-
- keys.AddRange(base.GetOwnPropertyKeys(types));
- return keys;
- }
-}
diff --git a/src/Starling.Bindings.Jint/BlobFileFormDataBinding.cs b/src/Starling.Bindings.Jint/BlobFileFormDataBinding.cs
deleted file mode 100644
index cd3d637d..00000000
--- a/src/Starling.Bindings.Jint/BlobFileFormDataBinding.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-namespace Starling.Bindings.Jint;
-
-///
-/// Real Blob / File / FormData classes for the Jint backend,
-/// mirroring the canonical backend's behavior. Implemented as self-contained JS
-/// classes (in-memory byte containers + an entry list) so they need no native
-/// state table: a Blob stores its bytes as a Uint8Array; File extends Blob with
-/// name/lastModified; FormData is a full entry list
-/// (append/set/delete/get/getAll/has/forEach/keys/values/iterator) accepting
-/// string and Blob/File values.
-///
-///
-/// Installed after NodeBindings (so the __starlingFormDataEntries hook used
-/// by new FormData(formElement) exists) and before/around FetchBinding,
-/// whose Response.blob() now mints a real Blob. The non-enumerable
-/// __bytes() method exposes a Blob's bytes to the fetch body path.
-///
-internal static class BlobFileFormDataBinding
-{
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- ctx.Engine.Execute(Bootstrap, "");
- }
-
- private const string Bootstrap = """
- (function () {
- 'use strict';
- const BYTES = Symbol('bytes');
- const enc = new TextEncoder();
-
- function partsToBytes(parts) {
- if (parts === undefined || parts === null) return new Uint8Array(0);
- if (!Array.isArray(parts) && typeof parts[Symbol.iterator] !== 'function')
- throw new TypeError("Blob parts must be an iterable");
- const chunks = []; let total = 0;
- for (const p of parts) {
- let b;
- if (p instanceof Blob) b = p.__bytes();
- else if (p instanceof ArrayBuffer) b = new Uint8Array(p.slice(0));
- else if (ArrayBuffer.isView(p)) b = new Uint8Array(p.buffer.slice(p.byteOffset, p.byteOffset + p.byteLength));
- else b = enc.encode(typeof p === 'string' ? p : String(p));
- chunks.push(b); total += b.length;
- }
- const out = new Uint8Array(total); let o = 0;
- for (const c of chunks) { out.set(c, o); o += c.length; }
- return out;
- }
-
- class Blob {
- constructor(parts, options) {
- this[BYTES] = partsToBytes(parts);
- this._type = (options && options.type !== undefined) ? String(options.type).toLowerCase() : '';
- }
- get size() { return this[BYTES].length; }
- get type() { return this._type; }
- // Non-spec helper for the fetch body path + slicing.
- __bytes() { return this[BYTES]; }
- arrayBuffer() {
- const b = this[BYTES];
- return Promise.resolve(b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength));
- }
- text() { return Promise.resolve(new TextDecoder().decode(this[BYTES])); }
- slice(start, end, contentType) {
- const b = this[BYTES], n = b.length;
- let s = start === undefined ? 0 : (start < 0 ? Math.max(n + (start | 0), 0) : Math.min(start | 0, n));
- let e = end === undefined ? n : (end < 0 ? Math.max(n + (end | 0), 0) : Math.min(end | 0, n));
- const out = new Blob([], { type: contentType !== undefined ? String(contentType) : '' });
- out[BYTES] = b.slice(s, Math.max(s, e));
- return out;
- }
- get [Symbol.toStringTag]() { return 'Blob'; }
- }
-
- class File extends Blob {
- constructor(parts, name, options) {
- super(parts, options);
- if (name === undefined) throw new TypeError("File requires a name");
- this._name = String(name);
- this._lastModified = (options && options.lastModified !== undefined) ? Number(options.lastModified) : 0;
- }
- get name() { return this._name; }
- get lastModified() { return this._lastModified; }
- get [Symbol.toStringTag]() { return 'File'; }
- }
-
- function fdValue(value, filename) {
- if (value instanceof Blob) {
- if (!(value instanceof File))
- return new File([value], filename !== undefined ? String(filename) : 'blob', { type: value.type });
- if (filename !== undefined)
- return new File([value], String(filename), { type: value.type, lastModified: value.lastModified });
- return value;
- }
- return String(value);
- }
-
- class FormData {
- constructor(form) {
- this.__entries = [];
- if (form !== undefined && form !== null && typeof __starlingFormDataEntries === 'function') {
- const items = __starlingFormDataEntries(form);
- for (let i = 0; i < items.length; i++) this.__entries.push([String(items[i][0]), String(items[i][1])]);
- }
- }
- append(name, value, filename) { this.__entries.push([String(name), fdValue(value, filename)]); }
- set(name, value, filename) {
- name = String(name); const v = fdValue(value, filename);
- const out = []; let placed = false;
- for (const e of this.__entries) {
- if (e[0] === name) { if (!placed) { out.push([name, v]); placed = true; } }
- else out.push(e);
- }
- if (!placed) out.push([name, v]);
- this.__entries = out;
- }
- delete(name) { name = String(name); this.__entries = this.__entries.filter(e => e[0] !== name); }
- get(name) { name = String(name); for (const e of this.__entries) if (e[0] === name) return e[1]; return null; }
- getAll(name) { name = String(name); return this.__entries.filter(e => e[0] === name).map(e => e[1]); }
- has(name) { name = String(name); return this.__entries.some(e => e[0] === name); }
- forEach(cb, thisArg) { for (const e of this.__entries.slice()) cb.call(thisArg, e[1], e[0], this); }
- *entries() { for (const e of this.__entries.slice()) yield [e[0], e[1]]; }
- *keys() { for (const e of this.__entries.slice()) yield e[0]; }
- *values() { for (const e of this.__entries.slice()) yield e[1]; }
- [Symbol.iterator]() { return this.entries(); }
- get [Symbol.toStringTag]() { return 'FormData'; }
- }
-
- const g = globalThis;
- const def = (n, v) => Object.defineProperty(g, n, { value: v, writable: true, enumerable: false, configurable: true });
- def('Blob', Blob);
- def('File', File);
- def('FormData', FormData);
- })();
- """;
-}
diff --git a/src/Starling.Bindings.Jint/CollectionsBinding.cs b/src/Starling.Bindings.Jint/CollectionsBinding.cs
deleted file mode 100644
index 961e4aa6..00000000
--- a/src/Starling.Bindings.Jint/CollectionsBinding.cs
+++ /dev/null
@@ -1,511 +0,0 @@
-using System.Globalization;
-using Jint;
-using Jint.Native;
-using Jint.Native.Object;
-using Jint.Native.Symbol;
-using Jint.Runtime;
-using Jint.Runtime.Descriptors;
-using Jint.Runtime.Interop;
-using Starling.Dom;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// Live DOM collections for the Jint backend — NodeList, HTMLCollection, and
-/// DOMTokenList — mirroring Starling.Bindings/{NodeListObject,
-/// HtmlCollectionObject,DomTokenListObject}.cs. Before this, every
-/// collection-returning member (childNodes, children,
-/// getElementsBy*, querySelectorAll, classList) returned a
-/// plain snapshot Array, so item()/namedItem(), named access
-/// (coll.id), liveness, and instanceof NodeList/HTMLCollection
-/// were all absent. This installs real interface prototypes + constructors and
-/// the exotic objects the Node bindings hand back.
-///
-internal static class CollectionsBinding
-{
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- if (ctx.Wrappers.NodeListPrototype is not null)
- {
- return; // idempotent
- }
-
- var engine = ctx.Engine;
-
- // ---- NodeList.prototype -------------------------------------------------
- var nodeListProto = new JsObject(engine);
- ctx.Wrappers.NodeListPrototype = nodeListProto;
- JintInterop.DefineAccessor(engine, nodeListProto, "length",
- (t, _) => JintInterop.Num(t is JintNodeListObject n ? n.Count : 0));
- JintInterop.DefineMethod(engine, nodeListProto, "item",
- (t, a) => t is JintNodeListObject n && a.Length > 0 ? n.Item((int)TypeConverter.ToNumber(a[0])) : JsValue.Null, 1);
- DefineIteration(engine, nodeListProto, t => (t as JintNodeListObject)?.ValuesArray(engine));
- WireInterface(engine, nodeListProto, "NodeList");
-
- // ---- HTMLCollection.prototype ------------------------------------------
- var htmlCollProto = new JsObject(engine);
- ctx.Wrappers.HtmlCollectionPrototype = htmlCollProto;
- JintInterop.DefineAccessor(engine, htmlCollProto, "length",
- (t, _) => JintInterop.Num(t is JintHtmlCollectionObject c ? c.Count : 0));
- JintInterop.DefineMethod(engine, htmlCollProto, "item",
- (t, a) => t is JintHtmlCollectionObject c && a.Length > 0 ? c.Item((int)TypeConverter.ToNumber(a[0])) : JsValue.Null, 1);
- JintInterop.DefineMethod(engine, htmlCollProto, "namedItem",
- (t, a) => t is JintHtmlCollectionObject c && a.Length > 0 ? c.NamedItemValue(TypeConverter.ToString(a[0])) : JsValue.Null, 1);
- DefineIterator(engine, htmlCollProto, t => (t as JintHtmlCollectionObject)?.ValuesArray(engine));
- WireInterface(engine, htmlCollProto, "HTMLCollection");
-
- // ---- DOMTokenList.prototype --------------------------------------------
- // The classList object sets its own per-element methods; the prototype
- // carries the iteration surface shared by every token list.
- var tokenListProto = new JsObject(engine);
- ctx.Wrappers.DomTokenListPrototype = tokenListProto;
- DefineIteration(engine, tokenListProto, t => (t as JintDomTokenListObject)?.ValuesArray(engine));
- WireInterface(engine, tokenListProto, "DOMTokenList");
- }
-
- // ---- factory helpers used by NodeBindings -------------------------------
-
- public static JintNodeListObject NodeList(JintBackendContext ctx, Func> source)
- => new(ctx, ctx.Wrappers.NodeListPrototype, source);
-
- public static JintHtmlCollectionObject HtmlCollection(JintBackendContext ctx, Func> source)
- => new(ctx, ctx.Wrappers.HtmlCollectionPrototype, source);
-
- // ---- shared iteration installers ----------------------------------------
-
- // NodeList: array-like iteration — values/keys/entries/forEach + @@iterator.
- private static void DefineIteration(global::Jint.Engine engine, ObjectInstance proto, Func snapshot)
- {
- DefineIterator(engine, proto, snapshot);
- JintInterop.DefineMethod(engine, proto, "keys", (t, _) =>
- {
- var arr = snapshot(t) ?? new JsArray(engine, System.Array.Empty());
- var keys = new JsValue[arr.Length];
- for (uint i = 0; i < arr.Length; i++)
- {
- keys[i] = JintInterop.Num(i);
- }
-
- return ArrayIterator(engine, new JsArray(engine, keys));
- }, 0);
- JintInterop.DefineMethod(engine, proto, "entries", (t, _) =>
- {
- var arr = snapshot(t) ?? new JsArray(engine, System.Array.Empty());
- var entries = new JsValue[arr.Length];
- for (uint i = 0; i < arr.Length; i++)
- {
- entries[i] = new JsArray(engine, new[] { JintInterop.Num(i), arr[(int)i] });
- }
-
- return ArrayIterator(engine, new JsArray(engine, entries));
- }, 0);
- JintInterop.DefineMethod(engine, proto, "forEach", (t, a) =>
- {
- var arr = snapshot(t) ?? new JsArray(engine, System.Array.Empty());
- if (a.Length == 0 || !a[0].IsCallable())
- {
- return JsValue.Undefined;
- }
-
- var cb = a[0];
- var thisArg = a.Length > 1 ? a[1] : JsValue.Undefined;
- for (uint i = 0; i < arr.Length; i++)
- {
- cb.Call(thisArg, new[] { arr[(int)i], JintInterop.Num(i), t });
- }
-
- return JsValue.Undefined;
- }, 1);
- }
-
- // Define `values` + `[Symbol.iterator]` (both yield the items), shared by all
- // three collection prototypes.
- private static void DefineIterator(global::Jint.Engine engine, ObjectInstance proto, Func snapshot)
- {
- JsValue Values(JsValue t, JsValue[] _)
- => ArrayIterator(engine, snapshot(t) ?? new JsArray(engine, System.Array.Empty()));
- JintInterop.DefineMethod(engine, proto, "values", Values, 0);
- var iterFn = new ClrFunction(engine, "[Symbol.iterator]", (t, args) => Values(t, args), 0, PropertyFlag.Configurable);
- proto.DefineOwnProperty(GlobalSymbolRegistry.Iterator,
- new PropertyDescriptor(iterFn, writable: true, enumerable: false, configurable: true));
- }
-
- // Build a real ES array iterator from a snapshot array (so .next()/for-of work).
- private static JsValue ArrayIterator(global::Jint.Engine engine, JsArray array)
- {
- var iterFn = array.Get(GlobalSymbolRegistry.Iterator);
- return iterFn.Call(array, System.Array.Empty());
- }
-
- // Wire an interface prototype to a constructible-illegal global ctor so
- // `coll instanceof NodeList` resolves and `NodeList.prototype` is reachable.
- private static void WireInterface(global::Jint.Engine engine, ObjectInstance proto, string name)
- {
- var ctor = new ClrFunction(engine, name,
- (_, _) => throw new JavaScriptException(engine.Intrinsics.TypeError, "Illegal constructor"), 0, PropertyFlag.Configurable);
- ctor.Set("prototype", proto);
- JintInterop.DefineDataProp(proto, "constructor", ctor, writable: true, enumerable: false, configurable: true);
- proto.DefineOwnProperty(GlobalSymbolRegistry.ToStringTag,
- new PropertyDescriptor(JintInterop.Str(name), writable: false, enumerable: false, configurable: true));
- JintInterop.DefineDataProp(engine.Global, name, ctor, writable: true, enumerable: false, configurable: true);
- }
-}
-
-/// Live NodeList (DOM §4.2.10.2): integer indices + length resolve
-/// against a snapshot function. No named properties (unlike HTMLCollection).
-internal sealed class JintNodeListObject : ObjectInstance
-{
- private readonly JintBackendContext _ctx;
- private readonly Func> _source;
-
- public JintNodeListObject(JintBackendContext ctx, ObjectInstance? prototype, Func> source)
- : base(ctx.Engine)
- {
- _ctx = ctx;
- _source = source;
- if (prototype is not null)
- {
- Prototype = prototype;
- }
- }
-
- private IReadOnlyList Items => _source();
- public int Count => Items.Count;
-
- public JsValue Item(int index)
- {
- var items = Items;
- return index >= 0 && index < items.Count ? _ctx.Wrappers.Wrap(items[index]) : JsValue.Null;
- }
-
- public JsArray ValuesArray(global::Jint.Engine engine)
- {
- var items = Items;
- var values = new JsValue[items.Count];
- for (var i = 0; i < items.Count; i++)
- {
- values[i] = _ctx.Wrappers.Wrap(items[i]);
- }
-
- return new JsArray(engine, values);
- }
-
- public override JsValue Get(JsValue property, JsValue receiver)
- {
- if (property.IsString() && CollectionIndex.TryIndex(property.AsString(), out var i))
- {
- var items = Items;
- return i < items.Count ? _ctx.Wrappers.Wrap(items[i]) : JsValue.Undefined;
- }
- return base.Get(property, receiver);
- }
-
- public override PropertyDescriptor GetOwnProperty(JsValue property)
- {
- if (property.IsString() && CollectionIndex.TryIndex(property.AsString(), out var i))
- {
- var items = Items;
- if (i < items.Count)
- {
- return new PropertyDescriptor(_ctx.Wrappers.Wrap(items[i]), writable: false, enumerable: true, configurable: true);
- }
- }
- return base.GetOwnProperty(property);
- }
-
- public override bool HasProperty(JsValue property)
- {
- if (property.IsString() && CollectionIndex.TryIndex(property.AsString(), out var i) && i < Items.Count)
- {
- return true;
- }
-
- return base.HasProperty(property);
- }
-
- public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol)
- {
- var keys = new List();
- if ((types & Types.String) != 0)
- {
- for (var i = 0; i < Items.Count; i++)
- {
- keys.Add(JintInterop.Str(i.ToString(CultureInfo.InvariantCulture)));
- }
- }
-
- keys.AddRange(base.GetOwnPropertyKeys(types));
- return keys;
- }
-}
-
-/// Live HTMLCollection (DOM §4.2.10): integer indices + supported named
-/// properties (element ids, and the name attribute of HTML-namespace
-/// elements) resolve against a snapshot function.
-internal sealed class JintHtmlCollectionObject : ObjectInstance
-{
- private readonly JintBackendContext _ctx;
- private readonly Func> _source;
-
- public JintHtmlCollectionObject(JintBackendContext ctx, ObjectInstance? prototype, Func> source)
- : base(ctx.Engine)
- {
- _ctx = ctx;
- _source = source;
- if (prototype is not null)
- {
- Prototype = prototype;
- }
- }
-
- private IReadOnlyList Items => _source();
- public int Count => Items.Count;
-
- public JsValue Item(int index)
- {
- var items = Items;
- return index >= 0 && index < items.Count ? _ctx.Wrappers.Wrap(items[index]) : JsValue.Null;
- }
-
- public JsValue NamedItemValue(string name)
- => NamedItem(name) is { } e ? _ctx.Wrappers.Wrap(e) : JsValue.Null;
-
- public JsArray ValuesArray(global::Jint.Engine engine)
- {
- var items = Items;
- var values = new JsValue[items.Count];
- for (var i = 0; i < items.Count; i++)
- {
- values[i] = _ctx.Wrappers.Wrap(items[i]);
- }
-
- return new JsArray(engine, values);
- }
-
- private Element? NamedItem(string name)
- {
- if (name.Length == 0)
- {
- return null;
- }
-
- var items = Items;
- foreach (var e in items)
- {
- if (e.GetAttribute("id") == name)
- {
- return e;
- }
- }
-
- foreach (var e in items)
- {
- if (e.Namespace == Element.HtmlNamespace && e.GetAttribute("name") == name)
- {
- return e;
- }
- }
-
- return null;
- }
-
- // A named property is shadowed by an own expando or a prototype/built-in
- // (item/namedItem/length/…), per WebIDL named-property visibility.
- private bool IsShadowed(JsValue name)
- {
- if (base.GetOwnProperty(name) != PropertyDescriptor.Undefined)
- {
- return true;
- }
-
- for (var p = Prototype; p is not null; p = p.Prototype)
- {
- if (p.GetOwnProperty(name) != PropertyDescriptor.Undefined)
- {
- return true;
- }
- }
-
- return false;
- }
-
- public override JsValue Get(JsValue property, JsValue receiver)
- {
- if (property.IsString())
- {
- var name = property.AsString();
- if (CollectionIndex.TryIndex(name, out var i))
- {
- var items = Items;
- return i < items.Count ? _ctx.Wrappers.Wrap(items[i]) : JsValue.Undefined;
- }
- if (!IsShadowed(property) && NamedItem(name) is { } named)
- {
- return _ctx.Wrappers.Wrap(named);
- }
- }
- return base.Get(property, receiver);
- }
-
- public override PropertyDescriptor GetOwnProperty(JsValue property)
- {
- if (property.IsString())
- {
- var name = property.AsString();
- if (CollectionIndex.TryIndex(name, out var i))
- {
- var items = Items;
- if (i < items.Count)
- {
- return new PropertyDescriptor(_ctx.Wrappers.Wrap(items[i]), writable: false, enumerable: true, configurable: true);
- }
- }
- else if (base.GetOwnProperty(property) == PropertyDescriptor.Undefined && NamedItem(name) is { } named)
- {
- return new PropertyDescriptor(_ctx.Wrappers.Wrap(named), writable: false, enumerable: true, configurable: true);
- }
- }
- return base.GetOwnProperty(property);
- }
-
- public override bool HasProperty(JsValue property)
- {
- if (property.IsString())
- {
- var name = property.AsString();
- if (CollectionIndex.TryIndex(name, out var i))
- {
- return i < Items.Count;
- }
-
- if (!IsShadowed(property) && NamedItem(name) is not null)
- {
- return true;
- }
- }
- return base.HasProperty(property);
- }
-
- public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol)
- {
- var keys = new List();
- if ((types & Types.String) != 0)
- {
- for (var i = 0; i < Items.Count; i++)
- {
- keys.Add(JintInterop.Str(i.ToString(CultureInfo.InvariantCulture)));
- }
- }
-
- keys.AddRange(base.GetOwnPropertyKeys(types));
- return keys;
- }
-}
-
-/// Live DOMTokenList backing element.classList (DOM §7.1):
-/// integer indices resolve to tokens against the element's live class list. The
-/// instance carries the token-mutating methods (add/remove/toggle/…); the
-/// iteration surface (values/keys/entries/forEach/@@iterator) is inherited from
-/// %DOMTokenListPrototype%.
-internal sealed class JintDomTokenListObject : ObjectInstance
-{
- private readonly DomTokenList _tokens;
-
- public JintDomTokenListObject(JintBackendContext ctx, DomTokenList tokens) : base(ctx.Engine)
- {
- _tokens = tokens;
- if (ctx.Wrappers.DomTokenListPrototype is { } p)
- {
- Prototype = p;
- }
- }
-
- public JsArray ValuesArray(global::Jint.Engine engine)
- {
- var values = new JsValue[_tokens.Count];
- for (var i = 0; i < _tokens.Count; i++)
- {
- values[i] = JintInterop.Str(_tokens[i]);
- }
-
- return new JsArray(engine, values);
- }
-
- public override JsValue Get(JsValue property, JsValue receiver)
- {
- if (property.IsString() && CollectionIndex.TryIndex(property.AsString(), out var i))
- {
- return i < _tokens.Count ? JintInterop.Str(_tokens[i]) : JsValue.Undefined;
- }
-
- return base.Get(property, receiver);
- }
-
- public override PropertyDescriptor GetOwnProperty(JsValue property)
- {
- if (property.IsString() && CollectionIndex.TryIndex(property.AsString(), out var i) && i < _tokens.Count)
- {
- return new PropertyDescriptor(JintInterop.Str(_tokens[i]), writable: false, enumerable: true, configurable: true);
- }
-
- return base.GetOwnProperty(property);
- }
-
- public override bool HasProperty(JsValue property)
- {
- if (property.IsString() && CollectionIndex.TryIndex(property.AsString(), out var i) && i < _tokens.Count)
- {
- return true;
- }
-
- return base.HasProperty(property);
- }
-
- public override List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol)
- {
- var keys = new List();
- if ((types & Types.String) != 0)
- {
- for (var i = 0; i < _tokens.Count; i++)
- {
- keys.Add(JintInterop.Str(i.ToString(CultureInfo.InvariantCulture)));
- }
- }
-
- keys.AddRange(base.GetOwnPropertyKeys(types));
- return keys;
- }
-}
-
-/// WebIDL "array index" parsing shared by the collection objects: a
-/// canonical non-negative integer string in [0, 2^32-2], no leading zeros.
-internal static class CollectionIndex
-{
- public static bool TryIndex(string name, out int index)
- {
- index = 0;
- if (name.Length == 0)
- {
- return false;
- }
-
- if (name.Length > 1 && name[0] == '0')
- {
- return false;
- }
-
- if (!ulong.TryParse(name, NumberStyles.None, CultureInfo.InvariantCulture, out var v))
- {
- return false;
- }
-
- if (v > 4294967294UL)
- {
- return false;
- }
-
- index = v > int.MaxValue ? int.MaxValue : (int)v;
- return true;
- }
-}
diff --git a/src/Starling.Bindings.Jint/ConsoleBinding.cs b/src/Starling.Bindings.Jint/ConsoleBinding.cs
deleted file mode 100644
index bd271db4..00000000
--- a/src/Starling.Bindings.Jint/ConsoleBinding.cs
+++ /dev/null
@@ -1,173 +0,0 @@
-using System.Diagnostics;
-using System.Globalization;
-using Jint;
-using Jint.Native;
-using Jint.Runtime;
-using Microsoft.Extensions.Logging;
-using Starling.Js.Hosting;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// The full console surface for the Jint backend. Beyond log/info/warn/
-/// error/debug/trace/dir/table, this adds the methods real sites call that used to
-/// throw "not a function": time/timeEnd/timeLog,
-/// count/countReset, group/groupCollapsed/groupEnd,
-/// assert, and clear. Output is routed through a sink so the live
-/// session and the bare unit-test context share one implementation.
-///
-internal static class ConsoleBinding
-{
- /// Install a console for a bare context (parity tests / no session),
- /// routing output to the context logger. Idempotent — skips if a console is
- /// already installed (e.g. by the session before InstallAll).
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- if (ctx.Engine.Global.HasOwnProperty("console"))
- {
- return;
- }
-
- var log = ctx.Log;
- Install(ctx.Engine, (level, msg) =>
- {
- switch (level)
- {
- case ConsoleLevel.Error: log.LogError("{Message}", msg); break;
- case ConsoleLevel.Warn: log.LogWarning("{Message}", msg); break;
- default: log.LogInformation("{Message}", msg); break;
- }
- });
- }
-
- /// Build and install the full console on ,
- /// writing through . Overwrites any existing console.
- public static void Install(global::Jint.Engine engine, Action sink)
- {
- ArgumentNullException.ThrowIfNull(engine);
- ArgumentNullException.ThrowIfNull(sink);
-
- var console = new JsObject(engine);
- var counts = new Dictionary(StringComparer.Ordinal);
- var timers = new Dictionary(StringComparer.Ordinal);
- var groupDepth = 0;
-
- string Indent(string s) => groupDepth > 0 ? new string(' ', groupDepth * 2) + s : s;
-
- void Method(string name, ConsoleLevel level)
- => JintInterop.DefineMethod(engine, console, name, (_, args) =>
- {
- sink(level, Indent(Format(args)));
- return JsValue.Undefined;
- }, 0);
-
- Method("log", ConsoleLevel.Log);
- Method("info", ConsoleLevel.Info);
- Method("warn", ConsoleLevel.Warn);
- Method("error", ConsoleLevel.Error);
- Method("debug", ConsoleLevel.Debug);
- Method("trace", ConsoleLevel.Trace);
- Method("dir", ConsoleLevel.Dir);
- Method("table", ConsoleLevel.Table);
-
- JintInterop.DefineMethod(engine, console, "assert", (_, args) =>
- {
- var cond = args.Length > 0 && TypeConverter.ToBoolean(args[0]);
- if (!cond)
- {
- var rest = args.Length > 1 ? args[1..] : System.Array.Empty();
- var msg = rest.Length > 0 ? "Assertion failed: " + Format(rest) : "Assertion failed";
- sink(ConsoleLevel.Error, Indent(msg));
- }
- return JsValue.Undefined;
- }, 0);
-
- JintInterop.DefineMethod(engine, console, "count", (_, args) =>
- {
- var label = Label(args, "default");
- counts.TryGetValue(label, out var n);
- counts[label] = ++n;
- sink(ConsoleLevel.Info, Indent($"{label}: {n}"));
- return JsValue.Undefined;
- }, 0);
- JintInterop.DefineMethod(engine, console, "countReset", (_, args) =>
- {
- counts.Remove(Label(args, "default"));
- return JsValue.Undefined;
- }, 0);
-
- JintInterop.DefineMethod(engine, console, "time", (_, args) =>
- {
- timers[Label(args, "default")] = Stopwatch.StartNew();
- return JsValue.Undefined;
- }, 0);
- JintInterop.DefineMethod(engine, console, "timeLog", (_, args) =>
- {
- var label = Label(args, "default");
- if (timers.TryGetValue(label, out var sw))
- {
- sink(ConsoleLevel.Info, Indent($"{label}: {sw.Elapsed.TotalMilliseconds.ToString("0.###", CultureInfo.InvariantCulture)}ms"));
- }
-
- return JsValue.Undefined;
- }, 0);
- JintInterop.DefineMethod(engine, console, "timeEnd", (_, args) =>
- {
- var label = Label(args, "default");
- if (timers.Remove(label, out var sw))
- {
- sink(ConsoleLevel.Info, Indent($"{label}: {sw.Elapsed.TotalMilliseconds.ToString("0.###", CultureInfo.InvariantCulture)}ms"));
- }
-
- return JsValue.Undefined;
- }, 0);
-
- JintInterop.DefineMethod(engine, console, "group", (_, args) =>
- {
- if (args.Length > 0)
- {
- sink(ConsoleLevel.Log, Indent(Format(args)));
- }
-
- groupDepth++;
- return JsValue.Undefined;
- }, 0);
- JintInterop.DefineMethod(engine, console, "groupCollapsed", (_, args) =>
- {
- if (args.Length > 0)
- {
- sink(ConsoleLevel.Log, Indent(Format(args)));
- }
-
- groupDepth++;
- return JsValue.Undefined;
- }, 0);
- JintInterop.DefineMethod(engine, console, "groupEnd", (_, _) =>
- {
- if (groupDepth > 0)
- {
- groupDepth--;
- }
-
- return JsValue.Undefined;
- }, 0);
-
- JintInterop.DefineMethod(engine, console, "clear", (_, _) => JsValue.Undefined, 0);
-
- JintInterop.DefineDataProp(engine.Global, "console", console, writable: true, enumerable: false, configurable: true);
- }
-
- private static string Label(JsValue[] args, string fallback)
- => args.Length > 0 && !args[0].IsUndefined() ? TypeConverter.ToString(args[0]) : fallback;
-
- private static string Format(JsValue[] args)
- {
- if (args.Length == 0)
- {
- return string.Empty;
- }
-
- return string.Join(" ", args.Select(a => a.IsNull() ? "null" : a.ToString()));
- }
-}
diff --git a/src/Starling.Bindings.Jint/CookieBinding.cs b/src/Starling.Bindings.Jint/CookieBinding.cs
deleted file mode 100644
index 9b3cc81c..00000000
--- a/src/Starling.Bindings.Jint/CookieBinding.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using Jint.Native;
-using Microsoft.Extensions.Logging;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// HTML §6.7.3 document.cookie on the Jint backend.
-///
-///
-/// does not yet expose a
-/// CookieJar to bindings. Cookies live in StarlingHttpClient for now.
-/// This binding therefore installs a graceful no-op accessor: the getter
-/// returns ""; the setter logs a debug diagnostic and discards the
-/// value. When a session-scoped CookieJar lands, this is the only file to
-/// teach about it.
-///
-internal static class CookieBinding
-{
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- var engine = ctx.Engine;
- var log = ctx.LoggerFactory.CreateLogger(typeof(CookieBinding));
- var documentProto = ctx.Wrappers.DocumentPrototype;
- if (documentProto is null)
- {
- // NodeBindings has not installed a Document prototype slot.
- // Without it we have nowhere idempotent to attach the accessor.
- CookieBindingLog.DocumentPrototypeNull(log);
- return;
- }
-
- if (documentProto.HasOwnProperty("cookie"))
- {
- return;
- }
-
- JintInterop.DefineAccessor(engine, documentProto, "cookie",
- (_, _) => JintInterop.Str(ctx.Cookies.BuildCookieHeader(ctx.BaseUrl)),
- (_, args) =>
- {
- var raw = args.Length > 0 ? args[0].ToString() : "";
- if (!string.IsNullOrEmpty(raw))
- {
- ctx.Cookies.StoreFromHeaders(ctx.BaseUrl, new[] { raw });
- }
-
- return JsValue.Undefined;
- });
- }
-}
-
-internal static partial class CookieBindingLog
-{
- [LoggerMessage(Level = LogLevel.Debug,
- Message = "DocumentPrototype is null; document.cookie accessor not installed.")]
- public static partial void DocumentPrototypeNull(ILogger logger);
-}
diff --git a/src/Starling.Bindings.Jint/CoreWebApiBinding.cs b/src/Starling.Bindings.Jint/CoreWebApiBinding.cs
deleted file mode 100644
index d19334db..00000000
--- a/src/Starling.Bindings.Jint/CoreWebApiBinding.cs
+++ /dev/null
@@ -1,218 +0,0 @@
-using System.Text;
-using Jint;
-using Jint.Native;
-using Jint.Native.Object;
-using Jint.Runtime;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// Core Web-API globals for the Jint backend that the canonical
-/// Starling.Bindings/CoreWebApiBinding.cs exposes but Jint lacked:
-/// btoa/atob (HTML §forgiving-base64) and structuredClone
-/// (HTML structured-clone for the common JS value graph).
-///
-internal static class CoreWebApiBinding
-{
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- var engine = ctx.Engine;
-
- if (!engine.Global.HasOwnProperty("btoa"))
- {
- JintInterop.DefineMethod(engine, engine.Global, "btoa", (_, a) =>
- {
- var s = a.Length > 0 ? TypeConverter.ToString(a[0]) : "";
- var bytes = new byte[s.Length];
- for (var i = 0; i < s.Length; i++)
- {
- if (s[i] > 0xFF)
- {
- throw DomExceptionBinding.Throw(ctx, "InvalidCharacterError", "String contains an invalid character");
- }
-
- bytes[i] = (byte)s[i];
- }
- return JintInterop.Str(Convert.ToBase64String(bytes));
- }, 1);
- }
-
- if (!engine.Global.HasOwnProperty("atob"))
- {
- JintInterop.DefineMethod(engine, engine.Global, "atob", (_, a) =>
- {
- var s = RemoveAsciiWhitespace(a.Length > 0 ? TypeConverter.ToString(a[0]) : "");
- if (s.Length % 4 == 1)
- {
- throw DomExceptionBinding.Throw(ctx, "InvalidCharacterError", "The string to be decoded is not correctly encoded");
- }
-
- if (s.Length % 4 != 0)
- {
- s = s.PadRight(s.Length + (4 - s.Length % 4), '=');
- }
-
- try
- {
- var bytes = Convert.FromBase64String(s);
- return JintInterop.Str(string.Create(bytes.Length, bytes, static (span, st) =>
- {
- for (var i = 0; i < st.Length; i++)
- {
- span[i] = (char)st[i];
- }
- }));
- }
- catch (FormatException)
- {
- throw DomExceptionBinding.Throw(ctx, "InvalidCharacterError", "The string to be decoded is not correctly encoded");
- }
- }, 1);
- }
-
- if (!engine.Global.HasOwnProperty("structuredClone"))
- {
- JintInterop.DefineMethod(engine, engine.Global, "structuredClone", (_, a) =>
- {
- var seen = new Dictionary(ReferenceEqualityComparer.Instance);
- return CloneValue(ctx, a.Length > 0 ? a[0] : JsValue.Undefined, seen);
- }, 1);
- }
- }
-
- private static JsValue CloneValue(JintBackendContext ctx, JsValue value, Dictionary seen)
- {
- var engine = ctx.Engine;
- if (value is not ObjectInstance obj)
- {
- if (value is JsSymbol)
- {
- throw DomExceptionBinding.Throw(ctx, "DataCloneError", "Symbol values cannot be cloned");
- }
-
- return value;
- }
- if (seen.TryGetValue(obj, out var existing))
- {
- return existing;
- }
-
- if (value.IsArrayBuffer() && value.AsArrayBuffer() is { } ab)
- {
- return engine.Intrinsics.ArrayBuffer.Construct((byte[])ab.Clone());
- }
-
- if (value is JsTypedArray ta)
- {
- return CloneTypedArray(ctx, ta);
- }
-
- if (value.IsCallable())
- {
- throw DomExceptionBinding.Throw(ctx, "DataCloneError", "Function objects cannot be cloned");
- }
-
- if (value is JsArray arr)
- {
- var c = new JsArray(engine, (uint)arr.Length);
- seen[obj] = c;
- for (uint i = 0; i < arr.Length; i++)
- {
- c[(int)i] = CloneValue(ctx, arr[(int)i], seen);
- }
-
- return c;
- }
-
- var clone = new JsObject(engine);
- seen[obj] = clone;
- foreach (var key in EnumerableStringKeys(obj))
- {
- clone.FastSetProperty(key, new global::Jint.Runtime.Descriptors.PropertyDescriptor(
- CloneValue(ctx, obj.Get(key), seen), writable: true, enumerable: true, configurable: true));
- }
-
- return clone;
- }
-
- private static ObjectInstance CloneTypedArray(JintBackendContext ctx, JsTypedArray ta)
- {
- var engine = ctx.Engine;
- // Copy the underlying bytes (honoring offset/length), rebuild the same kind.
- var bytes = ExtractBytes(ta);
- var buffer = engine.Intrinsics.ArrayBuffer.Construct(bytes);
- // Rebuild the same typed-array kind over the copied buffer.
- return engine.Construct(TypedArrayName(ta), buffer);
- }
-
- private static byte[] ExtractBytes(JsValue v)
- {
- if (v.IsArrayBuffer() && v.AsArrayBuffer() is { } ab)
- {
- return (byte[])ab.Clone();
- }
-
- if (v is ObjectInstance oi)
- {
- var bufVal = oi.Get("buffer");
- if (bufVal.IsArrayBuffer() && bufVal.AsArrayBuffer() is { } backing)
- {
- var offset = oi.Get("byteOffset").IsNumber() ? (int)oi.Get("byteOffset").AsNumber() : 0;
- var length = oi.Get("byteLength").IsNumber() ? (int)oi.Get("byteLength").AsNumber() : backing.Length;
- if (offset >= 0 && length >= 0 && offset + length <= backing.Length)
- {
- var slice = new byte[length];
- Array.Copy(backing, offset, slice, 0, length);
- return slice;
- }
- }
- }
- return Array.Empty();
- }
-
- private static string TypedArrayName(JsTypedArray ta)
- {
- var ctor = ta.Get("constructor");
- if (ctor is ObjectInstance oi)
- {
- var name = oi.Get("name");
- if (name.IsString())
- {
- return name.ToString();
- }
- }
- return "Uint8Array";
- }
-
- private static IEnumerable EnumerableStringKeys(ObjectInstance o)
- {
- foreach (var key in o.GetOwnPropertyKeys(Types.String))
- {
- if (!key.IsString())
- {
- continue;
- }
-
- var d = o.GetOwnProperty(key);
- if (d != global::Jint.Runtime.Descriptors.PropertyDescriptor.Undefined && d.Enumerable)
- {
- yield return key.AsString();
- }
- }
- }
-
- private static string RemoveAsciiWhitespace(string value)
- {
- var sb = new StringBuilder(value.Length);
- foreach (var ch in value)
- {
- if (ch is not (' ' or '\t' or '\n' or '\r' or '\f'))
- {
- sb.Append(ch);
- }
- }
-
- return sb.ToString();
- }
-}
diff --git a/src/Starling.Bindings.Jint/CryptoBinding.cs b/src/Starling.Bindings.Jint/CryptoBinding.cs
deleted file mode 100644
index f077888e..00000000
--- a/src/Starling.Bindings.Jint/CryptoBinding.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using System.Globalization;
-using System.Security.Cryptography;
-using Jint;
-using Jint.Native;
-using Jint.Native.Object;
-using Jint.Runtime;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// Web Crypto API §10.1 on the Jint backend.
-/// Mirrors Starling.Bindings/CryptoBinding.cs.
-///
-///
-/// Surface: crypto.getRandomValues(typedArray) (integer typed arrays
-/// only, ≤ 65536 bytes) and crypto.randomUUID() (RFC 4122 v4).
-/// SubtleCrypto / Web Crypto §11 is not implemented — its async primitives
-/// don't have a designed seam yet on either backend.
-///
-internal static class CryptoBinding
-{
- private const uint MaxBytes = 65536;
-
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- var engine = ctx.Engine;
-
- var crypto = new JsObject(engine);
- JintInterop.DefineMethod(engine, crypto, "getRandomValues",
- (_, args) => GetRandomValues(engine, args), length: 1);
- JintInterop.DefineMethod(engine, crypto, "randomUUID",
- (_, _) => JintInterop.Str(GenerateRandomUuid()), length: 0);
-
- JintInterop.DefineDataProp(engine.Global, "crypto", crypto,
- writable: true, enumerable: true, configurable: true);
- }
-
- private static JsTypedArray GetRandomValues(Engine engine, JsValue[] args)
- {
- if (args.Length == 0 || args[0] is not JsTypedArray ta)
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError,
- "getRandomValues requires a TypedArray argument");
- }
-
- var ctorName = TypedArrayConstructorName(ta);
- if (ctorName.StartsWith("Float", StringComparison.Ordinal))
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError,
- $"getRandomValues: {ctorName} is not an integer typed array");
- }
-
- if (ctorName.StartsWith("BigInt", StringComparison.Ordinal) ||
- ctorName.StartsWith("BigUint", StringComparison.Ordinal))
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError,
- $"getRandomValues: {ctorName} is not supported");
- }
-
- var byteLengthVal = ta.Get("byteLength");
- var byteLength = byteLengthVal.IsNumber()
- ? (uint)TypeConverter.ToNumber(byteLengthVal)
- : 0u;
- if (byteLength > MaxBytes)
- {
- throw new JavaScriptException(engine.Intrinsics.TypeError,
- $"getRandomValues: byte length {byteLength} exceeds the {MaxBytes}-byte quota");
- }
-
- // Fill via the typed array's own set semantics: generate a 32-bit
- // random value per element; Jint coerces to the array's element width.
- Span scratch = stackalloc byte[4];
- var len = ta.Length;
- for (uint i = 0; i < len; i++)
- {
- RandomNumberGenerator.Fill(scratch);
- var u = (uint)(scratch[0] | (scratch[1] << 8) | (scratch[2] << 16) | (scratch[3] << 24));
- ta[i] = JsNumber.Create(u);
- }
- return ta;
- }
-
- private static string TypedArrayConstructorName(JsTypedArray ta)
- {
- var ctor = ta.Get("constructor");
- if (ctor is ObjectInstance oi)
- {
- var name = oi.Get("name");
- if (name.IsString())
- {
- return name.ToString();
- }
- }
- return "TypedArray";
- }
-
- private static string GenerateRandomUuid()
- {
- Span bytes = stackalloc byte[16];
- RandomNumberGenerator.Fill(bytes);
- bytes[6] = (byte)((bytes[6] & 0x0F) | 0x40);
- bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);
- return $"{Hex(bytes[0])}{Hex(bytes[1])}{Hex(bytes[2])}{Hex(bytes[3])}-" +
- $"{Hex(bytes[4])}{Hex(bytes[5])}-" +
- $"{Hex(bytes[6])}{Hex(bytes[7])}-" +
- $"{Hex(bytes[8])}{Hex(bytes[9])}-" +
- $"{Hex(bytes[10])}{Hex(bytes[11])}{Hex(bytes[12])}{Hex(bytes[13])}{Hex(bytes[14])}{Hex(bytes[15])}";
- }
-
- private static string Hex(byte b) => b.ToString("x2", CultureInfo.InvariantCulture);
-}
diff --git a/src/Starling.Bindings.Jint/CssBinding.cs b/src/Starling.Bindings.Jint/CssBinding.cs
deleted file mode 100644
index a1fdc3b0..00000000
--- a/src/Starling.Bindings.Jint/CssBinding.cs
+++ /dev/null
@@ -1,173 +0,0 @@
-using Jint;
-using Jint.Native;
-using Jint.Native.Object;
-using Starling.Css.TypedOm;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// The window.CSS namespace + CSSStyleValue global. Exposes the
-/// Starling CSS Typed OM value model (CSS Typed OM 1) and the @property
-/// registration API (CSS Properties and Values API 1) to scripts. This is pure
-/// model exposure: the numeric factories and
-/// reuse Starling.Css.TypedOm, and registerProperty reuses the
-/// same descriptor-validity rules as the @property at-rule parser.
-///
-internal static class CssBinding
-{
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- var engine = ctx.Engine;
- var global = engine.Global;
-
- var css = new JsObject(engine);
-
- // CSS Typed OM 1 §4.1 — numeric value factories. Each returns a
- // CSSUnitValue-shaped object {value, unit, toString()}.
- DefineUnitFactory(engine, css, "number", "number");
- DefineUnitFactory(engine, css, "px", "px");
- DefineUnitFactory(engine, css, "percent", "%");
- DefineUnitFactory(engine, css, "em", "em");
- DefineUnitFactory(engine, css, "rem", "rem");
- DefineUnitFactory(engine, css, "vw", "vw");
- DefineUnitFactory(engine, css, "vh", "vh");
- DefineUnitFactory(engine, css, "deg", "deg");
- DefineUnitFactory(engine, css, "s", "s");
- DefineUnitFactory(engine, css, "ms", "ms");
-
- // CSSOM §2.1 — CSS.escape(ident).
- JintInterop.DefineMethod(engine, css, "escape", (_, args) =>
- JintInterop.Str(CssEscape(args.Length > 0 ? args[0].ToString() : string.Empty)), 1);
-
- // CSS Properties and Values API 1 §3 — CSS.registerProperty(definition).
- // Validates the descriptor and rejects duplicates; a per-document set
- // tracks names registered in this session.
- var registered = new HashSet(StringComparer.Ordinal);
- JintInterop.DefineMethod(engine, css, "registerProperty", (_, args) =>
- {
- if (args.Length < 1 || args[0] is not ObjectInstance def)
- {
- throw TypeErr(engine, "registerProperty requires a descriptor object");
- }
-
- var name = GetString(def, "name");
- var syntax = GetString(def, "syntax") ?? "*";
- var inheritsVal = def.Get("inherits");
- var initial = def.HasProperty("initialValue") ? GetString(def, "initialValue") : null;
-
- if (name is null || !name.StartsWith("--", StringComparison.Ordinal))
- {
- throw TypeErr(engine, "@property name must start with --");
- }
-
- if (inheritsVal.IsUndefined())
- {
- throw TypeErr(engine, "registerProperty requires an 'inherits' flag");
- }
-
- var isUniversal = syntax.Trim() == "*";
- if (!isUniversal && string.IsNullOrEmpty(initial))
- {
- throw TypeErr(engine, "initialValue is required for a non-universal syntax");
- }
-
- if (!registered.Add(name))
- {
- throw TypeErr(engine, $"property {name} is already registered");
- }
-
- // Descriptor is valid (validity rules mirror the @property at-rule model).
- return JsValue.Undefined;
- }, 1);
-
- // CSSOM §6 — CSS[Symbol.toStringTag] === "CSS".
- css.DefineOwnProperty(global::Jint.Native.Symbol.GlobalSymbolRegistry.ToStringTag,
- new global::Jint.Runtime.Descriptors.PropertyDescriptor(JintInterop.Str("CSS"),
- writable: false, enumerable: false, configurable: true));
-
- JintInterop.DefineDataProp(global, "CSS", css, writable: true, enumerable: false, configurable: true);
-
- // CSS Typed OM 1 §3.2 — CSSStyleValue.parse(property, cssText).
- var styleValue = new JsObject(engine);
- JintInterop.DefineMethod(engine, styleValue, "parse", (_, args) =>
- {
- var prop = args.Length > 0 ? args[0].ToString() : string.Empty;
- var text = args.Length > 1 ? args[1].ToString() : string.Empty;
- return BuildStyleValue(engine, CssStyleValue.Parse(prop, text));
- }, 2);
- JintInterop.DefineDataProp(global, "CSSStyleValue", styleValue, writable: true, enumerable: false, configurable: true);
- }
-
- private static void DefineUnitFactory(Engine engine, JsObject css, string name, string unit)
- => JintInterop.DefineMethod(engine, css, name, (_, args) =>
- BuildUnitValue(engine, args.Length > 0 ? args[0].AsNumber() : 0, unit), 1);
-
- private static JsObject BuildStyleValue(Engine engine, CssStyleValue value) => value switch
- {
- CssUnitValue u => BuildUnitValue(engine, u.Value, u.Unit),
- CssKeywordValue k => BuildKeywordValue(engine, k.Value),
- _ => BuildUnparsed(engine, value.ToString()),
- };
-
- private static JsObject BuildUnitValue(Engine engine, double value, string unit)
- {
- var o = new JsObject(engine);
- JintInterop.DefineDataProp(o, "value", JintInterop.Num(value));
- JintInterop.DefineDataProp(o, "unit", JintInterop.Str(unit));
- JintInterop.DefineMethod(engine, o, "toString", (_, _) =>
- JintInterop.Str(new CssUnitValue(value, unit).ToString()), 0);
- return o;
- }
-
- private static JsObject BuildKeywordValue(Engine engine, string keyword)
- {
- var o = new JsObject(engine);
- JintInterop.DefineDataProp(o, "value", JintInterop.Str(keyword));
- JintInterop.DefineMethod(engine, o, "toString", (_, _) => JintInterop.Str(keyword), 0);
- return o;
- }
-
- private static JsObject BuildUnparsed(Engine engine, string raw)
- {
- var o = new JsObject(engine);
- JintInterop.DefineMethod(engine, o, "toString", (_, _) => JintInterop.Str(raw), 0);
- return o;
- }
-
- private static string? GetString(ObjectInstance obj, string name)
- {
- var v = obj.Get(name);
- return v.IsUndefined() || v.IsNull() ? null : v.ToString();
- }
-
- private static global::Jint.Runtime.JavaScriptException TypeErr(Engine engine, string message)
- => new(engine.Intrinsics.TypeError, message);
-
- // CSSOM §2.1 serialize-an-identifier (the subset needed for CSS.escape):
- // escapes the NULL replacement, control/0x7F, leading digit, and any
- // non-ident code point with a backslash.
- private static string CssEscape(string s)
- {
- var sb = new System.Text.StringBuilder(s.Length);
- for (var i = 0; i < s.Length; i++)
- {
- var c = s[i];
- if (c == '\0') { sb.Append('�'); continue; }
- if ((c <= 0x1F) || c == 0x7F || (i == 0 && char.IsAsciiDigit(c)))
- {
- sb.Append('\\').Append(((int)c).ToString("x", System.Globalization.CultureInfo.InvariantCulture)).Append(' ');
- continue;
- }
- if (char.IsAsciiLetterOrDigit(c) || c == '-' || c == '_' || c > 0x7F)
- {
- sb.Append(c);
- }
- else
- {
- sb.Append('\\').Append(c);
- }
- }
- return sb.ToString();
- }
-}
diff --git a/src/Starling.Bindings.Jint/CssomBinding.cs b/src/Starling.Bindings.Jint/CssomBinding.cs
deleted file mode 100644
index 9bff8ea7..00000000
--- a/src/Starling.Bindings.Jint/CssomBinding.cs
+++ /dev/null
@@ -1,263 +0,0 @@
-using System.Globalization;
-using System.Runtime.CompilerServices;
-using Jint;
-using Jint.Native;
-using Jint.Runtime;
-using Jint.Runtime.Descriptors;
-using Starling.Css;
-using Starling.Css.Cssom;
-using Starling.Css.Parser;
-using Starling.Dom;
-
-namespace Starling.Bindings.Jint;
-
-///
-/// CSSOM host objects for the Jint backend (CSSOM §6), mirroring
-/// Starling.Bindings/CssomBinding.cs: document.styleSheets →
-/// StyleSheetList → CSSStyleSheet → cssRules → CSSStyleRule
-/// (selectorText/style/cssText), plus element.sheet on
-/// <style> elements. Backed by a live per
-/// element, cached per document so CSSOM edits round-trip.
-///
-internal static class CssomBinding
-{
- private static readonly ConditionalWeakTable> SheetsPerDocument = new();
-
- private sealed class StyleElementSheet { public string Source = ""; public CssomStyleSheet? Sheet; }
- private static readonly ConditionalWeakTable SheetPerStyleElement = new();
-
- public static void Install(JintBackendContext ctx)
- {
- ArgumentNullException.ThrowIfNull(ctx);
- var engine = ctx.Engine;
- var docProto = ctx.Wrappers.DocumentPrototype;
- var elProto = ctx.Wrappers.ElementPrototype;
- if (docProto is null || elProto is null)
- {
- return;
- }
-
- // document.styleSheets
- JintInterop.DefineAccessor(engine, docProto, "styleSheets", (t, _) =>
- {
- var doc = ctx.Wrappers.UnwrapDocument(t) ?? ctx.Document;
- return BuildStyleSheetList(ctx, GetOrBuildSheets(doc));
- });
-
- // element.sheet (CSSOM §6.5) — only " +
- "x