diff --git a/.editorconfig b/.editorconfig index 61905922..bd2130b7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,7 @@ trim_trailing_whitespace = true # Strong style — fail the build, don't lint forever dotnet_diagnostic.IDE0005.severity = error # remove unused usings +dotnet_diagnostic.IDE0011.severity = error # always use braces dotnet_diagnostic.CA1825.severity = error # avoid zero-length array allocations dotnet_diagnostic.CA1859.severity = error # concrete types where possible dotnet_diagnostic.CA1869.severity = error # cache JsonSerializerOptions @@ -22,6 +23,7 @@ dotnet_diagnostic.IDE0073.severity = none csharp_style_namespace_declarations = file_scoped:error csharp_style_prefer_primary_constructors = true:suggestion csharp_prefer_static_anonymous_function = true:warning +csharp_prefer_braces = true:error # always use braces (drives IDE0011) [*.{xml,csproj,props,targets}] indent_size = 2 diff --git a/bench/Starling.Bench/AnimationBench.cs b/bench/Starling.Bench/AnimationBench.cs index b82e03ea..c44a9009 100644 --- a/bench/Starling.Bench/AnimationBench.cs +++ b/bench/Starling.Bench/AnimationBench.cs @@ -113,9 +113,16 @@ public int SampleOnly() engine.Tick(_clock); var samples = 0; foreach (var el in engine.ActiveElements) + { foreach (var prop in engine.ActiveProperties(el)) + { if (engine.GetEffective(el, prop) is not null) + { samples++; + } + } + } + return samples; } } diff --git a/bench/Starling.Bench/AnimationTraceProgram.cs b/bench/Starling.Bench/AnimationTraceProgram.cs index 78da5539..b69c3039 100644 --- a/bench/Starling.Bench/AnimationTraceProgram.cs +++ b/bench/Starling.Bench/AnimationTraceProgram.cs @@ -76,7 +76,11 @@ public static int Run(string[] args) Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, ActivityStopped = a => { - if (a.OperationName != "paint.raster.command_record") return; + if (a.OperationName != "paint.raster.command_record") + { + return; + } + frameData.LastReused = TagInt(a, "raster.text.shaped_reused"); frameData.LastRebuilt = TagInt(a, "raster.text.shaped_rebuilt"); frameData.LastChars = TagInt(a, "raster.text.chars"); @@ -109,7 +113,11 @@ public static int Run(string[] args) frameData.Reset(); using (backend.Render(list, viewport, scale)) { } - if (f < warmup) continue; + if (f < warmup) + { + continue; + } + sumReused += frameData.LastReused; sumRebuilt += frameData.LastRebuilt; sumChars += frameData.LastChars; @@ -139,9 +147,14 @@ public static int Run(string[] args) Console.WriteLine($"raster.time.draw_text_ms mean/frame: {sumDrawMs / n:F3} font_create_ms mean/frame: {sumFontMs / n:F3}"); Console.WriteLine(); if (meanRebuilt > meanReused) + { Console.WriteLine("VERDICT: shaped_rebuilt dominates — text IS re-shaped at paint time every frame (heavy path confirmed)."); + } else + { Console.WriteLine("VERDICT: shaped_reused dominates — paint-time shaping is cached; the per-frame cost lives in relayout/raster, not reshape."); + } + return 0; } diff --git a/bench/Starling.Bench/Fixtures.cs b/bench/Starling.Bench/Fixtures.cs index 31265033..b8492eb8 100644 --- a/bench/Starling.Bench/Fixtures.cs +++ b/bench/Starling.Bench/Fixtures.cs @@ -17,8 +17,10 @@ public static bool GitHubSnapshotExists public static void RequireGitHubSnapshot() { if (!GitHubSnapshotExists) + { throw new InvalidOperationException( "GitHub local snapshot missing. Run tools/snapshot-vendor/vendor-github-home.sh first."); + } } private static string LocateRepoRoot() @@ -27,9 +29,15 @@ private static string LocateRepoRoot() while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "Starling.slnx")) && !File.Exists(Path.Combine(dir.FullName, "Starling.sln"))) + { dir = dir.Parent; + } + if (dir is null) + { throw new InvalidOperationException("Could not locate the Starling solution walking up from the bench binary."); + } + return dir.FullName; } @@ -118,8 +126,11 @@ public static string TextHeavyParagraphs(int paragraphs) var sb = new System.Text.StringBuilder(paragraphs * 120 + 64); sb.Append("
"); for (var i = 0; i < paragraphs; i++) + { sb.Append("

Paragraph ").Append(i) .Append(" has several words of body text that the engine must shape and wrap across the available width of the line box.

"); + } + sb.Append("
"); return sb.ToString(); } @@ -129,9 +140,17 @@ public static string NestedFlex(int depth) { var sb = new System.Text.StringBuilder(depth * 48 + 96); sb.Append(""); - for (var i = 0; i < depth; i++) sb.Append("
"); + for (var i = 0; i < depth; i++) + { + sb.Append("
"); + } + sb.Append("leaf"); - for (var i = 0; i < depth; i++) sb.Append("
"); + for (var i = 0; i < depth; i++) + { + sb.Append("
"); + } + sb.Append(""); return sb.ToString(); } @@ -143,7 +162,11 @@ public static string ManyBorders(int boxes) { var sb = new System.Text.StringBuilder(boxes * 28 + 96); sb.Append(""); - for (var i = 0; i < boxes; i++) sb.Append("
"); + for (var i = 0; i < boxes; i++) + { + sb.Append("
"); + } + sb.Append(""); return sb.ToString(); } @@ -168,7 +191,10 @@ public static string AnimatedBoxesHtml(int boxes) var sb = new System.Text.StringBuilder(boxes * 44 + 96); sb.Append("
"); for (var i = 0; i < boxes; i++) + { sb.Append("
Box ").Append(i).Append("
"); + } + sb.Append("
"); return sb.ToString(); } @@ -204,7 +230,11 @@ public static string SolidBackgrounds(int boxes) { var sb = new System.Text.StringBuilder(boxes * 24 + 96); sb.Append(""); - for (var i = 0; i < boxes; i++) sb.Append("
"); + for (var i = 0; i < boxes; i++) + { + sb.Append("
"); + } + sb.Append(""); return sb.ToString(); } @@ -229,9 +259,12 @@ public static string PromotedCards(int cards) var sb = new System.Text.StringBuilder(cards * 96 + 96); sb.Append("
"); for (var i = 0; i < cards; i++) + { sb.Append("

Card ").Append(i) .Append("

Card ").Append(i) .Append(" body text that the layer must shape and fill when its cache is cold.

"); + } + sb.Append("
"); return sb.ToString(); } diff --git a/bench/Starling.Bench/GitHubStyleBench.cs b/bench/Starling.Bench/GitHubStyleBench.cs index 7df0b4c4..e96cb36f 100644 --- a/bench/Starling.Bench/GitHubStyleBench.cs +++ b/bench/Starling.Bench/GitHubStyleBench.cs @@ -38,8 +38,10 @@ public void Setup() LoadInlineSheets(_doc); if (_cssTexts.Count == 0) + { throw new InvalidOperationException( "GitHub local snapshot has no CSS. Re-run tools/snapshot-vendor/vendor-github-home.sh."); + } } [Benchmark] @@ -47,7 +49,10 @@ public int ParseCss_GitHubHome() { var rules = 0; foreach (var css in _cssTexts) + { rules += CssParser.ParseStyleSheet(css).Rules.Count; + } + return rules; } @@ -109,15 +114,21 @@ private void LoadExternalSheets(Document doc) foreach (var link in Elements(doc)) { if (!IsStylesheetLink(link)) + { continue; + } var href = link.GetAttribute("href"); if (string.IsNullOrWhiteSpace(href)) + { continue; + } var path = SnapshotPathFromHref(href); if (path is null || !File.Exists(path) || _externalSheets.ContainsKey(href)) + { continue; + } var css = File.ReadAllText(path); _cssTexts.Add(css); @@ -130,11 +141,15 @@ private void LoadInlineSheets(Document doc) foreach (var element in Elements(doc)) { if (!string.Equals(element.LocalName, "style", StringComparison.Ordinal)) + { continue; + } var source = element.TextContent; if (string.IsNullOrWhiteSpace(source)) + { continue; + } _cssTexts.Add(source); _inlineSheets[element] = CssParser.ParseStyleSheet(source); @@ -162,18 +177,26 @@ private void AddAuthorSheets(Document doc, StyleEngine style, bool useCachedInli { var source = element.TextContent; if (string.IsNullOrWhiteSpace(source)) + { continue; + } if (useCachedInlineSheets && _inlineSheets.TryGetValue(element, out var cached)) + { style.AddStyleSheet(cached); + } else + { style.AddStyleSheet(CssParser.ParseStyleSheet(source)); + } } else if (IsStylesheetLink(element)) { var href = element.GetAttribute("href"); if (href is not null && _externalSheets.TryGetValue(href, out var sheet)) + { style.AddStyleSheet(sheet); + } } } } @@ -181,15 +204,23 @@ private void AddAuthorSheets(Document doc, StyleEngine style, bool useCachedInli private static bool IsStylesheetLink(Element element) { if (!string.Equals(element.LocalName, "link", StringComparison.Ordinal)) + { return false; + } var rel = element.GetAttribute("rel"); if (rel is null) + { return false; + } foreach (var token in rel.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)) + { if (string.Equals(token, "stylesheet", StringComparison.OrdinalIgnoreCase)) + { return true; + } + } return false; } @@ -198,11 +229,15 @@ private static bool IsStylesheetLink(Element element) { var end = href.IndexOfAny(['?', '#']); if (end >= 0) + { href = href[..end]; + } href = href.TrimStart('/'); if (!href.StartsWith("assets/", StringComparison.Ordinal)) + { return null; + } return Path.Combine(Fixtures.GitHubSnapshotRoot, href.Replace('/', Path.DirectorySeparatorChar)); } @@ -210,10 +245,16 @@ private static bool IsStylesheetLink(Element element) private static IEnumerable Elements(Node root) { if (root is Element element) + { yield return element; + } foreach (var child in root.ChildNodes) + { foreach (var nested in Elements(child)) + { yield return nested; + } + } } } diff --git a/bench/Starling.Bench/GitHubStyleSmoke.cs b/bench/Starling.Bench/GitHubStyleSmoke.cs index 909b3b82..7dda2ea9 100644 --- a/bench/Starling.Bench/GitHubStyleSmoke.cs +++ b/bench/Starling.Bench/GitHubStyleSmoke.cs @@ -58,7 +58,9 @@ public MetricRecorder() _listener.InstrumentPublished = (inst, lst) => { if (inst.Meter.Name == StarlingTelemetry.SourceName) + { lst.EnableMeasurementEvents(inst); + } }; _listener.SetMeasurementEventCallback((inst, m, _, _) => Add(inst.Name, m)); _listener.SetMeasurementEventCallback((inst, m, _, _) => Add(inst.Name, (double)m)); @@ -73,7 +75,9 @@ public void Print(string prefix) foreach (var pair in _counters .Where(pair => pair.Key.StartsWith(prefix, StringComparison.Ordinal)) .OrderBy(pair => pair.Key)) + { Console.WriteLine($" counter {pair.Key}: {pair.Value:N0}"); + } } public void Dispose() => _listener.Dispose(); @@ -94,13 +98,16 @@ public SpanRecorder() { ShouldListenTo = src => src.Name == StarlingTelemetry.SourceName, Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStarted = a => { lock (_active) _active[a.Id ?? a.OperationName] = Stopwatch.StartNew(); }, + ActivityStarted = a => { lock (_active) { _active[a.Id ?? a.OperationName] = Stopwatch.StartNew(); } }, ActivityStopped = a => { Stopwatch? sw; lock (_active) { - if (!_active.Remove(a.Id ?? a.OperationName, out sw)) return; + if (!_active.Remove(a.Id ?? a.OperationName, out sw)) + { + return; + } } sw.Stop(); lock (_spans) @@ -116,7 +123,9 @@ public SpanRecorder() public void Print() { foreach (var pair in _spans.OrderByDescending(pair => pair.Value)) + { Console.WriteLine($" span {pair.Key}: {pair.Value} ms"); + } } public void Dispose() => _listener.Dispose(); diff --git a/bench/Starling.Bench/H1ResponseBench.cs b/bench/Starling.Bench/H1ResponseBench.cs index 0558b8b1..d3119173 100644 --- a/bench/Starling.Bench/H1ResponseBench.cs +++ b/bench/Starling.Bench/H1ResponseBench.cs @@ -52,7 +52,11 @@ private static byte[] BuildContentLengthResponse(int payloadBytes) var head = $"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {payloadBytes}\r\nConnection: keep-alive\r\n\r\n"; var bytes = new byte[Encoding.ASCII.GetByteCount(head) + payloadBytes]; var off = Encoding.ASCII.GetBytes(head, bytes); - for (var i = 0; i < payloadBytes; i++) bytes[off + i] = (byte)('a' + (i % 26)); + for (var i = 0; i < payloadBytes; i++) + { + bytes[off + i] = (byte)('a' + (i % 26)); + } + return bytes; } @@ -64,7 +68,11 @@ private static byte[] BuildChunkedResponse(int chunks, int perChunk) { sb.Append(perChunk.ToString("X", System.Globalization.CultureInfo.InvariantCulture)); sb.Append("\r\n"); - for (var i = 0; i < perChunk; i++) sb.Append((char)('a' + (i % 26))); + for (var i = 0; i < perChunk; i++) + { + sb.Append((char)('a' + (i % 26))); + } + sb.Append("\r\n"); } sb.Append("0\r\n\r\n"); diff --git a/bench/Starling.Bench/HtmlBench.cs b/bench/Starling.Bench/HtmlBench.cs index 36f89c20..246c8480 100644 --- a/bench/Starling.Bench/HtmlBench.cs +++ b/bench/Starling.Bench/HtmlBench.cs @@ -40,7 +40,11 @@ private static int TokenizeAndCount(string source) tk.Feed(source); tk.EndOfInput(); var count = 0; - while (tk.ReadToken() is not null) count++; + while (tk.ReadToken() is not null) + { + count++; + } + return count; } } diff --git a/bench/Starling.Bench/IncrementalLayoutBench.cs b/bench/Starling.Bench/IncrementalLayoutBench.cs index 862c9311..b541bac5 100644 --- a/bench/Starling.Bench/IncrementalLayoutBench.cs +++ b/bench/Starling.Bench/IncrementalLayoutBench.cs @@ -131,9 +131,13 @@ public double Incremental_NoChange() private void ToggleScratch() { if (_scratchChild.ParentNode is not null) + { _scratchParent.RemoveChild(_scratchChild); + } else + { _scratchParent.AppendChild(_scratchChild); + } } private static Element MakeRow(Document doc, string id) @@ -153,17 +157,34 @@ private static Element MakeRow(Document doc, string id) { for (var c = node.FirstChild; c is not null; c = c.NextSibling) { - if (c is Text t && !string.IsNullOrWhiteSpace(t.Data)) return t; - if (FirstText(c) is { } found) return found; + if (c is Text t && !string.IsNullOrWhiteSpace(t.Data)) + { + return t; + } + + if (FirstText(c) is { } found) + { + return found; + } } return null; } private static Element? FirstElement(Element root, string localName) { - if (string.Equals(root.LocalName, localName, StringComparison.OrdinalIgnoreCase)) return root; + if (string.Equals(root.LocalName, localName, StringComparison.OrdinalIgnoreCase)) + { + return root; + } + for (var c = root.FirstChild; c is not null; c = c.NextSibling) - if (c is Element e && FirstElement(e, localName) is { } found) return found; + { + if (c is Element e && FirstElement(e, localName) is { } found) + { + return found; + } + } + return null; } } @@ -274,8 +295,15 @@ public double Incremental_TextChange() { for (var c = node.FirstChild; c is not null; c = c.NextSibling) { - if (c is Text t && !string.IsNullOrWhiteSpace(t.Data)) return t; - if (c is Element e && IsRendered(e) && FirstRenderedText(e) is { } found) return found; + if (c is Text t && !string.IsNullOrWhiteSpace(t.Data)) + { + return t; + } + + if (c is Element e && IsRendered(e) && FirstRenderedText(e) is { } found) + { + return found; + } } return null; } @@ -289,9 +317,19 @@ public double Incremental_TextChange() private static Element? FirstElement(Element root, string localName) { - if (string.Equals(root.LocalName, localName, StringComparison.OrdinalIgnoreCase)) return root; + if (string.Equals(root.LocalName, localName, StringComparison.OrdinalIgnoreCase)) + { + return root; + } + for (var c = root.FirstChild; c is not null; c = c.NextSibling) - if (c is Element e && FirstElement(e, localName) is { } found) return found; + { + if (c is Element e && FirstElement(e, localName) is { } found) + { + return found; + } + } + return null; } } diff --git a/bench/Starling.Bench/JsBench.cs b/bench/Starling.Bench/JsBench.cs index d4f6442a..7c0974cc 100644 --- a/bench/Starling.Bench/JsBench.cs +++ b/bench/Starling.Bench/JsBench.cs @@ -56,7 +56,11 @@ public int Lex_FibRecursive() { var lex = new JsLexer(FibRecursiveSrc); var count = 0; - while (lex.Next().Kind != JsTokenKind.EndOfFile) count++; + while (lex.Next().Kind != JsTokenKind.EndOfFile) + { + count++; + } + return count; } diff --git a/bench/Starling.Bench/Program.cs b/bench/Starling.Bench/Program.cs index cdb001dc..d7a2ff3c 100644 --- a/bench/Starling.Bench/Program.cs +++ b/bench/Starling.Bench/Program.cs @@ -13,15 +13,29 @@ public static int Main(string[] args) // Custom run-modes dispatched before the BenchmarkDotNet switcher: the // frame-replay harness and its baseline-compare tool. if (args.Length > 0 && args[0] == "replay") + { return ReplayProgram.Run(args[1..]); + } + if (args.Length > 0 && args[0] == "compare") + { return ReplayCompare.Run(args[1..]); + } + if (args.Length > 0 && args[0] == "report") + { return ReplayReport.Run(args[1..]); + } + if (args.Length > 0 && args[0] == "animtrace") + { return AnimationTraceProgram.Run(args[1..]); + } + if (args.Length > 0 && args[0] == "github-style-smoke") + { return GitHubStyleSmoke.Run(); + } // Add tail-latency columns so the BenchmarkDotNet tables carry p95/p90, // not just mean/median — the percentiles that predict frame budget misses. diff --git a/bench/Starling.Bench/RasterBench.cs b/bench/Starling.Bench/RasterBench.cs index f789ddf2..f9497ca9 100644 --- a/bench/Starling.Bench/RasterBench.cs +++ b/bench/Starling.Bench/RasterBench.cs @@ -59,7 +59,10 @@ private static DisplayList BuildList(string html, string? css) var doc = HtmlParser.Parse(html); var style = new StyleEngine(); if (css is not null) + { style.AddStyleSheet(CssParser.ParseStyleSheet(css)); + } + var root = new LayoutEngine(style).LayoutDocument(doc, Viewport); return new DisplayListBuilder().Build(root); } diff --git a/bench/Starling.Bench/Replay/CountingTextMeasurer.cs b/bench/Starling.Bench/Replay/CountingTextMeasurer.cs index 73e42739..23c622ae 100644 --- a/bench/Starling.Bench/Replay/CountingTextMeasurer.cs +++ b/bench/Starling.Bench/Replay/CountingTextMeasurer.cs @@ -37,9 +37,14 @@ public ShapedRun Shape(string text, double fontSize, FontSpec spec) { _shapeCalls++; if (_seen.Add((text, fontSize, spec))) + { _shapeMisses++; + } else + { _shapeHits++; + } + return _inner.Shape(text, fontSize, spec); } diff --git a/bench/Starling.Bench/Replay/FrameReplayHarness.cs b/bench/Starling.Bench/Replay/FrameReplayHarness.cs index 3be3bd2e..b84fb963 100644 --- a/bench/Starling.Bench/Replay/FrameReplayHarness.cs +++ b/bench/Starling.Bench/Replay/FrameReplayHarness.cs @@ -140,7 +140,9 @@ public ReplayResult Run() ["display_list"] = PhaseStats.From(dlT, dlA), }; if (_options.RunRaster) + { phases["raster"] = PhaseStats.From(raT, raA); + } var measure = new MeasureStats( MeanMeasureWidthCalls: mwSum / n, @@ -183,7 +185,11 @@ private BlockBox RunFrame(int frameIndex, double nowMs, Stopwatch sw, ref PhaseT { // Age the recently-mutated promotion window by one frame (mirrors the live // shell) before this frame records new mutations. - if (_options.Composite) _scenario.Document.DecayRecentMutations(); + if (_options.Composite) + { + _scenario.Document.DecayRecentMutations(); + } + _scenario.MutateForFrame(frameIndex); var a0 = GC.GetAllocatedBytesForCurrentThread(); @@ -198,10 +204,15 @@ private BlockBox RunFrame(int frameIndex, double nowMs, Stopwatch sw, ref PhaseT a0 = GC.GetAllocatedBytesForCurrentThread(); sw.Restart(); if (_session is not null) + { root = _session.Layout(_scenario.Document, _scenario.Viewport, _measurer, nowMs); + } else + { root = new LayoutEngine(_scenario.Style, _measurer) .LayoutDocument(_scenario.Document, _scenario.Viewport, nowMs); + } + sw.Stop(); pt.LayoutTicks = sw.ElapsedTicks; pt.LayoutAlloc = GC.GetAllocatedBytesForCurrentThread() - a0; @@ -254,10 +265,26 @@ private BlockBox RunFrame(int frameIndex, double nowMs, Stopwatch sw, ref PhaseT // is actively animating — the live shell's predicate (LTF-01 / LTF-06). private bool Promote(Box box) { - if (box.Element is not { } el) return false; - if (_scenario.Document.WasRecentlyMutated(el)) return true; - foreach (var _ in _scenario.Style.AnimationEngine.ActiveProperties(el)) return true; - foreach (var _ in _scenario.Style.TransitionEngine.ActiveProperties(el)) return true; + if (box.Element is not { } el) + { + return false; + } + + if (_scenario.Document.WasRecentlyMutated(el)) + { + return true; + } + + foreach (var _ in _scenario.Style.AnimationEngine.ActiveProperties(el)) + { + return true; + } + + foreach (var _ in _scenario.Style.TransitionEngine.ActiveProperties(el)) + { + return true; + } + return false; } @@ -266,7 +293,10 @@ private static int CountLayers(CompositorLayer layer) var count = 1; var children = layer.Children; for (var i = 0; i < children.Count; i++) + { count += CountLayers(children[i]); + } + return count; } @@ -298,7 +328,10 @@ private static int CountBoxes(Box box) var count = 1; var children = box.Children; for (var i = 0; i < children.Count; i++) + { count += CountBoxes(children[i]); + } + return count; } diff --git a/bench/Starling.Bench/Replay/PhaseStats.cs b/bench/Starling.Bench/Replay/PhaseStats.cs index ce6e6073..83f2905b 100644 --- a/bench/Starling.Bench/Replay/PhaseStats.cs +++ b/bench/Starling.Bench/Replay/PhaseStats.cs @@ -29,7 +29,9 @@ public static PhaseStats From(long[] ticks, long[] allocBytes) ArgumentNullException.ThrowIfNull(ticks); ArgumentNullException.ThrowIfNull(allocBytes); if (ticks.Length == 0) + { return default; + } var n = ticks.Length; var ms = new double[n]; @@ -41,8 +43,15 @@ public static PhaseStats From(long[] ticks, long[] allocBytes) var v = ticks[i] * 1000.0 / Stopwatch.Frequency; ms[i] = v; sum += v; - if (v > 16.666_67) dropped60++; - if (v > 8.333_33) dropped120++; + if (v > 16.666_67) + { + dropped60++; + } + + if (v > 8.333_33) + { + dropped120++; + } } Array.Sort(ms); @@ -51,7 +60,10 @@ public static PhaseStats From(long[] ticks, long[] allocBytes) for (var i = 0; i < n; i++) { allocSum += allocBytes[i]; - if (allocBytes[i] > allocMax) allocMax = allocBytes[i]; + if (allocBytes[i] > allocMax) + { + allocMax = allocBytes[i]; + } } return new PhaseStats( @@ -70,11 +82,19 @@ public static PhaseStats From(long[] ticks, long[] allocBytes) private static double Percentile(double[] sortedAsc, double p) { var n = sortedAsc.Length; - if (n == 1) return sortedAsc[0]; + if (n == 1) + { + return sortedAsc[0]; + } + var rank = p / 100.0 * (n - 1); var lo = (int)Math.Floor(rank); var hi = (int)Math.Ceiling(rank); - if (lo == hi) return sortedAsc[lo]; + if (lo == hi) + { + return sortedAsc[lo]; + } + return sortedAsc[lo] + (sortedAsc[hi] - sortedAsc[lo]) * (rank - lo); } } diff --git a/bench/Starling.Bench/Replay/ReplayCompare.cs b/bench/Starling.Bench/Replay/ReplayCompare.cs index 5d2910d1..56e37648 100644 --- a/bench/Starling.Bench/Replay/ReplayCompare.cs +++ b/bench/Starling.Bench/Replay/ReplayCompare.cs @@ -21,9 +21,13 @@ public static int Run(string[] args) for (var i = 0; i < args.Length; i++) { if (args[i] == "--threshold") + { threshold = double.Parse(args[++i], CultureInfo.InvariantCulture); + } else + { positional.Add(args[i]); + } } } catch (Exception ex) when (ex is FormatException or IndexOutOfRangeException) @@ -41,7 +45,9 @@ public static int Run(string[] args) var baseline = Load(positional[0]); var candidate = Load(positional[1]); if (baseline is null || candidate is null) + { return 2; + } if (baseline.Page != candidate.Page || baseline.ScopeLabel != candidate.ScopeLabel) { @@ -57,8 +63,15 @@ public static int Run(string[] args) var regressed = false; foreach (var key in new[] { "frame", "style_anim", "layout", "display_list", "raster" }) { - if (!baseline.Phases.TryGetValue(key, out var b)) continue; - if (!candidate.Phases.TryGetValue(key, out var c)) continue; + if (!baseline.Phases.TryGetValue(key, out var b)) + { + continue; + } + + if (!candidate.Phases.TryGetValue(key, out var c)) + { + continue; + } // Percentage thresholds with small absolute floors so a near-zero // phase (e.g. style_anim with no animations) does not flag on noise. @@ -88,7 +101,10 @@ public static int Run(string[] args) } var result = JsonSerializer.Deserialize(File.ReadAllText(path), ReplayJsonContext.Default.ReplayResult); if (result is null) + { Console.Error.WriteLine($"could not parse: {path}"); + } + return result; } diff --git a/bench/Starling.Bench/Replay/ReplayProgram.cs b/bench/Starling.Bench/Replay/ReplayProgram.cs index 78824934..462a8870 100644 --- a/bench/Starling.Bench/Replay/ReplayProgram.cs +++ b/bench/Starling.Bench/Replay/ReplayProgram.cs @@ -19,7 +19,9 @@ public static int Run(string[] args) } if (args[0] == "--selftest") + { return SelfTest(); + } var page = args[0]; var frames = 600; @@ -103,7 +105,11 @@ private static void PrintReport(ReplayResult r) Console.WriteLine($"{"phase",-13}{"mean",9}{"p50",9}{"p95",9}{"p99",9}{"max",9}{"drop>16.7",11}{"drop>8.3",10}{"alloc/f",12}"); foreach (var key in new[] { "frame", "style_anim", "layout", "display_list", "raster" }) { - if (!r.Phases.TryGetValue(key, out var p)) continue; + if (!r.Phases.TryGetValue(key, out var p)) + { + continue; + } + Console.WriteLine( $"{key,-13}{Ms(p.MeanMs),9}{Ms(p.P50Ms),9}{Ms(p.P95Ms),9}{Ms(p.P99Ms),9}{Ms(p.MaxMs),9}" + $"{p.DroppedOver16_67ms,11}{p.DroppedOver8_33ms,10}{Bytes(p.MeanAllocBytes),12}"); @@ -115,9 +121,11 @@ private static void PrintReport(ReplayResult r) + $"shape cache hit-rate {r.TextMeasure.ShapeCacheHitRate.ToString("P1", CultureInfo.InvariantCulture)}"); Console.WriteLine($"Nodes visited/frame: {r.TextMeasure.MeanNodesVisited.ToString("F0", CultureInfo.InvariantCulture)}"); if (r.Composite is { } c) + { Console.WriteLine( $"Compositor: layers/frame {F1(c.MeanLayersPerFrame)} rastered/frame {F1(c.MeanLayersRasteredPerFrame)} " + $"blitted-from-cache/frame {F1(c.MeanLayersBlittedPerFrame)}"); + } } /// diff --git a/bench/Starling.Bench/Replay/ReplayReport.cs b/bench/Starling.Bench/Replay/ReplayReport.cs index 63f28ed0..f851f40e 100644 --- a/bench/Starling.Bench/Replay/ReplayReport.cs +++ b/bench/Starling.Bench/Replay/ReplayReport.cs @@ -62,9 +62,15 @@ public static int Run(string[] args) private static string? ResolveDateDir(string resultsRoot, string? dateOverride) { if (dateOverride is not null) + { return Path.Combine(resultsRoot, dateOverride); + } + if (!Directory.Exists(resultsRoot)) + { return null; + } + var dirs = Directory.GetDirectories(resultsRoot); Array.Sort(dirs, StringComparer.Ordinal); // yyyy-MM-dd sorts chronologically return dirs.Length > 0 ? dirs[^1] : null; @@ -74,14 +80,19 @@ private static List LoadReplayResults(string? dateDir) { var list = new List(); if (dateDir is null || !Directory.Exists(dateDir)) + { return list; + } + foreach (var file in Directory.GetFiles(dateDir, "*.json")) { try { var r = JsonSerializer.Deserialize(File.ReadAllText(file), ReplayJsonContext.Default.ReplayResult); if (r is not null) + { list.Add(r); + } } catch (JsonException) { @@ -92,9 +103,17 @@ private static List LoadReplayResults(string? dateDir) list.Sort((a, b) => { var p = string.CompareOrdinal(a.Page, b.Page); - if (p != 0) return p; + if (p != 0) + { + return p; + } + var s = string.CompareOrdinal(a.ScopeLabel, b.ScopeLabel); - if (s != 0) return s; + if (s != 0) + { + return s; + } + return b.RasterEnabled.CompareTo(a.RasterEnabled); }); return list; @@ -105,7 +124,10 @@ private static List LoadReplayResults(string? dateDir) { var list = new List<(string, string)>(); if (!Directory.Exists(bdnDir)) + { return list; + } + foreach (var file in Directory.GetFiles(bdnDir, "*-report-github.md")) { var name = Path.GetFileNameWithoutExtension(file) @@ -115,9 +137,13 @@ private static List LoadReplayResults(string? dateDir) // Skip stale or failed runs: a successful BenchmarkDotNet table always // carries a time unit. An all-NA table (a run that errored) has none. if (table.Length > 0 && HasMeasurements(table)) + { list.Add((name, table)); + } else if (table.Length > 0) + { Console.WriteLine($" skipped {name}: no successful measurements (stale or failed run)."); + } } list.Sort((a, b) => string.CompareOrdinal(a.Item1, b.Item1)); return list; @@ -136,8 +162,13 @@ private static string ExtractTable(string[] lines) { var sb = new StringBuilder(); foreach (var line in lines) + { if (line.StartsWith('|')) + { sb.AppendLine(line.TrimEnd()); + } + } + return sb.ToString().TrimEnd(); } @@ -233,7 +264,9 @@ private static void RenderReplaySection(StringBuilder sb, List rep // Close each page block after its last row by peeking is awkward; a // trailing blank line per row is harmless and keeps tables separated. if (IsLastOfPage(replay, r)) + { sb.AppendLine(); + } } } diff --git a/bench/Starling.Bench/Replay/ReplayScenarios.cs b/bench/Starling.Bench/Replay/ReplayScenarios.cs index 19b392b6..118b7689 100644 --- a/bench/Starling.Bench/Replay/ReplayScenarios.cs +++ b/bench/Starling.Bench/Replay/ReplayScenarios.cs @@ -57,7 +57,9 @@ private static ReplayScenario FlexStatus() MutateForFrame = frame => { if (status is not null) + { status.Data = frame % 2 == 0 ? "running 16 ms" : "running 32 ms"; + } }, }; } @@ -77,7 +79,9 @@ private static ReplayScenario ListPage() MutateForFrame = frame => { if (text is not null) + { text.Data = "Item 0 frame " + (frame % 100); + } }, }; } @@ -130,7 +134,10 @@ private static ReplayScenario CompositorDemo() // spin (its slice is upright, so its content hash stays stable and // it re-blits from cache). if (status is not null) + { status.Data = frame % 2 == 0 ? "running 16 ms" : "running 32 ms"; + } + spin?.SetAttribute("style", spinBase + $"transform:rotate({frame * 12 % 360}deg)"); }, }; @@ -147,9 +154,19 @@ private static StyleEngine NewStyle() private static Text? FindFirstText(Element? element) { - if (element is null) return null; + if (element is null) + { + return null; + } + for (var child = element.FirstChild; child is not null; child = child.NextSibling) - if (child is Text t) return t; + { + if (child is Text t) + { + return t; + } + } + return null; } } diff --git a/bench/Starling.Bench/SsimBench.cs b/bench/Starling.Bench/SsimBench.cs index bbb1fa4d..4a2e7665 100644 --- a/bench/Starling.Bench/SsimBench.cs +++ b/bench/Starling.Bench/SsimBench.cs @@ -30,7 +30,9 @@ public void Setup() Array.Copy(_a, _noisy, size); // Inject 1% noise so the SSIM scoring path runs meaningfully. for (var i = 0; i < size; i += 100) + { _noisy[i] = (byte)(_a[i] ^ 0x40); + } } [Benchmark] diff --git a/bench/Starling.Bench/StyleBench.cs b/bench/Starling.Bench/StyleBench.cs index ee2b3d9f..79d925fd 100644 --- a/bench/Starling.Bench/StyleBench.cs +++ b/bench/Starling.Bench/StyleBench.cs @@ -47,9 +47,15 @@ public int Compute_SingleElement_NginxBody() foreach (var child in root.ChildNodes) { if (child is Element e && string.Equals(e.LocalName, tag, StringComparison.OrdinalIgnoreCase)) + { return e; + } + var nested = FirstByTag(child, tag); - if (nested is not null) return nested; + if (nested is not null) + { + return nested; + } } return null; } diff --git a/bench/Starling.HtmlParserBench/Fixtures.cs b/bench/Starling.HtmlParserBench/Fixtures.cs index 5e7b2bfe..fd3612c0 100644 --- a/bench/Starling.HtmlParserBench/Fixtures.cs +++ b/bench/Starling.HtmlParserBench/Fixtures.cs @@ -52,10 +52,16 @@ private static string LocateRepoRoot() while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "Starling.slnx")) && !File.Exists(Path.Combine(dir.FullName, "Starling.sln"))) + { dir = dir.Parent; + } + if (dir is null) + { throw new InvalidOperationException( "Could not locate the Starling solution walking up from the bench binary."); + } + return dir.FullName; } } diff --git a/bench/Starling.JsEngineBench/EngineComparisonBench.cs b/bench/Starling.JsEngineBench/EngineComparisonBench.cs index 48c1ae84..a1c51b1a 100644 --- a/bench/Starling.JsEngineBench/EngineComparisonBench.cs +++ b/bench/Starling.JsEngineBench/EngineComparisonBench.cs @@ -109,7 +109,11 @@ private bool Validate(string label, Func run) private void Skip(string label, Exception ex) { var msg = ex.Message.ReplaceLineEndings(" "); - if (msg.Length > 120) msg = msg[..120]; + if (msg.Length > 120) + { + msg = msg[..120]; + } + Console.WriteLine($"[skip] {label} / {FileName}: {ex.GetType().Name}: {msg}"); } diff --git a/src/Starling.AppHost/AppHost.cs b/src/Starling.AppHost/AppHost.cs index f248821a..79ac3e22 100644 --- a/src/Starling.AppHost/AppHost.cs +++ b/src/Starling.AppHost/AppHost.cs @@ -101,7 +101,10 @@ static string LocateRepoRoot() { var dir = AppContext.BaseDirectory; while (!string.IsNullOrEmpty(dir) && !File.Exists(Path.Combine(dir, "Starling.slnx"))) + { dir = Path.GetDirectoryName(dir); + } + return string.IsNullOrEmpty(dir) ? throw new InvalidOperationException("Could not locate Starling.slnx from " + AppContext.BaseDirectory) : dir; @@ -142,14 +145,24 @@ static string LocateRepoRoot() string? selected = null; string? selectedFlag = null; foreach (var arg in args) + { foreach (var (flag, value) in mappings) { - if (arg != flag) continue; + if (arg != flag) + { + continue; + } + if (selected is not null && selected != value) + { throw new InvalidOperationException( $"Conflicting {label} flags: {selectedFlag} and {flag}. Pass only one."); + } + (selected, selectedFlag) = (value, flag); } + } + return selected; } diff --git a/src/Starling.Bindings.Jint/AnimationFrameBinding.cs b/src/Starling.Bindings.Jint/AnimationFrameBinding.cs index c39aa357..16b8a536 100644 --- a/src/Starling.Bindings.Jint/AnimationFrameBinding.cs +++ b/src/Starling.Bindings.Jint/AnimationFrameBinding.cs @@ -29,8 +29,11 @@ public static void Install(JintBackendContext ctx) 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); @@ -38,7 +41,11 @@ public static void Install(JintBackendContext ctx) JintInterop.DefineMethod(engine, engine.Global, "cancelAnimationFrame", (_, args) => { - if (TryCoerceId(args, out var id)) loop.CancelAnimationFrame(id); + if (TryCoerceId(args, out var id)) + { + loop.CancelAnimationFrame(id); + } + return JsValue.Undefined; }, 1); } @@ -46,10 +53,22 @@ public static void Install(JintBackendContext ctx) private static bool TryCoerceId(JsValue[] args, out int id) { id = 0; - if (args.Length == 0) return false; + 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; + if (double.IsNaN(n) || double.IsInfinity(n)) + { + return false; + } + + if (n < int.MinValue || n > int.MaxValue) + { + return false; + } + id = (int)n; return true; } diff --git a/src/Starling.Bindings.Jint/AttrBinding.cs b/src/Starling.Bindings.Jint/AttrBinding.cs index b138fae3..b35ca8bc 100644 --- a/src/Starling.Bindings.Jint/AttrBinding.cs +++ b/src/Starling.Bindings.Jint/AttrBinding.cs @@ -28,7 +28,10 @@ public static void Install(JintBackendContext ctx) var engine = ctx.Engine; var elProto = ctx.Wrappers.ElementPrototype; var docProto = ctx.Wrappers.DocumentPrototype; - if (elProto is null || docProto is null) return; + if (elProto is null || docProto is null) + { + return; + } // ---- NamedNodeMap.prototype -------------------------------------------- var proto = new JsObject(engine); @@ -47,21 +50,39 @@ public static void Install(JintBackendContext ctx) : JsValue.Null, 2); JintInterop.DefineMethod(engine, proto, "setNamedItem", (t, a) => { - if (t is not JintNamedNodeMapObject m) return JsValue.Null; + 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 (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; + 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."); @@ -70,7 +91,11 @@ public static void Install(JintBackendContext ctx) }, 1); JintInterop.DefineMethod(engine, proto, "removeNamedItemNS", (t, a) => { - if (t is not JintNamedNodeMapObject m) return JsValue.Null; + 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) @@ -83,41 +108,73 @@ public static void Install(JintBackendContext ctx) // ---- 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; + 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; + 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.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.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.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); @@ -128,7 +185,11 @@ public static void Install(JintBackendContext ctx) 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"); + if (name.Length == 0) + { + throw DomExceptionBinding.Throw(ctx, "InvalidCharacterError", "createAttribute: empty name"); + } + return ctx.Wrappers.Wrap(new AttrNode(name.ToLowerInvariant())); }, 1); } @@ -164,7 +225,10 @@ public JintNamedNodeMapObject(JintBackendContext ctx, Element element) : base(ct { _ctx = ctx; Element = element; - if (ctx.Wrappers.NamedNodeMapPrototype is { } p) Prototype = p; + if (ctx.Wrappers.NamedNodeMapPrototype is { } p) + { + Prototype = p; + } } public int Length => Element.Attributes.Count; @@ -177,7 +241,13 @@ public JintNamedNodeMapObject(JintBackendContext ctx, Element element) : base(ct private bool IsOnPrototype(JsValue name) { for (var p = Prototype; p is not null; p = p.Prototype) - if (p.GetOwnProperty(name) != PropertyDescriptor.Undefined) return true; + { + if (p.GetOwnProperty(name) != PropertyDescriptor.Undefined) + { + return true; + } + } + return false; } @@ -187,9 +257,14 @@ public override JsValue Get(JsValue property, JsValue receiver) { 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); } @@ -202,11 +277,15 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) 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); } @@ -216,8 +295,15 @@ 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; + 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); } @@ -226,8 +312,13 @@ public override List GetOwnPropertyKeys(Types types = Types.String | Ty { 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/CollectionsBinding.cs b/src/Starling.Bindings.Jint/CollectionsBinding.cs index 6d0471d2..961e4aa6 100644 --- a/src/Starling.Bindings.Jint/CollectionsBinding.cs +++ b/src/Starling.Bindings.Jint/CollectionsBinding.cs @@ -26,7 +26,11 @@ internal static class CollectionsBinding public static void Install(JintBackendContext ctx) { ArgumentNullException.ThrowIfNull(ctx); - if (ctx.Wrappers.NodeListPrototype is not null) return; // idempotent + if (ctx.Wrappers.NodeListPrototype is not null) + { + return; // idempotent + } + var engine = ctx.Engine; // ---- NodeList.prototype ------------------------------------------------- @@ -78,7 +82,11 @@ private static void DefineIteration(global::Jint.Engine engine, ObjectInstance p { 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); + 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, _) => @@ -86,17 +94,27 @@ private static void DefineIteration(global::Jint.Engine engine, ObjectInstance p 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; + 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); } @@ -146,7 +164,10 @@ public JintNodeListObject(JintBackendContext ctx, ObjectInstance? prototype, Fun { _ctx = ctx; _source = source; - if (prototype is not null) Prototype = prototype; + if (prototype is not null) + { + Prototype = prototype; + } } private IReadOnlyList Items => _source(); @@ -162,7 +183,11 @@ 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]); + for (var i = 0; i < items.Count; i++) + { + values[i] = _ctx.Wrappers.Wrap(items[i]); + } + return new JsArray(engine, values); } @@ -182,14 +207,20 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) { 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; + if (property.IsString() && CollectionIndex.TryIndex(property.AsString(), out var i) && i < Items.Count) + { + return true; + } + return base.HasProperty(property); } @@ -197,7 +228,13 @@ public override List GetOwnPropertyKeys(Types types = Types.String | Ty { 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))); + { + for (var i = 0; i < Items.Count; i++) + { + keys.Add(JintInterop.Str(i.ToString(CultureInfo.InvariantCulture))); + } + } + keys.AddRange(base.GetOwnPropertyKeys(types)); return keys; } @@ -216,7 +253,10 @@ public JintHtmlCollectionObject(JintBackendContext ctx, ObjectInstance? prototyp { _ctx = ctx; _source = source; - if (prototype is not null) Prototype = prototype; + if (prototype is not null) + { + Prototype = prototype; + } } private IReadOnlyList Items => _source(); @@ -235,18 +275,38 @@ 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]); + 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; + if (name.Length == 0) + { + return null; + } + var items = Items; foreach (var e in items) - if (e.GetAttribute("id") == name) return e; + { + if (e.GetAttribute("id") == name) + { + return e; + } + } + foreach (var e in items) - if (e.Namespace == Element.HtmlNamespace && e.GetAttribute("name") == name) return e; + { + if (e.Namespace == Element.HtmlNamespace && e.GetAttribute("name") == name) + { + return e; + } + } + return null; } @@ -254,9 +314,19 @@ public JsArray ValuesArray(global::Jint.Engine engine) // (item/namedItem/length/…), per WebIDL named-property visibility. private bool IsShadowed(JsValue name) { - if (base.GetOwnProperty(name) != PropertyDescriptor.Undefined) return true; + 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; + { + if (p.GetOwnProperty(name) != PropertyDescriptor.Undefined) + { + return true; + } + } + return false; } @@ -270,7 +340,10 @@ public override JsValue Get(JsValue property, JsValue receiver) 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); + if (!IsShadowed(property) && NamedItem(name) is { } named) + { + return _ctx.Wrappers.Wrap(named); + } } return base.Get(property, receiver); } @@ -284,10 +357,14 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) { 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); } @@ -297,8 +374,15 @@ 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; + 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); } @@ -307,7 +391,13 @@ public override List GetOwnPropertyKeys(Types types = Types.String | Ty { 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))); + { + for (var i = 0; i < Items.Count; i++) + { + keys.Add(JintInterop.Str(i.ToString(CultureInfo.InvariantCulture))); + } + } + keys.AddRange(base.GetOwnPropertyKeys(types)); return keys; } @@ -325,33 +415,50 @@ internal sealed class JintDomTokenListObject : ObjectInstance public JintDomTokenListObject(JintBackendContext ctx, DomTokenList tokens) : base(ctx.Engine) { _tokens = tokens; - if (ctx.Wrappers.DomTokenListPrototype is { } p) Prototype = p; + 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]); + 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; + if (property.IsString() && CollectionIndex.TryIndex(property.AsString(), out var i) && i < _tokens.Count) + { + return true; + } + return base.HasProperty(property); } @@ -359,7 +466,13 @@ public override List GetOwnPropertyKeys(Types types = Types.String | Ty { 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))); + { + for (var i = 0; i < _tokens.Count; i++) + { + keys.Add(JintInterop.Str(i.ToString(CultureInfo.InvariantCulture))); + } + } + keys.AddRange(base.GetOwnPropertyKeys(types)); return keys; } @@ -372,10 +485,26 @@ 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; + 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 index 45f97571..bd271db4 100644 --- a/src/Starling.Bindings.Jint/ConsoleBinding.cs +++ b/src/Starling.Bindings.Jint/ConsoleBinding.cs @@ -24,7 +24,11 @@ internal static class ConsoleBinding public static void Install(JintBackendContext ctx) { ArgumentNullException.ThrowIfNull(ctx); - if (ctx.Engine.Global.HasOwnProperty("console")) return; + if (ctx.Engine.Global.HasOwnProperty("console")) + { + return; + } + var log = ctx.Log; Install(ctx.Engine, (level, msg) => { @@ -102,32 +106,50 @@ void Method(string name, ConsoleLevel level) { 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))); + 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))); + if (args.Length > 0) + { + sink(ConsoleLevel.Log, Indent(Format(args))); + } + groupDepth++; return JsValue.Undefined; }, 0); JintInterop.DefineMethod(engine, console, "groupEnd", (_, _) => { - if (groupDepth > 0) groupDepth--; + if (groupDepth > 0) + { + groupDepth--; + } + return JsValue.Undefined; }, 0); @@ -141,7 +163,11 @@ private static string Label(JsValue[] args, string fallback) private static string Format(JsValue[] args) { - if (args.Length == 0) return string.Empty; + 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 index f5a3053b..9b3cc81c 100644 --- a/src/Starling.Bindings.Jint/CookieBinding.cs +++ b/src/Starling.Bindings.Jint/CookieBinding.cs @@ -30,14 +30,21 @@ public static void Install(JintBackendContext ctx) return; } - if (documentProto.HasOwnProperty("cookie")) 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 }); + if (!string.IsNullOrEmpty(raw)) + { + ctx.Cookies.StoreFromHeaders(ctx.BaseUrl, new[] { raw }); + } + return JsValue.Undefined; }); } diff --git a/src/Starling.Bindings.Jint/CoreWebApiBinding.cs b/src/Starling.Bindings.Jint/CoreWebApiBinding.cs index 41df4e91..d19334db 100644 --- a/src/Starling.Bindings.Jint/CoreWebApiBinding.cs +++ b/src/Starling.Bindings.Jint/CoreWebApiBinding.cs @@ -20,30 +20,48 @@ public static void Install(JintBackendContext 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"); + 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), '='); + 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]; + for (var i = 0; i < st.Length; i++) + { + span[i] = (char)st[i]; + } })); } catch (FormatException) @@ -51,13 +69,16 @@ public static void Install(JintBackendContext ctx) 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) @@ -65,33 +86,53 @@ private static JsValue CloneValue(JintBackendContext ctx, JsValue value, Diction var engine = ctx.Engine; if (value is not ObjectInstance obj) { - if (value is JsSymbol) throw DomExceptionBinding.Throw(ctx, "DataCloneError", "Symbol values cannot be cloned"); + 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 (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); + 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; } @@ -107,7 +148,11 @@ private static ObjectInstance CloneTypedArray(JintBackendContext ctx, JsTypedArr private static byte[] ExtractBytes(JsValue v) { - if (v.IsArrayBuffer() && v.AsArrayBuffer() is { } ab) return (byte[])ab.Clone(); + if (v.IsArrayBuffer() && v.AsArrayBuffer() is { } ab) + { + return (byte[])ab.Clone(); + } + if (v is ObjectInstance oi) { var bufVal = oi.Get("buffer"); @@ -132,7 +177,10 @@ private static string TypedArrayName(JsTypedArray ta) if (ctor is ObjectInstance oi) { var name = oi.Get("name"); - if (name.IsString()) return name.ToString(); + if (name.IsString()) + { + return name.ToString(); + } } return "Uint8Array"; } @@ -141,10 +189,16 @@ private static IEnumerable EnumerableStringKeys(ObjectInstance o) { foreach (var key in o.GetOwnPropertyKeys(Types.String)) { - if (!key.IsString()) continue; + if (!key.IsString()) + { + continue; + } + var d = o.GetOwnProperty(key); if (d != global::Jint.Runtime.Descriptors.PropertyDescriptor.Undefined && d.Enumerable) + { yield return key.AsString(); + } } } @@ -152,7 +206,13 @@ 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); + { + 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 index 336ed27f..f077888e 100644 --- a/src/Starling.Bindings.Jint/CryptoBinding.cs +++ b/src/Starling.Bindings.Jint/CryptoBinding.cs @@ -39,25 +39,34 @@ public static void Install(JintBackendContext ctx) 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. @@ -78,7 +87,10 @@ private static string TypedArrayConstructorName(JsTypedArray ta) if (ctor is ObjectInstance oi) { var name = oi.Get("name"); - if (name.IsString()) return name.ToString(); + if (name.IsString()) + { + return name.ToString(); + } } return "TypedArray"; } diff --git a/src/Starling.Bindings.Jint/CssBinding.cs b/src/Starling.Bindings.Jint/CssBinding.cs index 348193bc..a1fdc3b0 100644 --- a/src/Starling.Bindings.Jint/CssBinding.cs +++ b/src/Starling.Bindings.Jint/CssBinding.cs @@ -47,7 +47,9 @@ public static void Install(JintBackendContext ctx) 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") ?? "*"; @@ -55,15 +57,25 @@ public static void Install(JintBackendContext ctx) 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; @@ -148,9 +160,13 @@ private static string CssEscape(string s) 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 index 8797d89c..9bff8ea7 100644 --- a/src/Starling.Bindings.Jint/CssomBinding.cs +++ b/src/Starling.Bindings.Jint/CssomBinding.cs @@ -32,7 +32,10 @@ public static void Install(JintBackendContext ctx) var engine = ctx.Engine; var docProto = ctx.Wrappers.DocumentPrototype; var elProto = ctx.Wrappers.ElementPrototype; - if (docProto is null || elProto is null) return; + if (docProto is null || elProto is null) + { + return; + } // document.styleSheets JintInterop.DefineAccessor(engine, docProto, "styleSheets", (t, _) => @@ -43,14 +46,24 @@ public static void Install(JintBackendContext ctx) // element.sheet (CSSOM §6.5) — only