using AngleSharp.Dom;
namespace XFESystem.Browser;
public sealed partial class BrowserService
{
private readonly HashSet<IElement> 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");
}
using AngleSharp.Dom;
namespace XFESystem.Browser;
public sealed partial class BrowserService
{
private readonly HashSet<IElement> 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");
}