using System.Globalization;
using System.Text;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
namespace XFESystem.Browser;
public sealed partial class BrowserService
{
private string? remoteEvidenceAddress, remoteBodySha256;
private bool remoteCertificateValidated;
private void RecordRemoteResponseEvidence(string? address, string? bodyHash, bool certificateValidated)
{ remoteEvidenceAddress = address; remoteBodySha256 = bodyHash; remoteCertificateValidated = certificateValidated; }
/// <summary>Read actual DOM, loaded resources and existing layout commands on the owning worker.
/// No script, resource load or screenshot enters this readback.</summary>
public string ReadRemotePageProof(long snapshotSequence = 0) => WithGate(() =>
{
if (document is null || displayList is null || currentAddress?.Scheme is not ("http" or "https") ||
remoteEvidenceAddress != currentAddress.AbsoluteUri || string.IsNullOrEmpty(remoteBodySha256)) return "{}";
var visible = new BrowserRect(0, 0, viewport.Width, viewport.Height);
var json = new StringBuilder("{\"Kind\":\"ActualRemotePageDomReadback\",\"Address\":").Append(JavaScriptString(currentAddress.AbsoluteUri))
.Append(",\"Title\":").Append(JavaScriptString(document.Title ?? "")).Append(",\"BodySha256\":").Append(JavaScriptString(remoteBodySha256))
.Append(",\"CertificateValidated\":").Append(remoteCertificateValidated ? "true" : "false")
.Append(",\"SnapshotSequence\":").Append(snapshotSequence)
.Append(",\"DocumentGeneration\":").Append(DocumentGeneration)
.Append(",\"ViewportWidth\":").Append(Number(viewport.Width)).Append(",\"ViewportHeight\":").Append(Number(viewport.Height))
.Append(",\"DocumentHeight\":").Append(Number(displayList.DocumentHeight))
.Append(",\"ScrollY\":").Append(Number(viewport.ScrollY)).Append(",\"TextRuns\":[");
bool comma = false; int count = 0;
foreach (var command in displayList.Commands)
{
if (command.Kind != BrowserDrawKind.Text || command.Opacity <= 0 || string.IsNullOrWhiteSpace(command.Text)) continue;
var bounds = command.ViewportPinned ? command.Bounds : (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Translate(-viewport.ScrollX, -viewport.ScrollY);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"Text\":").Append(JavaScriptString(command.Text.Length > 256 ? command.Text[..256] : command.Text))
.Append(",\"Bounds\":").Append(Rect(bounds)).Append(",\"Color\":").Append(JavaScriptString("#" + command.Color.ToString("X6")))
.Append(",\"Ancestors\":").Append(JavaScriptString(AncestorTags(command.Node)))
.Append(",\"ListItemId\":").Append(ListItemIdentity(command.Node)).Append('}');
if (++count == 256) break;
}
json.Append("],\"SearchInputs\":["); comma = false;
foreach (var element in document.QuerySelectorAll("input[name],textarea[name]").Take(16))
{
if (comma) json.Append(','); comma = true;
json.Append("{\"Name\":").Append(JavaScriptString(element.GetAttribute("name") ?? ""))
.Append(",\"Value\":").Append(JavaScriptString(element.GetAttribute("name") == "q" ? element is IHtmlInputElement { Type: "text" or "search" } input ? input.Value ?? "" : element is IHtmlTextAreaElement area ? area.Value ?? "" : "" : ""))
.Append(",\"Bounds\":").Append(Rect(displayList.GetBoundingClientRect(element))).Append('}');
}
json.Append("],\"LoadedImages\":["); comma = false; count = 0;
foreach (var command in displayList.Commands)
{
if (command.Kind != BrowserDrawKind.Image || command.Opacity <= 0 || FindImage(command.ImageSource) is not { } image) continue;
var bounds = (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Translate(-viewport.ScrollX, -viewport.ScrollY);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
string source = command.ImageSource.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ? "data:image" : ImageKey(command.ImageSource);
json.Append("{\"Source\":").Append(JavaScriptString(source)).Append(",\"Bounds\":").Append(Rect(bounds))
.Append(",\"Format\":").Append(JavaScriptString(imageFormats.GetValueOrDefault(ImageKey(command.ImageSource), "")))
.Append(",\"Width\":").Append(image.Width).Append(",\"Height\":").Append(image.Height)
.Append(",\"Opacity\":").Append(Number(command.Opacity)).Append(",\"Samples\":[");
bool sampleComma = false;
// Source-nearest samples qualify only the existing axis-aligned
// image path; affine images carry geometry/decoding evidence here.
for (int row = 0; command.Transform is null && row < 6; row++) for (int column = 0; column < 8; column++)
{
int x = (int)Math.Floor(bounds.X + (column + .5) * bounds.Width / 8), y = (int)Math.Floor(bounds.Y + (row + .5) * bounds.Height / 6);
if (!visible.Contains(x, y)) continue;
int sx = Math.Clamp((int)((x - bounds.X) * image.Width / bounds.Width), 0, image.Width - 1);
int sy = Math.Clamp((int)((y - bounds.Y) * image.Height / bounds.Height), 0, image.Height - 1);
if (sampleComma) json.Append(','); sampleComma = true;
json.Append("{\"X\":").Append(x).Append(",\"Y\":").Append(y).Append(",\"Argb\":")
.Append(JavaScriptString(image.Pixel(sx, sy).ToString("X8", CultureInfo.InvariantCulture))).Append('}');
}
json.Append("]}"); if (++count == 16) break;
}
json.Append("],\"Links\":["); comma = false; count = 0;
foreach (var element in document.QuerySelectorAll("a[href]"))
{
var bounds = displayList.GetBoundingClientRect(element);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 } || displayList.GetComputedStyle(element)?.Visibility == "hidden" || string.IsNullOrWhiteSpace(element.TextContent)) continue;
if (!Uri.TryCreate(currentAddress, element.GetAttribute("href"), out var target)) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"Address\":").Append(JavaScriptString(target.AbsoluteUri)).Append(",\"Text\":")
.Append(JavaScriptString(element.TextContent[..Math.Min(element.TextContent.Length, 256)]))
.Append(",\"Bounds\":").Append(Rect(bounds)).Append('}');
if (++count == 64) break;
}
json.Append("],\"LayoutSamples\":["); comma = false; count = 0;
foreach (var box in displayList.Boxes)
{
if (box.Node is not IElement element || box.GeneratedPseudo is not null || !box.Visible ||
element.LocalName is not ("cite" or "h2") && box.Transform.IsIdentity) continue;
var bounds = displayList.GetBoundingClientRect(element);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"Tag\":").Append(JavaScriptString(element.LocalName)).Append(",\"Class\":").Append(JavaScriptString(element.ClassName ?? ""))
.Append(",\"Bounds\":").Append(Rect(bounds)).Append(",\"ListItemId\":").Append(ListItemIdentity(element))
.Append(",\"Transform\":[").Append(string.Join(',', new[] { box.Transform.A, box.Transform.B, box.Transform.C, box.Transform.D, box.Transform.E, box.Transform.F }.Select(Number)))
.Append("],\"CssTransform\":").Append(JavaScriptString(box.Style.Get("transform", "none"))).Append('}');
if (++count == 64) break;
}
json.Append("],\"MissingVisibleImages\":["); comma = false; count = 0;
var imageClip = new BrowserRect(viewport.ScrollX, viewport.ScrollY, viewport.Width, viewport.Height); var imageClips = new Stack<BrowserRect>();
var paintedLinks = new HashSet<INode>(ReferenceEqualityComparer.Instance);
foreach (var command in displayList.Commands)
{
if (command.Kind == BrowserDrawKind.PushClip) { imageClips.Push(imageClip); imageClip = imageClip.Intersect(command.Bounds); continue; }
if (command.Kind == BrowserDrawKind.PopClip) { if (imageClips.TryPop(out var prior)) imageClip = prior; continue; }
if (command.Kind == BrowserDrawKind.Text && command.Opacity > 0 && !string.IsNullOrWhiteSpace(command.Text) &&
(command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Intersect(imageClip) is { Width: > 0, Height: > 0 })
for (var element = command.Node as IElement ?? command.Node?.ParentElement; element is not null; element = element.ParentElement)
if (element.LocalName == "a") { paintedLinks.Add(element); break; }
if (command.Kind != BrowserDrawKind.Image || command.Opacity <= 0 || command.ImageSource.Length == 0 || FindImage(command.ImageSource) is not null) continue;
var bounds = (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Intersect(imageClip).Translate(-viewport.ScrollX, -viewport.ScrollY);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"Source\":").Append(JavaScriptString(command.ImageSource.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ? "data:image" : ImageKey(command.ImageSource)))
.Append(",\"Bounds\":").Append(Rect(bounds)).Append('}');
if (++count == 64) break;
}
json.Append("],\"NavigationLinks\":["); comma = false; count = 0;
foreach (var element in document.QuerySelectorAll("nav a[href],[role=navigation] a[href]"))
{
if (string.IsNullOrWhiteSpace(element.TextContent) || HiddenNavigationAncestor(element)) continue;
var menu = element.Closest("nav,[role=navigation]")!;
var menuBounds = displayList.GetBoundingClientRect(menu);
if (menuBounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"MenuId\":").Append(displayList.GetNodeIdentity(menu)).Append(",\"ContainerBounds\":").Append(Rect(menuBounds))
.Append(",\"Text\":").Append(JavaScriptString(element.TextContent.Length > 128 ? element.TextContent[..128] : element.TextContent))
.Append(",\"Bounds\":").Append(Rect(displayList.GetBoundingClientRect(element)))
.Append(",\"Painted\":").Append(paintedLinks.Contains(element) ? "true" : "false").Append('}');
if (++count == 64) break;
}
json.Append("],\"LoadedStyles\":["); comma = false;
foreach (var receipt in LoadedResources.Where(item => item.Kind == "style").Take(128))
{
if (comma) json.Append(','); comma = true;
json.Append("{\"Address\":").Append(JavaScriptString(receipt.Address))
.Append(",\"FinalAddress\":").Append(JavaScriptString(receipt.Address))
.Append(",\"RequestedAddress\":").Append(JavaScriptString(receipt.RequestedAddress ?? receipt.Address))
.Append(",\"Characters\":").Append(receipt.Characters).Append('}');
}
json.Append("],\"DeclaredStyleLinks\":["); comma = false;
foreach (var link in document.QuerySelectorAll("link[rel~=stylesheet][href]").Take(128))
{
if (!Uri.TryCreate(currentAddress, link.GetAttribute("href"), out var uri)) continue;
if (comma) json.Append(','); comma = true; json.Append(JavaScriptString(uri.AbsoluteUri));
}
json.Append("],\"InlineStyleCount\":").Append(document.QuerySelectorAll("style").Length)
.Append(",\"DecodedImageFormats\":[").Append(string.Join(',', imageFormats.Values.Distinct().Select(JavaScriptString))).Append(']')
.Append(",\"DocumentInitializationComplete\":").Append(!documentEnhancementsPending && !awaitingDocumentPresentation ? "true" : "false")
.Append(",\"ScriptBudgetExceeded\":").Append(scriptBudgetExhausted ? "true" : "false")
.Append(",\"PendingResources\":").Append(pendingResources.Count + (dom?.NetworkRequestCount ?? 0) + (documentEnhancementsPending ? Math.Max(1, pendingDocumentScripts.Count) : 0))
.Append(",\"DomBudgetValidations\":").Append(dom?.BudgetValidationCount ?? 0)
.Append(",\"DomBudgetNodeVisits\":").Append(dom?.BudgetNodeVisits ?? 0)
.Append(",\"DeclaredCsp\":").Append(cspDeclared ? "true" : "false")
.Append(",\"AuthorStylesBlockedByCsp\":").Append(cspBlocksAuthorStyles ? "true" : "false")
.Append(",\"BaseHrefCount\":").Append(document.QuerySelectorAll("base[href]").Length)
.Append(",\"Diagnostics\":").Append(JavaScriptString(pageDiagnostics?.ToString() ?? ""))
.Append(",\"ReadOnly\":true,\"NoHostScreenshotSubstitution\":true}");
return json.ToString();
});
private int ListItemIdentity(INode? node)
{
for (var item = node as IElement ?? node?.ParentElement; item is not null; item = item.ParentElement)
if (displayList!.GetComputedStyle(item)?.Display == "list-item")
return displayList.GetNodeIdentity(item);
return 0;
}
private bool HiddenNavigationAncestor(IElement element)
{
for (IElement? item = element; item is not null; item = item.ParentElement)
if (displayList!.GetComputedStyle(item) is { } style && (style.Display == "none" || style.Visibility == "hidden")) return true;
return false;
}
private static string AncestorTags(INode? node)
{
var tags = new StringBuilder(); int count = 0;
for (var element = node as IElement ?? node?.ParentElement; element is not null && count++ < 12; element = element.ParentElement)
{ if (tags.Length > 0) tags.Append('|'); tags.Append(element.LocalName); }
return tags.ToString();
}
private static string Number(double value) => double.IsFinite(value) ? value.ToString("0.###", CultureInfo.InvariantCulture) : "0";
private static string Rect(BrowserRect value) => "{\"X\":" + Number(value.X) + ",\"Y\":" + Number(value.Y) +
",\"Width\":" + Number(value.Width) + ",\"Height\":" + Number(value.Height) + "}";
}
using System.Globalization;
using System.Text;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
namespace XFESystem.Browser;
public sealed partial class BrowserService
{
private string? remoteEvidenceAddress, remoteBodySha256;
private bool remoteCertificateValidated;
private void RecordRemoteResponseEvidence(string? address, string? bodyHash, bool certificateValidated)
{ remoteEvidenceAddress = address; remoteBodySha256 = bodyHash; remoteCertificateValidated = certificateValidated; }
/// <summary>Read actual DOM, loaded resources and existing layout commands on the owning worker.
/// No script, resource load or screenshot enters this readback.</summary>
public string ReadRemotePageProof(long snapshotSequence = 0) => WithGate(() =>
{
if (document is null || displayList is null || currentAddress?.Scheme is not ("http" or "https") ||
remoteEvidenceAddress != currentAddress.AbsoluteUri || string.IsNullOrEmpty(remoteBodySha256)) return "{}";
var visible = new BrowserRect(0, 0, viewport.Width, viewport.Height);
var json = new StringBuilder("{\"Kind\":\"ActualRemotePageDomReadback\",\"Address\":").Append(JavaScriptString(currentAddress.AbsoluteUri))
.Append(",\"Title\":").Append(JavaScriptString(document.Title ?? "")).Append(",\"BodySha256\":").Append(JavaScriptString(remoteBodySha256))
.Append(",\"CertificateValidated\":").Append(remoteCertificateValidated ? "true" : "false")
.Append(",\"SnapshotSequence\":").Append(snapshotSequence)
.Append(",\"DocumentGeneration\":").Append(DocumentGeneration)
.Append(",\"ViewportWidth\":").Append(Number(viewport.Width)).Append(",\"ViewportHeight\":").Append(Number(viewport.Height))
.Append(",\"DocumentHeight\":").Append(Number(displayList.DocumentHeight))
.Append(",\"ScrollY\":").Append(Number(viewport.ScrollY)).Append(",\"TextRuns\":[");
bool comma = false; int count = 0;
foreach (var command in displayList.Commands)
{
if (command.Kind != BrowserDrawKind.Text || command.Opacity <= 0 || string.IsNullOrWhiteSpace(command.Text)) continue;
var bounds = command.ViewportPinned ? command.Bounds : (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Translate(-viewport.ScrollX, -viewport.ScrollY);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"Text\":").Append(JavaScriptString(command.Text.Length > 256 ? command.Text[..256] : command.Text))
.Append(",\"Bounds\":").Append(Rect(bounds)).Append(",\"Color\":").Append(JavaScriptString("#" + command.Color.ToString("X6")))
.Append(",\"Ancestors\":").Append(JavaScriptString(AncestorTags(command.Node)))
.Append(",\"ListItemId\":").Append(ListItemIdentity(command.Node)).Append('}');
if (++count == 256) break;
}
json.Append("],\"SearchInputs\":["); comma = false;
foreach (var element in document.QuerySelectorAll("input[name],textarea[name]").Take(16))
{
if (comma) json.Append(','); comma = true;
json.Append("{\"Name\":").Append(JavaScriptString(element.GetAttribute("name") ?? ""))
.Append(",\"Value\":").Append(JavaScriptString(element.GetAttribute("name") == "q" ? element is IHtmlInputElement { Type: "text" or "search" } input ? input.Value ?? "" : element is IHtmlTextAreaElement area ? area.Value ?? "" : "" : ""))
.Append(",\"Bounds\":").Append(Rect(displayList.GetBoundingClientRect(element))).Append('}');
}
json.Append("],\"LoadedImages\":["); comma = false; count = 0;
foreach (var command in displayList.Commands)
{
if (command.Kind != BrowserDrawKind.Image || command.Opacity <= 0 || FindImage(command.ImageSource) is not { } image) continue;
var bounds = (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Translate(-viewport.ScrollX, -viewport.ScrollY);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
string source = command.ImageSource.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ? "data:image" : ImageKey(command.ImageSource);
json.Append("{\"Source\":").Append(JavaScriptString(source)).Append(",\"Bounds\":").Append(Rect(bounds))
.Append(",\"Format\":").Append(JavaScriptString(imageFormats.GetValueOrDefault(ImageKey(command.ImageSource), "")))
.Append(",\"Width\":").Append(image.Width).Append(",\"Height\":").Append(image.Height)
.Append(",\"Opacity\":").Append(Number(command.Opacity)).Append(",\"Samples\":[");
bool sampleComma = false;
// Source-nearest samples qualify only the existing axis-aligned
// image path; affine images carry geometry/decoding evidence here.
for (int row = 0; command.Transform is null && row < 6; row++) for (int column = 0; column < 8; column++)
{
int x = (int)Math.Floor(bounds.X + (column + .5) * bounds.Width / 8), y = (int)Math.Floor(bounds.Y + (row + .5) * bounds.Height / 6);
if (!visible.Contains(x, y)) continue;
int sx = Math.Clamp((int)((x - bounds.X) * image.Width / bounds.Width), 0, image.Width - 1);
int sy = Math.Clamp((int)((y - bounds.Y) * image.Height / bounds.Height), 0, image.Height - 1);
if (sampleComma) json.Append(','); sampleComma = true;
json.Append("{\"X\":").Append(x).Append(",\"Y\":").Append(y).Append(",\"Argb\":")
.Append(JavaScriptString(image.Pixel(sx, sy).ToString("X8", CultureInfo.InvariantCulture))).Append('}');
}
json.Append("]}"); if (++count == 16) break;
}
json.Append("],\"Links\":["); comma = false; count = 0;
foreach (var element in document.QuerySelectorAll("a[href]"))
{
var bounds = displayList.GetBoundingClientRect(element);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 } || displayList.GetComputedStyle(element)?.Visibility == "hidden" || string.IsNullOrWhiteSpace(element.TextContent)) continue;
if (!Uri.TryCreate(currentAddress, element.GetAttribute("href"), out var target)) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"Address\":").Append(JavaScriptString(target.AbsoluteUri)).Append(",\"Text\":")
.Append(JavaScriptString(element.TextContent[..Math.Min(element.TextContent.Length, 256)]))
.Append(",\"Bounds\":").Append(Rect(bounds)).Append('}');
if (++count == 64) break;
}
json.Append("],\"LayoutSamples\":["); comma = false; count = 0;
foreach (var box in displayList.Boxes)
{
if (box.Node is not IElement element || box.GeneratedPseudo is not null || !box.Visible ||
element.LocalName is not ("cite" or "h2") && box.Transform.IsIdentity) continue;
var bounds = displayList.GetBoundingClientRect(element);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"Tag\":").Append(JavaScriptString(element.LocalName)).Append(",\"Class\":").Append(JavaScriptString(element.ClassName ?? ""))
.Append(",\"Bounds\":").Append(Rect(bounds)).Append(",\"ListItemId\":").Append(ListItemIdentity(element))
.Append(",\"Transform\":[").Append(string.Join(',', new[] { box.Transform.A, box.Transform.B, box.Transform.C, box.Transform.D, box.Transform.E, box.Transform.F }.Select(Number)))
.Append("],\"CssTransform\":").Append(JavaScriptString(box.Style.Get("transform", "none"))).Append('}');
if (++count == 64) break;
}
json.Append("],\"MissingVisibleImages\":["); comma = false; count = 0;
var imageClip = new BrowserRect(viewport.ScrollX, viewport.ScrollY, viewport.Width, viewport.Height); var imageClips = new Stack<BrowserRect>();
var paintedLinks = new HashSet<INode>(ReferenceEqualityComparer.Instance);
foreach (var command in displayList.Commands)
{
if (command.Kind == BrowserDrawKind.PushClip) { imageClips.Push(imageClip); imageClip = imageClip.Intersect(command.Bounds); continue; }
if (command.Kind == BrowserDrawKind.PopClip) { if (imageClips.TryPop(out var prior)) imageClip = prior; continue; }
if (command.Kind == BrowserDrawKind.Text && command.Opacity > 0 && !string.IsNullOrWhiteSpace(command.Text) &&
(command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Intersect(imageClip) is { Width: > 0, Height: > 0 })
for (var element = command.Node as IElement ?? command.Node?.ParentElement; element is not null; element = element.ParentElement)
if (element.LocalName == "a") { paintedLinks.Add(element); break; }
if (command.Kind != BrowserDrawKind.Image || command.Opacity <= 0 || command.ImageSource.Length == 0 || FindImage(command.ImageSource) is not null) continue;
var bounds = (command.Transform ?? BrowserAffineTransform.Identity).Bounds(command.Bounds).Intersect(imageClip).Translate(-viewport.ScrollX, -viewport.ScrollY);
if (bounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"Source\":").Append(JavaScriptString(command.ImageSource.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ? "data:image" : ImageKey(command.ImageSource)))
.Append(",\"Bounds\":").Append(Rect(bounds)).Append('}');
if (++count == 64) break;
}
json.Append("],\"NavigationLinks\":["); comma = false; count = 0;
foreach (var element in document.QuerySelectorAll("nav a[href],[role=navigation] a[href]"))
{
if (string.IsNullOrWhiteSpace(element.TextContent) || HiddenNavigationAncestor(element)) continue;
var menu = element.Closest("nav,[role=navigation]")!;
var menuBounds = displayList.GetBoundingClientRect(menu);
if (menuBounds.Intersect(visible) is not { Width: > 0, Height: > 0 }) continue;
if (comma) json.Append(','); comma = true;
json.Append("{\"MenuId\":").Append(displayList.GetNodeIdentity(menu)).Append(",\"ContainerBounds\":").Append(Rect(menuBounds))
.Append(",\"Text\":").Append(JavaScriptString(element.TextContent.Length > 128 ? element.TextContent[..128] : element.TextContent))
.Append(",\"Bounds\":").Append(Rect(displayList.GetBoundingClientRect(element)))
.Append(",\"Painted\":").Append(paintedLinks.Contains(element) ? "true" : "false").Append('}');
if (++count == 64) break;
}
json.Append("],\"LoadedStyles\":["); comma = false;
foreach (var receipt in LoadedResources.Where(item => item.Kind == "style").Take(128))
{
if (comma) json.Append(','); comma = true;
json.Append("{\"Address\":").Append(JavaScriptString(receipt.Address))
.Append(",\"FinalAddress\":").Append(JavaScriptString(receipt.Address))
.Append(",\"RequestedAddress\":").Append(JavaScriptString(receipt.RequestedAddress ?? receipt.Address))
.Append(",\"Characters\":").Append(receipt.Characters).Append('}');
}
json.Append("],\"DeclaredStyleLinks\":["); comma = false;
foreach (var link in document.QuerySelectorAll("link[rel~=stylesheet][href]").Take(128))
{
if (!Uri.TryCreate(currentAddress, link.GetAttribute("href"), out var uri)) continue;
if (comma) json.Append(','); comma = true; json.Append(JavaScriptString(uri.AbsoluteUri));
}
json.Append("],\"InlineStyleCount\":").Append(document.QuerySelectorAll("style").Length)
.Append(",\"DecodedImageFormats\":[").Append(string.Join(',', imageFormats.Values.Distinct().Select(JavaScriptString))).Append(']')
.Append(",\"DocumentInitializationComplete\":").Append(!documentEnhancementsPending && !awaitingDocumentPresentation ? "true" : "false")
.Append(",\"ScriptBudgetExceeded\":").Append(scriptBudgetExhausted ? "true" : "false")
.Append(",\"PendingResources\":").Append(pendingResources.Count + (dom?.NetworkRequestCount ?? 0) + (documentEnhancementsPending ? Math.Max(1, pendingDocumentScripts.Count) : 0))
.Append(",\"DomBudgetValidations\":").Append(dom?.BudgetValidationCount ?? 0)
.Append(",\"DomBudgetNodeVisits\":").Append(dom?.BudgetNodeVisits ?? 0)
.Append(",\"DeclaredCsp\":").Append(cspDeclared ? "true" : "false")
.Append(",\"AuthorStylesBlockedByCsp\":").Append(cspBlocksAuthorStyles ? "true" : "false")
.Append(",\"BaseHrefCount\":").Append(document.QuerySelectorAll("base[href]").Length)
.Append(",\"Diagnostics\":").Append(JavaScriptString(pageDiagnostics?.ToString() ?? ""))
.Append(",\"ReadOnly\":true,\"NoHostScreenshotSubstitution\":true}");
return json.ToString();
});
private int ListItemIdentity(INode? node)
{
for (var item = node as IElement ?? node?.ParentElement; item is not null; item = item.ParentElement)
if (displayList!.GetComputedStyle(item)?.Display == "list-item")
return displayList.GetNodeIdentity(item);
return 0;
}
private bool HiddenNavigationAncestor(IElement element)
{
for (IElement? item = element; item is not null; item = item.ParentElement)
if (displayList!.GetComputedStyle(item) is { } style && (style.Display == "none" || style.Visibility == "hidden")) return true;
return false;
}
private static string AncestorTags(INode? node)
{
var tags = new StringBuilder(); int count = 0;
for (var element = node as IElement ?? node?.ParentElement; element is not null && count++ < 12; element = element.ParentElement)
{ if (tags.Length > 0) tags.Append('|'); tags.Append(element.LocalName); }
return tags.ToString();
}
private static string Number(double value) => double.IsFinite(value) ? value.ToString("0.###", CultureInfo.InvariantCulture) : "0";
private static string Rect(BrowserRect value) => "{\"X\":" + Number(value.X) + ",\"Y\":" + Number(value.Y) +
",\"Width\":" + Number(value.Width) + ",\"Height\":" + Number(value.Height) + "}";
}