using AngleSharp.Dom; namespace XFESystem.Browser; public sealed partial class BrowserService { private readonly HashSet executedScripts = new(ReferenceEqualityComparer.Instance); private bool drainingResources; private bool scriptBudgetExhausted; private BrowserDomHost CreateDomHost() => new() { Mutated = InvalidateDocument, FocusChanged = _ => InvalidateDocument(), ResourceRequested = element => { if (scheduledResources.Add(element)) pendingResources.Enqueue(element); }, Measure = node => EnsureLayout().GetBoundingClientRect(node), MeasureRange = (start, startOffset, end, endOffset) => EnsureLayout().GetRangeRects(start,startOffset,end,endOffset), HitTest = (x,y) => EnsureLayout().HitTest(x,y)?.Node, ComputedStyle = (node, property) => EnsureLayout().GetComputedStyle(node)?.GetPropertyValue(property), MeasureText = (text, font) => textMetrics.Measure(text, font), RasterizeText = (text, font) => metricCanvas?.RasterizeWebText(text, font.Size, font.Family, font.Weight) ?? throw new NotSupportedException("The platform did not supply a real Canvas text rasterizer."), Viewport = () => viewport }; private Uri ResourceAddress(string reference) { Uri basis = currentAddress!; string? declared = document?.QuerySelector("base[href]")?.GetAttribute("href"); if (!string.IsNullOrEmpty(declared) && Uri.TryCreate(basis, declared, out var candidate)) { ValidateWebAddress(candidate); basis = candidate; } var address = new Uri(basis, reference); ValidateWebAddress(address); return address; } private bool RunScript(IElement script) { CheckOperationCancellation(); if (scriptBudgetExhausted) return false; if (cspDeclared || !executedScripts.Add(script)) return true; string type = (script.GetAttribute("type") ?? string.Empty).Trim().ToLowerInvariant(); if (type == "module") { Diagnostic("Module scripts are not supported by the classic-script interpreter."); return true; } if (type is not ("" or "text/javascript" or "application/javascript" or "text/ecmascript" or "application/ecmascript")) return true; try { bool external = script.HasAttribute("src"); string source = external ? resourceLoader!.Get(ResourceAddress(script.GetAttribute("src")!), "script").Body : script.TextContent; CheckOperationCancellation(); Trace?.Invoke("XFE_BROWSER_SCRIPT_BEGIN|" + (script.GetAttribute("src") ?? "Inline") + "|Characters|" + source.Length + "\n"); CheckOperationCancellation(); long previousRevision = Revision; dom!.CurrentScript = script; var result = external && resourceLoader!.LastResponsePackaged ? runtime!.EvaluatePackaged(source, ResourceAddress(script.GetAttribute("src")!).AbsoluteUri) : runtime!.Evaluate(source); dom.CurrentScript = null; Trace?.Invoke("XFE_BROWSER_SCRIPT_END|" + (script.GetAttribute("src") ?? "Inline") + "|Success|" + result.Success + "\n"); CheckOperationCancellation(); if (result.ConsoleOutput.Length != 0) Diagnostic(result.ConsoleOutput); if (!result.Success) Diagnostic("JavaScript error: " + result.Error); if (script.HasAttribute("src")) dom!.DispatchEvent(script, result.Success ? "load" : "error", new() { Bubbles = false, Cancelable = false }); scriptBudgetExhausted |= result.BudgetExceeded; // DOM mutation and event callbacks already invalidate their real // changes. Loading a pure library need not re-rasterize the page. if (!documentEnhancementsPending || Revision != previousRevision) InvalidateDocument(); return !result.BudgetExceeded; } catch (OperationCanceledException) when (OperationCancelled) { throw; } catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException) { dom!.CurrentScript = null; Diagnostic("External script failed: " + error.Message); dom!.DispatchEvent(script, "error", new() { Bubbles = false, Cancelable = false }); return true; } } private void LoadStyle(IElement link) { CheckOperationCancellation(); if (cspDeclared || !(link.GetAttribute("rel") ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries).Contains("stylesheet", StringComparer.OrdinalIgnoreCase)) return; try { if (linkedStyles.Count(item => !ReferenceEquals(item.Owner, link)) >= activeLimits.MaximumStyleSheets) throw new BrowserLimitException("The stylesheet count limit was reached."); var response = resourceLoader!.Get(ResourceAddress(link.GetAttribute("href") ?? ""), "style"); CheckOperationCancellation(); Trace?.Invoke("XFE_BROWSER_STYLE|" + response.Address + "|Characters|" + response.Body.Length + "\n"); linkedStyles.RemoveAll(item => ReferenceEquals(item.Owner, link)); linkedStyles.Add((response.Body, link)); InvalidateDocument(); dom?.DispatchEvent(link, "load", new() { Bubbles = false, Cancelable = false }); } catch (OperationCanceledException) when (OperationCancelled) { throw; } catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException) { Diagnostic("Stylesheet failed: " + error.Message); dom?.DispatchEvent(link, "error", new() { Bubbles = false, Cancelable = false }); } } private void DrainResources(int maximum = int.MaxValue) { if (drainingResources || dom is null) return; if (documentEnhancementsPending) maximum = Math.Min(maximum, 1); drainingResources = true; // Explicit success/catch cleanup is needed by the current native runtime. try { int count = 0; while (count < maximum && pendingResources.TryDequeue(out var element)) { CheckOperationCancellation(); scheduledResources.Remove(element); if (++count > activeLimits.MaximumExternalResources) throw new BrowserLimitException("The dynamic resource queue limit was reached."); if (element.Owner != document || !IsConnected(element)) continue; if (element.LocalName == "script") RunScript(element); else if (element.LocalName == "link") LoadStyle(element); } } catch { drainingResources = false; throw; } drainingResources = false; } private static bool IsConnected(INode node) { while (node.Parent is not null) node = node.Parent; return node.NodeType == NodeType.Document; } private void Diagnostic(string message) => pageDiagnostics?.Append(BoundedText.Safe(message, 2_048) + "\n"); }