using AngleSharp.Dom;
using Jint;
using Jint.Native;
using Jint.Runtime;
namespace XFESystem.Browser;
public sealed partial class DomBridge
{
private static AdjacentPosition AdjacentPositionOf(string position) => position.ToLowerInvariant() switch
{
"beforebegin" => AdjacentPosition.BeforeBegin,
"afterbegin" => AdjacentPosition.AfterBegin,
"beforeend" => AdjacentPosition.BeforeEnd,
"afterend" => AdjacentPosition.AfterEnd,
_ => throw new ArgumentException("Invalid adjacent insertion position.")
};
private AdjacentPosition ParseAdjacentPosition(string position)
{
try { return AdjacentPositionOf(position); }
catch (ArgumentException) { throw AdjacentError("SyntaxError", "Invalid adjacent insertion position.", 12); }
}
private static (INode? Parent, INode? Before) AdjacentDestination(IElement element, AdjacentPosition position) => position switch
{
AdjacentPosition.BeforeBegin => (element.Parent, element),
AdjacentPosition.AfterBegin => (element, element.FirstChild),
AdjacentPosition.BeforeEnd => (element, null),
AdjacentPosition.AfterEnd => (element.Parent, element.NextSibling),
_ => throw new ArgumentOutOfRangeException(nameof(position))
};
private void InsertAdjacentHtml(IElement element, string position, JsValue value)
{
var where = ParseAdjacentPosition(position);
var (parent, before) = AdjacentDestination(element, where);
if (parent is null || parent.NodeType == NodeType.Document)
throw AdjacentError("NoModificationAllowedError", "HTML insertion requires a parent other than Document.", 7);
// HTML fragment parsing uses the actual surrounding element (table/select/SVG included).
// The synthetic body context is required when the insertion context is an HTML html node.
var context = parent as IElement;
if (context is null || context.LocalName == "html" && context.NamespaceUri == "http://www.w3.org/1999/xhtml")
context = document.CreateElement("body");
var markup = LimitedText(value);
var fragment = document.CreateDocumentFragment();
// AngleSharp 1.7.1 HtmlParser.ParseFragment recreates its context as an HTML
// element, losing SVG/MathML namespaces. Element.Insert's fragment parser uses
// the actual context, so parse into an unobserved shallow clone first.
var staging = (IElement)context.Clone(false);
staging.Insert(AdjacentPosition.BeforeEnd, markup);
foreach (var node in staging.ChildNodes.ToArray()) fragment.AppendChild(node);
var added = fragment.ChildNodes.ToArray();
if (added.Length == 0) return;
// Budget the complete fragment before changing the live tree. A rejected insertion
// cannot leave an early sibling behind, change existing identities, or queue resources.
ValidateAdjacentInsertion(parent, added);
parent.InsertBefore(fragment, before);
Changed(parent, "childList", added: added);
// Parsed scripts remain inert, matching innerHTML. Stylesheet links are page-owned work.
foreach (var node in added)
if (Connected(node))
foreach (var link in SelfAndDescendants(node).Where(child => child.LocalName == "link"))
host.ResourceRequested?.Invoke(link);
}
private void InsertAdjacentText(IElement element, string position, JsValue value)
{
var (parent, before) = AdjacentDestination(element, ParseAdjacentPosition(position));
var text = LimitedText(value);
if (parent is null) return;
var node = document.CreateTextNode(text);
ValidateAdjacentInsertion(parent, [node]);
Insert(parent, node, before);
}
private JsValue InsertAdjacentElement(IElement element, string position, JsValue value)
{
var where = ParseAdjacentPosition(position);
var child = value.IsObject() && nodes.TryGetValue(value.AsObject(), out var known) ? known : null;
if (child is not IElement) throw AdjacentError("TypeError", "insertAdjacentElement requires an Element.", 0);
var (parent, before) = AdjacentDestination(element, where);
// Unlike HTML insertion, these methods use insert-adjacent's null-parent return.
if (parent is null) return JsValue.Null;
return Insert(parent, child, before);
}
private void ValidateAdjacentInsertion(INode parent, INode[] added)
{
ValidateBudget(added);
var parentDepth = 0;
for (var ancestor = parent.Parent; ancestor is not null; ancestor = ancestor.Parent) ++parentDepth;
var pending = new Stack<(INode Node, int Depth)>();
foreach (var node in added) pending.Push((node, parentDepth + 1));
while (pending.TryPop(out var entry))
{
if (entry.Depth > limits.MaximumDomDepth) throw new BrowserLimitException("The insertion exceeds the DOM depth limit.");
foreach (var node in entry.Node.ChildNodes) pending.Push((node, entry.Depth + 1));
}
var projected = added.Prepend(parent).Distinct<INode>(ReferenceEqualityComparer.Instance).ToArray();
if (wrappers.Count + projected.Count(node => !wrappers.ContainsKey(node)) > limits.MaximumDomNodes ||
identities.Count + projected.Count(node => !identities.ContainsKey(node)) > limits.MaximumDomNodes)
throw new BrowserLimitException("The insertion exceeds the DOM wrapper or identity limit.");
}
private JavaScriptException AdjacentError(string name, string message, int code)
{
var value = new BrowserHostObject(engine);
Data(value, "name", name); Data(value, "message", message); Data(value, "code", code);
return new JavaScriptException(value);
}
}
using AngleSharp.Dom;
using Jint;
using Jint.Native;
using Jint.Runtime;
namespace XFESystem.Browser;
public sealed partial class DomBridge
{
private static AdjacentPosition AdjacentPositionOf(string position) => position.ToLowerInvariant() switch
{
"beforebegin" => AdjacentPosition.BeforeBegin,
"afterbegin" => AdjacentPosition.AfterBegin,
"beforeend" => AdjacentPosition.BeforeEnd,
"afterend" => AdjacentPosition.AfterEnd,
_ => throw new ArgumentException("Invalid adjacent insertion position.")
};
private AdjacentPosition ParseAdjacentPosition(string position)
{
try { return AdjacentPositionOf(position); }
catch (ArgumentException) { throw AdjacentError("SyntaxError", "Invalid adjacent insertion position.", 12); }
}
private static (INode? Parent, INode? Before) AdjacentDestination(IElement element, AdjacentPosition position) => position switch
{
AdjacentPosition.BeforeBegin => (element.Parent, element),
AdjacentPosition.AfterBegin => (element, element.FirstChild),
AdjacentPosition.BeforeEnd => (element, null),
AdjacentPosition.AfterEnd => (element.Parent, element.NextSibling),
_ => throw new ArgumentOutOfRangeException(nameof(position))
};
private void InsertAdjacentHtml(IElement element, string position, JsValue value)
{
var where = ParseAdjacentPosition(position);
var (parent, before) = AdjacentDestination(element, where);
if (parent is null || parent.NodeType == NodeType.Document)
throw AdjacentError("NoModificationAllowedError", "HTML insertion requires a parent other than Document.", 7);
// HTML fragment parsing uses the actual surrounding element (table/select/SVG included).
// The synthetic body context is required when the insertion context is an HTML html node.
var context = parent as IElement;
if (context is null || context.LocalName == "html" && context.NamespaceUri == "http://www.w3.org/1999/xhtml")
context = document.CreateElement("body");
var markup = LimitedText(value);
var fragment = document.CreateDocumentFragment();
// AngleSharp 1.7.1 HtmlParser.ParseFragment recreates its context as an HTML
// element, losing SVG/MathML namespaces. Element.Insert's fragment parser uses
// the actual context, so parse into an unobserved shallow clone first.
var staging = (IElement)context.Clone(false);
staging.Insert(AdjacentPosition.BeforeEnd, markup);
foreach (var node in staging.ChildNodes.ToArray()) fragment.AppendChild(node);
var added = fragment.ChildNodes.ToArray();
if (added.Length == 0) return;
// Budget the complete fragment before changing the live tree. A rejected insertion
// cannot leave an early sibling behind, change existing identities, or queue resources.
ValidateAdjacentInsertion(parent, added);
parent.InsertBefore(fragment, before);
Changed(parent, "childList", added: added);
// Parsed scripts remain inert, matching innerHTML. Stylesheet links are page-owned work.
foreach (var node in added)
if (Connected(node))
foreach (var link in SelfAndDescendants(node).Where(child => child.LocalName == "link"))
host.ResourceRequested?.Invoke(link);
}
private void InsertAdjacentText(IElement element, string position, JsValue value)
{
var (parent, before) = AdjacentDestination(element, ParseAdjacentPosition(position));
var text = LimitedText(value);
if (parent is null) return;
var node = document.CreateTextNode(text);
ValidateAdjacentInsertion(parent, [node]);
Insert(parent, node, before);
}
private JsValue InsertAdjacentElement(IElement element, string position, JsValue value)
{
var where = ParseAdjacentPosition(position);
var child = value.IsObject() && nodes.TryGetValue(value.AsObject(), out var known) ? known : null;
if (child is not IElement) throw AdjacentError("TypeError", "insertAdjacentElement requires an Element.", 0);
var (parent, before) = AdjacentDestination(element, where);
// Unlike HTML insertion, these methods use insert-adjacent's null-parent return.
if (parent is null) return JsValue.Null;
return Insert(parent, child, before);
}
private void ValidateAdjacentInsertion(INode parent, INode[] added)
{
ValidateBudget(added);
var parentDepth = 0;
for (var ancestor = parent.Parent; ancestor is not null; ancestor = ancestor.Parent) ++parentDepth;
var pending = new Stack<(INode Node, int Depth)>();
foreach (var node in added) pending.Push((node, parentDepth + 1));
while (pending.TryPop(out var entry))
{
if (entry.Depth > limits.MaximumDomDepth) throw new BrowserLimitException("The insertion exceeds the DOM depth limit.");
foreach (var node in entry.Node.ChildNodes) pending.Push((node, entry.Depth + 1));
}
var projected = added.Prepend(parent).Distinct<INode>(ReferenceEqualityComparer.Instance).ToArray();
if (wrappers.Count + projected.Count(node => !wrappers.ContainsKey(node)) > limits.MaximumDomNodes ||
identities.Count + projected.Count(node => !identities.ContainsKey(node)) > limits.MaximumDomNodes)
throw new BrowserLimitException("The insertion exceeds the DOM wrapper or identity limit.");
}
private JavaScriptException AdjacentError(string name, string message, int code)
{
var value = new BrowserHostObject(engine);
Data(value, "name", name); Data(value, "message", message); Data(value, "code", code);
return new JavaScriptException(value);
}
}