using System.Globalization;
using AngleSharp.Dom;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Interop;
namespace XFESystem.Browser;
public sealed partial class DomBridge
{
private JsValue Wrap(INode? node)
{
if (node is null) return JsValue.Null;
if (wrappers.TryGetValue(node, out var known)) return known;
if (node is AngleSharp.Html.Dom.IHtmlInputElement or AngleSharp.Html.Dom.IHtmlTextAreaElement) characterBudgetValid = false;
if (wrappers.Count >= limits.MaximumDomNodes) throw new BrowserLimitException("The DOM wrapper limit was reached.");
NodeIdentity(node); var wrapper = new BrowserHostObject(engine); wrappers.Add(node, wrapper); nodes.Add(wrapper, node);
CallInternal(bootstrap.Get("init"), [wrapper, (int)node.NodeType, (node as IElement)?.LocalName ?? ""]); Data(wrapper, "__nodeId", identities[node].Id);
Property(wrapper, "nodeType", () => (int)node.NodeType); Property(wrapper, "nodeName", () => node.NodeName);
Property(wrapper, "nodeValue", () => Text(node.NodeValue), value => { if (node.NodeType is NodeType.Text or NodeType.Comment) MutateText(node, value); });
Property(wrapper, "textContent", () => Text(node.TextContent), value => MutateText(node, value));
if (node is IText text)
{
Property(wrapper, "data", () => Text(text.TextContent), value => MutateText(text, value));
Property(wrapper, "length", () => text.TextContent.Length);
Function(wrapper, "splitText", args => SplitTextNode(text, Value(args, 0)));
}
Property(wrapper, "parentNode", () => Wrap(node.Parent)); Property(wrapper, "parentElement", () => Wrap(node.ParentElement));
Property(wrapper, "ownerDocument", () => node is IDocument ? JsValue.Null : Wrap(node.Owner));
Property(wrapper, "firstChild", () => Wrap(node.FirstChild)); Property(wrapper, "lastChild", () => Wrap(node.LastChild));
Property(wrapper, "nextSibling", () => Wrap(node.NextSibling)); Property(wrapper, "previousSibling", () => Wrap(node.PreviousSibling));
var childNodes = LiveList(() => node.ChildNodes);
Property(wrapper, "childNodes", () => childNodes); Property(wrapper, "isConnected", () => Connected(node));
Function(wrapper, "append", args => { foreach (var value in args) Insert(node, NodeOrText(value), null); return JsValue.Undefined; });
Function(wrapper, "prepend", args => { var before = node.FirstChild; foreach (var value in args) Insert(node, NodeOrText(value), before); return JsValue.Undefined; });
if (node is IParentNode) Function(wrapper, "replaceChildren", args => { ReplaceChildren(node, args); return JsValue.Undefined; });
if (node.NodeType is NodeType.Element or NodeType.Text or NodeType.Comment)
{
Function(wrapper, "before", args => { InsertSiblings(node, args, before: true, replace: false); return JsValue.Undefined; });
Function(wrapper, "after", args => { InsertSiblings(node, args, before: false, replace: false); return JsValue.Undefined; });
Function(wrapper, "replaceWith", args => { InsertSiblings(node, args, before: false, replace: true); return JsValue.Undefined; });
}
Function(wrapper, "remove", _ => { var parent = node.Parent; if (parent is not null) { parent.RemoveChild(node); Changed(parent, "childList", removed: [node]); } return JsValue.Undefined; });
Function(wrapper, "cloneNode", args => CloneDomNode(node, Bool(Value(args, 0))));
Function(wrapper, "contains", args => { var other = OptionalNode(Value(args, 0)); return other is not null && Ancestor(node, other); });
Function(wrapper, "hasChildNodes", _ => node.ChildNodes.Length != 0);
if (node is IParentNode parentNode)
{
var children = LiveList(() => parentNode.Children, "HTMLCollection");
Property(wrapper, "children", () => children); Property(wrapper, "childElementCount", () => parentNode.Children.Length);
Property(wrapper, "firstElementChild", () => Wrap(parentNode.Children.FirstOrDefault())); Property(wrapper, "lastElementChild", () => Wrap(parentNode.Children.LastOrDefault()));
Function(wrapper, "querySelector", args => Wrap(parentNode.QuerySelector(Argument(args, 0)))); Function(wrapper, "querySelectorAll", args => List(parentNode.QuerySelectorAll(Argument(args, 0))));
Function(wrapper, "getElementsByTagName", args => { var tag = Argument(args, 0); return LiveList(() => Descendants(node).Where(element => tag == "*" || element.LocalName.Equals(tag, StringComparison.OrdinalIgnoreCase)), "HTMLCollection"); });
Function(wrapper, "getElementsByClassName", args => { var tokens = Argument(args, 0).Split(' ', StringSplitOptions.RemoveEmptyEntries); return LiveList(() => Descendants(node).Where(element => tokens.All(element.ClassList.Contains)), "HTMLCollection"); });
}
if (node is IElement element) InstallElement(wrapper, element);
if (node is IDocument owner) InstallDocument(wrapper, owner);
if (node is IElement custom && custom.LocalName.Contains('-')) CallInternal(bootstrap.Get("upgrade"), [wrapper]);
return wrapper;
}
private void InstallNodeMutationPrototypes()
{
var prototype = engine.GetValue("Node").AsObject().Get("prototype").AsObject();
void Method(string name, Func<INode, JsValue[], JsValue> action) => prototype.DefineOwnProperty(name,
new PropertyDescriptor(new ClrFunction(engine, name, (receiver, args) =>
{
if (!receiver.IsObject() || !nodes.TryGetValue(receiver.AsObject(), out var parent))
throw AdjacentError("TypeError", "The receiver must be a node from this page.", 0);
return action(parent, args);
}), true, true, true));
Method("appendChild", (parent, args) => Insert(parent, RequireNode(Value(args, 0)), null));
Method("insertBefore", (parent, args) => Insert(parent, RequireNode(Value(args, 0)), OptionalNode(Value(args, 1))));
Method("removeChild", (parent, args) =>
{
var child = RequireNode(Value(args, 0));
if (child.Parent != parent) throw new ArgumentException("The node is not a child of this parent.");
parent.RemoveChild(child); Changed(parent, "childList", removed: [child]); return Wrap(child);
});
Method("replaceChild", (parent, args) =>
{
var child = RequireNode(Value(args, 0)); var old = RequireNode(Value(args, 1));
if (old.Parent != parent) throw new ArgumentException("The node is not a child of this parent.");
Insert(parent, child, old); parent.RemoveChild(old); Changed(parent, "childList", removed: [old]); return Wrap(old);
});
}
private void ReplaceChildren(INode parent, JsValue[] arguments)
{
var replacements = arguments.Select(NodeOrText).ToArray();
foreach (var child in replacements) if (Ancestor(child, parent)) throw AdjacentError("HierarchyRequestError", "A DOM node cannot contain itself.", 3);
var fragment = document.CreateDocumentFragment();
foreach (var child in replacements) Insert(fragment, child, null);
var removed = parent.ChildNodes.ToArray();
foreach (var child in removed) parent.RemoveChild(child);
if (removed.Length != 0) Changed(parent, "childList", removed: removed);
Insert(parent, fragment, null);
}
private void InsertSiblings(INode node, JsValue[] arguments, bool before, bool replace)
{
var parent = node.Parent;
if (parent is null) return;
var replacements = arguments.Select(NodeOrText).ToArray();
foreach (var child in replacements) if (Ancestor(child, parent)) throw AdjacentError("HierarchyRequestError", "A DOM node cannot contain itself.", 3);
// Determine a viable anchor before conversion moves any existing sibling
// into the temporary fragment (including the receiver itself).
INode? anchor = before ? node.PreviousSibling : node.NextSibling;
while (anchor is not null && replacements.Contains(anchor, ReferenceEqualityComparer.Instance))
anchor = before ? anchor.PreviousSibling : anchor.NextSibling;
var fragment = document.CreateDocumentFragment();
foreach (var child in replacements) Insert(fragment, child, null);
if (before) anchor = anchor is null ? parent.FirstChild : anchor.NextSibling;
if (replace && ReferenceEquals(node.Parent, parent))
{
Insert(parent, fragment, node);
parent.RemoveChild(node); Changed(parent, "childList", removed: [node]);
}
else Insert(parent, fragment, anchor);
}
private void InstallElement(ObjectInstance wrapper, IElement element)
{
if (element.LocalName == "form")
{
Function(wrapper, "submit", _ => { host.SubmitForm?.Invoke(element, null); return JsValue.Undefined; });
Function(wrapper, "requestSubmit", args =>
{
var submitter = Value(args, 0).IsUndefined() ? null : RequireNode(Value(args, 0)) as IElement;
if (submitter is not null && (!ReferenceEquals(submitter.Closest("form"), element) || submitter.LocalName is not ("input" or "button") ||
(submitter.GetAttribute("type") ?? (submitter.LocalName == "button" ? "submit" : "text")) is not ("submit" or "image")))
throw AdjacentError("NotFoundError", "The submitter is not a submit control of this form.", 8);
var init = new BrowserHostObject(engine); Data(init, "bubbles", true); Data(init, "cancelable", true);
var evt = CallInternal(bootstrap.Get("event"), ["submit", init]); Data(evt.AsObject(), "submitter", Wrap(submitter));
if (CallInternal(bootstrap.Get("dispatch"), [wrapper, evt]).AsBoolean()) host.SubmitForm?.Invoke(element, submitter);
return JsValue.Undefined;
});
}
Property(wrapper, "innerHTML", () => element.InnerHtml, value => MutateHtml(element, value)); Property(wrapper, "outerHTML", () => element.OuterHtml);
if (element.NamespaceUri == "http://www.w3.org/1999/xhtml")
Property(wrapper, "innerText", () => InnerTextValue(element), value => MutateInnerText(element, value));
Property(wrapper, "tagName", () => element.TagName); Property(wrapper, "localName", () => element.LocalName); Property(wrapper, "namespaceURI", () => Text(element.NamespaceUri));
Property(wrapper, "id", () => element.Id ?? "", value => SetAttribute(element, "id", value)); Property(wrapper, "className", () => element.ClassName ?? "", value => SetAttribute(element, "class", value));
foreach (var name in new[] { "type", "name", "title", "placeholder", "tabIndex", "contentEditable", "rel", "media", "target", "download", "dir", "lang" })
{ var attribute = name.ToLowerInvariant(); Property(wrapper, name, () => element.GetAttribute(attribute) ?? string.Empty, value => SetAttribute(element, attribute, value)); }
foreach (var name in new[] { "src", "href" }) Property(wrapper, name, () => ResolveUrl(element.GetAttribute(name)), value => SetAttribute(element, name, value));
if (element.NamespaceUri == "http://www.w3.org/1999/xhtml" && element.LocalName is "a" or "area") InstallHyperlink(wrapper, element);
Property(wrapper, "nonce", () => element.GetAttribute("nonce") ?? "", value => SetAttribute(element, "nonce", value));
foreach (string attribute in new[] { "rel", "type", "name", "target", "title", "placeholder", "crossorigin" })
Property(wrapper, attribute, () => element.GetAttribute(attribute) ?? "", value => SetAttribute(element, attribute, value));
if (element.LocalName == "script") Property(wrapper, "text", () => element.TextContent, value => MutateText(element, value));
if (element.LocalName == "img")
{
Property(wrapper, "naturalWidth", () => host.ImageInfo?.Invoke(element).Width ?? 0);
Property(wrapper, "naturalHeight", () => host.ImageInfo?.Invoke(element).Height ?? 0);
Property(wrapper, "complete", () => host.ImageInfo?.Invoke(element).Complete ?? false);
Function(wrapper, "__decodeImage", _ => { host.ResourceRequested?.Invoke(element); return JsValue.Undefined; });
}
foreach (var name in new[] { "disabled", "checked", "readOnly", "hidden", "multiple", "selected" })
{ var attribute = name.ToLowerInvariant(); Property(wrapper, name, () => element.HasAttribute(attribute), value => { if (Bool(value)) SetAttribute(element, attribute, string.Empty); else RemoveAttribute(element, attribute); }); }
Property(wrapper, "nextElementSibling", () => Wrap(element.NextElementSibling)); Property(wrapper, "previousElementSibling", () => Wrap(element.PreviousElementSibling));
Property(wrapper, "attributes", () => Attributes(element));
Function(wrapper, "getAttribute", args => Text(element.GetAttribute(ShortArgument(args, 0)))); Function(wrapper, "hasAttribute", args => element.HasAttribute(ShortArgument(args, 0)));
Function(wrapper, "getAttributeNode", args => AttributeNode(element, ShortArgument(args, 0)));
Function(wrapper, "setAttribute", args => { SetAttribute(element, ShortArgument(args, 0), Value(args, 1)); return JsValue.Undefined; });
Function(wrapper, "setAttributeNS", args => { SetAttribute(element, ShortArgument(args, 1), Value(args, 2)); return JsValue.Undefined; });
Function(wrapper, "removeAttribute", args => { RemoveAttribute(element, ShortArgument(args, 0)); return JsValue.Undefined; });
Function(wrapper, "getAttributeNS", args => Text(element.GetAttribute(ShortArgument(args, 1)))); Function(wrapper, "hasAttributeNS", args => element.HasAttribute(ShortArgument(args, 1)));
Function(wrapper, "removeAttributeNS", args => { RemoveAttribute(element, ShortArgument(args, 1)); return JsValue.Undefined; });
Function(wrapper, "matches", args => element.Matches(Argument(args, 0)));
Function(wrapper, "insertAdjacentHTML", args => { InsertAdjacentHtml(element, ShortArgument(args, 0), Value(args, 1)); return JsValue.Undefined; });
Function(wrapper, "insertAdjacentText", args => { InsertAdjacentText(element, ShortArgument(args, 0), Value(args, 1)); return JsValue.Undefined; });
Function(wrapper, "insertAdjacentElement", args => InsertAdjacentElement(element, ShortArgument(args, 0), Value(args, 1)));
Function(wrapper, "closest", args => { var selector = Argument(args, 0); for (var p = element; p is not null; p = p.ParentElement) if (p.Matches(selector)) return Wrap(p); return JsValue.Null; });
Function(wrapper, "__styleGet", args => StyleGet(element, ShortArgument(args, 0)));
Function(wrapper, "__styleSet", args => { StyleSet(element, ShortArgument(args, 0), Argument(args, 1), Argument(args, 2)); return JsValue.Undefined; });
Function(wrapper, "__computed", args => host.ComputedStyle?.Invoke(element, ShortArgument(args, 0)) ?? StyleGet(element, ShortArgument(args, 0)));
Function(wrapper, "getBoundingClientRect", _ => Rectangle(element)); Function(wrapper, "getClientRects", _ => ListValues([Rectangle(element)]));
foreach (var name in new[] { "clientWidth", "offsetWidth", "scrollWidth", "clientHeight", "offsetHeight", "scrollHeight", "offsetLeft", "offsetTop" })
Property(wrapper, name, () => { var rect = host.Measure?.Invoke(element) ?? default; return name.EndsWith("Width", StringComparison.Ordinal) ? rect.Width : name.EndsWith("Height", StringComparison.Ordinal) ? rect.Height : name == "offsetLeft" ? rect.X : rect.Y; });
Data(wrapper, "scrollLeft", 0, true); Data(wrapper, "scrollTop", 0, true);
InstallForm(wrapper, element);
if (element.LocalName == "canvas") InstallCanvas(wrapper, element);
Function(wrapper, "focus", _ => { Focus(element); return JsValue.Undefined; }); Function(wrapper, "blur", _ => { if (focused == element) Focus(null); return JsValue.Undefined; });
Function(wrapper, "click", _ => DispatchInternal(element, "click", true, true));
Function(wrapper, "setPointerCapture", _ => JsValue.Undefined); Function(wrapper, "releasePointerCapture", _ => JsValue.Undefined); Function(wrapper, "hasPointerCapture", _ => false);
}
private void InstallDocument(ObjectInstance projected, IDocument owner)
{
Property(projected, "contentType", () => parsedDocuments.GetValueOrDefault(owner, "text/html"));
Function(projected, "getElementById", args => Wrap(owner.GetElementById(Argument(args, 0))));
Function(projected, "createElement", args => Created(owner.CreateElement(ShortArgument(args, 0)), true));
Function(projected, "createElementNS", args => Created(owner.CreateElement(Argument(args, 0), ShortArgument(args, 1)), true));
Function(projected, "createTextNode", args => Created(owner.CreateTextNode(Argument(args, 0)))); Function(projected, "createComment", args => Created(owner.CreateComment(Argument(args, 0))));
Function(projected, "createDocumentFragment", _ => Created(owner.CreateDocumentFragment())); Function(projected, "createEvent", _ => CallInternal(bootstrap.Get("event"), [string.Empty, new BrowserHostObject(engine)]));
Function(projected, "createRange", _ => WrapRange(owner.CreateRange()));
Function(projected, "createTreeWalker", args => CreateTreeWalker(owner, args));
Function(projected, "getSelection", _ => DocumentSelection());
Function(projected, "caretRangeFromPoint", args => CaretFromPoint(Number(Value(args, 0)), Number(Value(args, 1)), false));
Function(projected, "caretPositionFromPoint", args => CaretFromPoint(Number(Value(args, 0)), Number(Value(args, 1)), true));
Function(projected, "queryCommandSupported", _ => false); Function(projected, "execCommand", _ => false);
Property(projected, "body", () => Wrap(owner.Body)); Property(projected, "head", () => Wrap(owner.Head)); Property(projected, "documentElement", () => Wrap(owner.DocumentElement ?? owner.ChildNodes.OfType<IElement>().FirstOrDefault()));
Property(projected, "activeElement", () => Wrap(owner == document ? focused ?? owner.Body : owner.Body)); Property(projected, "defaultView", () => owner == document ? engine.Global : JsValue.Null); Property(projected, "readyState", () => owner == document ? readyState : "complete");
Property(projected, "title", () => owner.Title, value => SetTitle(owner, value)); Property(projected, "URL", () => address.AbsoluteUri); Property(projected, "baseURI", () => address.AbsoluteUri);
Property(projected, "location", () => owner == document ? engine.GetValue("location") : JsValue.Null, value => { if (owner == document) host.Navigate?.Invoke(LimitedText(value)); });
Property(projected, "compatMode", () => "CSS1Compat"); Property(projected, "characterSet", () => "UTF-8"); Property(projected, "currentScript", () => Wrap(owner == document ? CurrentScript : null));
Property(projected, "cookie", () => owner == document ? host.ReadCookies?.Invoke() ?? "" : "", value => { if (owner == document) host.WriteCookie?.Invoke(LimitedText(value)); });
Property(projected, "visibilityState", () => "visible"); Property(projected, "hidden", () => false);
Property(projected, "images", () => LiveList(() => owner.QuerySelectorAll("img"), "HTMLCollection"));
Property(projected, "scripts", () => LiveList(() => owner.QuerySelectorAll("script"), "HTMLCollection"));
Property(projected, "forms", () => LiveList(() => owner.QuerySelectorAll("form"), "HTMLCollection"));
Property(projected, "links", () => LiveList(() => owner.QuerySelectorAll("a[href],area[href]"), "HTMLCollection"));
}
private void InstallWindow()
{
Function(engine.Global, "getSelection", _ => DocumentSelection());
foreach (string name in new[] { "onload", "onunload", "onbeforeunload", "onerror", "onresize", "onscroll", "onfocus", "onblur", "onpageshow", "onpagehide", "onpopstate", "onhashchange", "onmessage", "onunhandledrejection" })
Data(engine.Global, name, JsValue.Null, true);
var navigator = new BrowserHostObject(engine); Data(navigator, "userAgent", BrowserIdentity.UserAgent); Data(navigator, "platform", "XFESystem");
Data(navigator, "language", "zh-CN"); Data(navigator, "languages", ListValues(["zh-CN", "en"])); Data(navigator, "hardwareConcurrency", 1); Data(navigator, "onLine", true); Data(navigator, "maxTouchPoints", 0); engine.SetValue("navigator", navigator);
var location = new BrowserHostObject(engine); Property(location, "href", () => address.AbsoluteUri, value => host.Navigate?.Invoke(LimitedText(value))); Data(location, "protocol", address.Scheme + ":"); Data(location, "host", address.Authority);
Data(location, "hostname", address.Host); Data(location, "port", address.IsDefaultPort ? string.Empty : address.Port.ToString(CultureInfo.InvariantCulture));
Data(location, "pathname", address.AbsolutePath); Data(location, "search", address.Query); Data(location, "hash", address.Fragment);
Data(location, "origin", address.IsAbsoluteUri && address.Scheme is "http" or "https" ? address.GetLeftPart(UriPartial.Authority) : "null");
Function(location, "assign", args => { host.Navigate?.Invoke(Argument(args, 0)); return JsValue.Undefined; });
Function(location, "replace", args => { host.Navigate?.Invoke(Argument(args, 0)); return JsValue.Undefined; });
Function(location, "reload", _ => { host.Navigate?.Invoke(address.AbsoluteUri); return JsValue.Undefined; });
Function(location, "toString", _ => address.AbsoluteUri);
Property(engine.Global, "location", () => location, value => host.Navigate?.Invoke(LimitedText(value)));
var history = new BrowserHostObject(engine); Data(history, "scrollRestoration", "auto", true); Data(history, "length", 1); Data(history, "state", JsValue.Null); engine.SetValue("history", history);
Property(engine.Global, "innerWidth", () => host.Viewport?.Invoke().Width ?? 800); Property(engine.Global, "innerHeight", () => host.Viewport?.Invoke().Height ?? 600);
Property(engine.Global, "scrollX", () => host.Viewport?.Invoke().ScrollX ?? 0); Property(engine.Global, "scrollY", () => host.Viewport?.Invoke().ScrollY ?? 0);
Property(engine.Global, "pageXOffset", () => host.Viewport?.Invoke().ScrollX ?? 0); Property(engine.Global, "pageYOffset", () => host.Viewport?.Invoke().ScrollY ?? 0);
Data(engine.Global, "devicePixelRatio", 1); Data(engine.Global, "isSecureContext", address.Scheme == "https");
var screen = new BrowserHostObject(engine); Property(screen, "width", () => host.Viewport?.Invoke().Width ?? 800); Property(screen, "height", () => host.Viewport?.Invoke().Height ?? 600); engine.SetValue("screen", screen);
}
private ObjectInstance Rectangle(INode node)
{
var rect = host.Measure?.Invoke(node) ?? default; var value = new BrowserHostObject(engine);
Data(value, "x", rect.X); Data(value, "y", rect.Y); Data(value, "width", rect.Width); Data(value, "height", rect.Height);
Data(value, "left", rect.X); Data(value, "top", rect.Y); Data(value, "right", rect.X + rect.Width); Data(value, "bottom", rect.Y + rect.Height); Function(value, "toJSON", _ => value); return value;
}
private JsValue Attributes(IElement element)
{
var values = new List<JsValue>(); foreach (var attribute in element.Attributes) { var value = new BrowserHostObject(engine); Data(value, "name", attribute.Name); Data(value, "value", attribute.Value); Data(value, "nodeName", attribute.Name); Data(value, "nodeValue", attribute.Value); values.Add(value); } return ListValues(values);
}
private JsValue AttributeNode(IElement element, string name)
{
var attribute = element.GetAttributeNode(name);
if (attribute is null) return JsValue.Null;
var value = new BrowserHostObject(engine);
Data(value, "name", attribute.Name); Data(value, "nodeName", attribute.Name); Data(value, "nodeType", 2); Data(value, "specified", true);
Property(value, "value", () => attribute.Value, text => SetAttribute(element, attribute.Name, text));
Property(value, "nodeValue", () => attribute.Value, text => SetAttribute(element, attribute.Name, text));
Property(value, "ownerElement", () => element.HasAttribute(attribute.Name) ? Wrap(element) : JsValue.Null);
return value;
}
private static string StyleGet(IElement element, string name)
{
foreach (var declaration in (element.GetAttribute("style") ?? string.Empty).Split(';').Reverse())
{ var colon = declaration.IndexOf(':'); if (colon > 0 && declaration[..colon].Trim().Equals(name, StringComparison.OrdinalIgnoreCase)) return declaration[(colon + 1)..].Trim().Replace("!important", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(); } return string.Empty;
}
private void StyleSet(IElement element, string name, string value, string priority)
{
if (name.Length is 0 or > 128 || name.Any(c => !char.IsAsciiLetterOrDigit(c) && c != '-')) throw new ArgumentException("Invalid CSS property name.");
var matching = (element.GetAttribute("style") ?? string.Empty).Split(';').Where(part => { int colon = part.IndexOf(':'); return colon > 0 && part[..colon].Trim().Equals(name, StringComparison.OrdinalIgnoreCase); }).ToArray();
if (matching.Length == 1 && value.Length != 0)
{
string previous = matching[0][(matching[0].IndexOf(':') + 1)..].Trim();
bool important = previous.EndsWith("!important", StringComparison.OrdinalIgnoreCase);
if (important) previous = previous[..^10].TrimEnd();
// CSSOM leaves an unchanged declaration alone, including its
// position in cssText and the absence of an attribute mutation.
if (previous == value.Trim() && important == priority.Equals("important", StringComparison.OrdinalIgnoreCase)) return;
}
var declarations = (element.GetAttribute("style") ?? string.Empty).Split(';').Where(part => { var colon = part.IndexOf(':'); return colon > 0 && !part[..colon].Trim().Equals(name, StringComparison.OrdinalIgnoreCase); }).ToList();
if (value.Length != 0) declarations.Add(name + ":" + value + (priority.Equals("important", StringComparison.OrdinalIgnoreCase) ? " !important" : string.Empty)); SetAttribute(element, "style", string.Join(';', declarations));
}
}
using System.Globalization;
using AngleSharp.Dom;
using Jint;
using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime.Descriptors;
using Jint.Runtime.Interop;
namespace XFESystem.Browser;
public sealed partial class DomBridge
{
private JsValue Wrap(INode? node)
{
if (node is null) return JsValue.Null;
if (wrappers.TryGetValue(node, out var known)) return known;
if (node is AngleSharp.Html.Dom.IHtmlInputElement or AngleSharp.Html.Dom.IHtmlTextAreaElement) characterBudgetValid = false;
if (wrappers.Count >= limits.MaximumDomNodes) throw new BrowserLimitException("The DOM wrapper limit was reached.");
NodeIdentity(node); var wrapper = new BrowserHostObject(engine); wrappers.Add(node, wrapper); nodes.Add(wrapper, node);
CallInternal(bootstrap.Get("init"), [wrapper, (int)node.NodeType, (node as IElement)?.LocalName ?? ""]); Data(wrapper, "__nodeId", identities[node].Id);
Property(wrapper, "nodeType", () => (int)node.NodeType); Property(wrapper, "nodeName", () => node.NodeName);
Property(wrapper, "nodeValue", () => Text(node.NodeValue), value => { if (node.NodeType is NodeType.Text or NodeType.Comment) MutateText(node, value); });
Property(wrapper, "textContent", () => Text(node.TextContent), value => MutateText(node, value));
if (node is IText text)
{
Property(wrapper, "data", () => Text(text.TextContent), value => MutateText(text, value));
Property(wrapper, "length", () => text.TextContent.Length);
Function(wrapper, "splitText", args => SplitTextNode(text, Value(args, 0)));
}
Property(wrapper, "parentNode", () => Wrap(node.Parent)); Property(wrapper, "parentElement", () => Wrap(node.ParentElement));
Property(wrapper, "ownerDocument", () => node is IDocument ? JsValue.Null : Wrap(node.Owner));
Property(wrapper, "firstChild", () => Wrap(node.FirstChild)); Property(wrapper, "lastChild", () => Wrap(node.LastChild));
Property(wrapper, "nextSibling", () => Wrap(node.NextSibling)); Property(wrapper, "previousSibling", () => Wrap(node.PreviousSibling));
var childNodes = LiveList(() => node.ChildNodes);
Property(wrapper, "childNodes", () => childNodes); Property(wrapper, "isConnected", () => Connected(node));
Function(wrapper, "append", args => { foreach (var value in args) Insert(node, NodeOrText(value), null); return JsValue.Undefined; });
Function(wrapper, "prepend", args => { var before = node.FirstChild; foreach (var value in args) Insert(node, NodeOrText(value), before); return JsValue.Undefined; });
if (node is IParentNode) Function(wrapper, "replaceChildren", args => { ReplaceChildren(node, args); return JsValue.Undefined; });
if (node.NodeType is NodeType.Element or NodeType.Text or NodeType.Comment)
{
Function(wrapper, "before", args => { InsertSiblings(node, args, before: true, replace: false); return JsValue.Undefined; });
Function(wrapper, "after", args => { InsertSiblings(node, args, before: false, replace: false); return JsValue.Undefined; });
Function(wrapper, "replaceWith", args => { InsertSiblings(node, args, before: false, replace: true); return JsValue.Undefined; });
}
Function(wrapper, "remove", _ => { var parent = node.Parent; if (parent is not null) { parent.RemoveChild(node); Changed(parent, "childList", removed: [node]); } return JsValue.Undefined; });
Function(wrapper, "cloneNode", args => CloneDomNode(node, Bool(Value(args, 0))));
Function(wrapper, "contains", args => { var other = OptionalNode(Value(args, 0)); return other is not null && Ancestor(node, other); });
Function(wrapper, "hasChildNodes", _ => node.ChildNodes.Length != 0);
if (node is IParentNode parentNode)
{
var children = LiveList(() => parentNode.Children, "HTMLCollection");
Property(wrapper, "children", () => children); Property(wrapper, "childElementCount", () => parentNode.Children.Length);
Property(wrapper, "firstElementChild", () => Wrap(parentNode.Children.FirstOrDefault())); Property(wrapper, "lastElementChild", () => Wrap(parentNode.Children.LastOrDefault()));
Function(wrapper, "querySelector", args => Wrap(parentNode.QuerySelector(Argument(args, 0)))); Function(wrapper, "querySelectorAll", args => List(parentNode.QuerySelectorAll(Argument(args, 0))));
Function(wrapper, "getElementsByTagName", args => { var tag = Argument(args, 0); return LiveList(() => Descendants(node).Where(element => tag == "*" || element.LocalName.Equals(tag, StringComparison.OrdinalIgnoreCase)), "HTMLCollection"); });
Function(wrapper, "getElementsByClassName", args => { var tokens = Argument(args, 0).Split(' ', StringSplitOptions.RemoveEmptyEntries); return LiveList(() => Descendants(node).Where(element => tokens.All(element.ClassList.Contains)), "HTMLCollection"); });
}
if (node is IElement element) InstallElement(wrapper, element);
if (node is IDocument owner) InstallDocument(wrapper, owner);
if (node is IElement custom && custom.LocalName.Contains('-')) CallInternal(bootstrap.Get("upgrade"), [wrapper]);
return wrapper;
}
private void InstallNodeMutationPrototypes()
{
var prototype = engine.GetValue("Node").AsObject().Get("prototype").AsObject();
void Method(string name, Func<INode, JsValue[], JsValue> action) => prototype.DefineOwnProperty(name,
new PropertyDescriptor(new ClrFunction(engine, name, (receiver, args) =>
{
if (!receiver.IsObject() || !nodes.TryGetValue(receiver.AsObject(), out var parent))
throw AdjacentError("TypeError", "The receiver must be a node from this page.", 0);
return action(parent, args);
}), true, true, true));
Method("appendChild", (parent, args) => Insert(parent, RequireNode(Value(args, 0)), null));
Method("insertBefore", (parent, args) => Insert(parent, RequireNode(Value(args, 0)), OptionalNode(Value(args, 1))));
Method("removeChild", (parent, args) =>
{
var child = RequireNode(Value(args, 0));
if (child.Parent != parent) throw new ArgumentException("The node is not a child of this parent.");
parent.RemoveChild(child); Changed(parent, "childList", removed: [child]); return Wrap(child);
});
Method("replaceChild", (parent, args) =>
{
var child = RequireNode(Value(args, 0)); var old = RequireNode(Value(args, 1));
if (old.Parent != parent) throw new ArgumentException("The node is not a child of this parent.");
Insert(parent, child, old); parent.RemoveChild(old); Changed(parent, "childList", removed: [old]); return Wrap(old);
});
}
private void ReplaceChildren(INode parent, JsValue[] arguments)
{
var replacements = arguments.Select(NodeOrText).ToArray();
foreach (var child in replacements) if (Ancestor(child, parent)) throw AdjacentError("HierarchyRequestError", "A DOM node cannot contain itself.", 3);
var fragment = document.CreateDocumentFragment();
foreach (var child in replacements) Insert(fragment, child, null);
var removed = parent.ChildNodes.ToArray();
foreach (var child in removed) parent.RemoveChild(child);
if (removed.Length != 0) Changed(parent, "childList", removed: removed);
Insert(parent, fragment, null);
}
private void InsertSiblings(INode node, JsValue[] arguments, bool before, bool replace)
{
var parent = node.Parent;
if (parent is null) return;
var replacements = arguments.Select(NodeOrText).ToArray();
foreach (var child in replacements) if (Ancestor(child, parent)) throw AdjacentError("HierarchyRequestError", "A DOM node cannot contain itself.", 3);
// Determine a viable anchor before conversion moves any existing sibling
// into the temporary fragment (including the receiver itself).
INode? anchor = before ? node.PreviousSibling : node.NextSibling;
while (anchor is not null && replacements.Contains(anchor, ReferenceEqualityComparer.Instance))
anchor = before ? anchor.PreviousSibling : anchor.NextSibling;
var fragment = document.CreateDocumentFragment();
foreach (var child in replacements) Insert(fragment, child, null);
if (before) anchor = anchor is null ? parent.FirstChild : anchor.NextSibling;
if (replace && ReferenceEquals(node.Parent, parent))
{
Insert(parent, fragment, node);
parent.RemoveChild(node); Changed(parent, "childList", removed: [node]);
}
else Insert(parent, fragment, anchor);
}
private void InstallElement(ObjectInstance wrapper, IElement element)
{
if (element.LocalName == "form")
{
Function(wrapper, "submit", _ => { host.SubmitForm?.Invoke(element, null); return JsValue.Undefined; });
Function(wrapper, "requestSubmit", args =>
{
var submitter = Value(args, 0).IsUndefined() ? null : RequireNode(Value(args, 0)) as IElement;
if (submitter is not null && (!ReferenceEquals(submitter.Closest("form"), element) || submitter.LocalName is not ("input" or "button") ||
(submitter.GetAttribute("type") ?? (submitter.LocalName == "button" ? "submit" : "text")) is not ("submit" or "image")))
throw AdjacentError("NotFoundError", "The submitter is not a submit control of this form.", 8);
var init = new BrowserHostObject(engine); Data(init, "bubbles", true); Data(init, "cancelable", true);
var evt = CallInternal(bootstrap.Get("event"), ["submit", init]); Data(evt.AsObject(), "submitter", Wrap(submitter));
if (CallInternal(bootstrap.Get("dispatch"), [wrapper, evt]).AsBoolean()) host.SubmitForm?.Invoke(element, submitter);
return JsValue.Undefined;
});
}
Property(wrapper, "innerHTML", () => element.InnerHtml, value => MutateHtml(element, value)); Property(wrapper, "outerHTML", () => element.OuterHtml);
if (element.NamespaceUri == "http://www.w3.org/1999/xhtml")
Property(wrapper, "innerText", () => InnerTextValue(element), value => MutateInnerText(element, value));
Property(wrapper, "tagName", () => element.TagName); Property(wrapper, "localName", () => element.LocalName); Property(wrapper, "namespaceURI", () => Text(element.NamespaceUri));
Property(wrapper, "id", () => element.Id ?? "", value => SetAttribute(element, "id", value)); Property(wrapper, "className", () => element.ClassName ?? "", value => SetAttribute(element, "class", value));
foreach (var name in new[] { "type", "name", "title", "placeholder", "tabIndex", "contentEditable", "rel", "media", "target", "download", "dir", "lang" })
{ var attribute = name.ToLowerInvariant(); Property(wrapper, name, () => element.GetAttribute(attribute) ?? string.Empty, value => SetAttribute(element, attribute, value)); }
foreach (var name in new[] { "src", "href" }) Property(wrapper, name, () => ResolveUrl(element.GetAttribute(name)), value => SetAttribute(element, name, value));
if (element.NamespaceUri == "http://www.w3.org/1999/xhtml" && element.LocalName is "a" or "area") InstallHyperlink(wrapper, element);
Property(wrapper, "nonce", () => element.GetAttribute("nonce") ?? "", value => SetAttribute(element, "nonce", value));
foreach (string attribute in new[] { "rel", "type", "name", "target", "title", "placeholder", "crossorigin" })
Property(wrapper, attribute, () => element.GetAttribute(attribute) ?? "", value => SetAttribute(element, attribute, value));
if (element.LocalName == "script") Property(wrapper, "text", () => element.TextContent, value => MutateText(element, value));
if (element.LocalName == "img")
{
Property(wrapper, "naturalWidth", () => host.ImageInfo?.Invoke(element).Width ?? 0);
Property(wrapper, "naturalHeight", () => host.ImageInfo?.Invoke(element).Height ?? 0);
Property(wrapper, "complete", () => host.ImageInfo?.Invoke(element).Complete ?? false);
Function(wrapper, "__decodeImage", _ => { host.ResourceRequested?.Invoke(element); return JsValue.Undefined; });
}
foreach (var name in new[] { "disabled", "checked", "readOnly", "hidden", "multiple", "selected" })
{ var attribute = name.ToLowerInvariant(); Property(wrapper, name, () => element.HasAttribute(attribute), value => { if (Bool(value)) SetAttribute(element, attribute, string.Empty); else RemoveAttribute(element, attribute); }); }
Property(wrapper, "nextElementSibling", () => Wrap(element.NextElementSibling)); Property(wrapper, "previousElementSibling", () => Wrap(element.PreviousElementSibling));
Property(wrapper, "attributes", () => Attributes(element));
Function(wrapper, "getAttribute", args => Text(element.GetAttribute(ShortArgument(args, 0)))); Function(wrapper, "hasAttribute", args => element.HasAttribute(ShortArgument(args, 0)));
Function(wrapper, "getAttributeNode", args => AttributeNode(element, ShortArgument(args, 0)));
Function(wrapper, "setAttribute", args => { SetAttribute(element, ShortArgument(args, 0), Value(args, 1)); return JsValue.Undefined; });
Function(wrapper, "setAttributeNS", args => { SetAttribute(element, ShortArgument(args, 1), Value(args, 2)); return JsValue.Undefined; });
Function(wrapper, "removeAttribute", args => { RemoveAttribute(element, ShortArgument(args, 0)); return JsValue.Undefined; });
Function(wrapper, "getAttributeNS", args => Text(element.GetAttribute(ShortArgument(args, 1)))); Function(wrapper, "hasAttributeNS", args => element.HasAttribute(ShortArgument(args, 1)));
Function(wrapper, "removeAttributeNS", args => { RemoveAttribute(element, ShortArgument(args, 1)); return JsValue.Undefined; });
Function(wrapper, "matches", args => element.Matches(Argument(args, 0)));
Function(wrapper, "insertAdjacentHTML", args => { InsertAdjacentHtml(element, ShortArgument(args, 0), Value(args, 1)); return JsValue.Undefined; });
Function(wrapper, "insertAdjacentText", args => { InsertAdjacentText(element, ShortArgument(args, 0), Value(args, 1)); return JsValue.Undefined; });
Function(wrapper, "insertAdjacentElement", args => InsertAdjacentElement(element, ShortArgument(args, 0), Value(args, 1)));
Function(wrapper, "closest", args => { var selector = Argument(args, 0); for (var p = element; p is not null; p = p.ParentElement) if (p.Matches(selector)) return Wrap(p); return JsValue.Null; });
Function(wrapper, "__styleGet", args => StyleGet(element, ShortArgument(args, 0)));
Function(wrapper, "__styleSet", args => { StyleSet(element, ShortArgument(args, 0), Argument(args, 1), Argument(args, 2)); return JsValue.Undefined; });
Function(wrapper, "__computed", args => host.ComputedStyle?.Invoke(element, ShortArgument(args, 0)) ?? StyleGet(element, ShortArgument(args, 0)));
Function(wrapper, "getBoundingClientRect", _ => Rectangle(element)); Function(wrapper, "getClientRects", _ => ListValues([Rectangle(element)]));
foreach (var name in new[] { "clientWidth", "offsetWidth", "scrollWidth", "clientHeight", "offsetHeight", "scrollHeight", "offsetLeft", "offsetTop" })
Property(wrapper, name, () => { var rect = host.Measure?.Invoke(element) ?? default; return name.EndsWith("Width", StringComparison.Ordinal) ? rect.Width : name.EndsWith("Height", StringComparison.Ordinal) ? rect.Height : name == "offsetLeft" ? rect.X : rect.Y; });
Data(wrapper, "scrollLeft", 0, true); Data(wrapper, "scrollTop", 0, true);
InstallForm(wrapper, element);
if (element.LocalName == "canvas") InstallCanvas(wrapper, element);
Function(wrapper, "focus", _ => { Focus(element); return JsValue.Undefined; }); Function(wrapper, "blur", _ => { if (focused == element) Focus(null); return JsValue.Undefined; });
Function(wrapper, "click", _ => DispatchInternal(element, "click", true, true));
Function(wrapper, "setPointerCapture", _ => JsValue.Undefined); Function(wrapper, "releasePointerCapture", _ => JsValue.Undefined); Function(wrapper, "hasPointerCapture", _ => false);
}
private void InstallDocument(ObjectInstance projected, IDocument owner)
{
Property(projected, "contentType", () => parsedDocuments.GetValueOrDefault(owner, "text/html"));
Function(projected, "getElementById", args => Wrap(owner.GetElementById(Argument(args, 0))));
Function(projected, "createElement", args => Created(owner.CreateElement(ShortArgument(args, 0)), true));
Function(projected, "createElementNS", args => Created(owner.CreateElement(Argument(args, 0), ShortArgument(args, 1)), true));
Function(projected, "createTextNode", args => Created(owner.CreateTextNode(Argument(args, 0)))); Function(projected, "createComment", args => Created(owner.CreateComment(Argument(args, 0))));
Function(projected, "createDocumentFragment", _ => Created(owner.CreateDocumentFragment())); Function(projected, "createEvent", _ => CallInternal(bootstrap.Get("event"), [string.Empty, new BrowserHostObject(engine)]));
Function(projected, "createRange", _ => WrapRange(owner.CreateRange()));
Function(projected, "createTreeWalker", args => CreateTreeWalker(owner, args));
Function(projected, "getSelection", _ => DocumentSelection());
Function(projected, "caretRangeFromPoint", args => CaretFromPoint(Number(Value(args, 0)), Number(Value(args, 1)), false));
Function(projected, "caretPositionFromPoint", args => CaretFromPoint(Number(Value(args, 0)), Number(Value(args, 1)), true));
Function(projected, "queryCommandSupported", _ => false); Function(projected, "execCommand", _ => false);
Property(projected, "body", () => Wrap(owner.Body)); Property(projected, "head", () => Wrap(owner.Head)); Property(projected, "documentElement", () => Wrap(owner.DocumentElement ?? owner.ChildNodes.OfType<IElement>().FirstOrDefault()));
Property(projected, "activeElement", () => Wrap(owner == document ? focused ?? owner.Body : owner.Body)); Property(projected, "defaultView", () => owner == document ? engine.Global : JsValue.Null); Property(projected, "readyState", () => owner == document ? readyState : "complete");
Property(projected, "title", () => owner.Title, value => SetTitle(owner, value)); Property(projected, "URL", () => address.AbsoluteUri); Property(projected, "baseURI", () => address.AbsoluteUri);
Property(projected, "location", () => owner == document ? engine.GetValue("location") : JsValue.Null, value => { if (owner == document) host.Navigate?.Invoke(LimitedText(value)); });
Property(projected, "compatMode", () => "CSS1Compat"); Property(projected, "characterSet", () => "UTF-8"); Property(projected, "currentScript", () => Wrap(owner == document ? CurrentScript : null));
Property(projected, "cookie", () => owner == document ? host.ReadCookies?.Invoke() ?? "" : "", value => { if (owner == document) host.WriteCookie?.Invoke(LimitedText(value)); });
Property(projected, "visibilityState", () => "visible"); Property(projected, "hidden", () => false);
Property(projected, "images", () => LiveList(() => owner.QuerySelectorAll("img"), "HTMLCollection"));
Property(projected, "scripts", () => LiveList(() => owner.QuerySelectorAll("script"), "HTMLCollection"));
Property(projected, "forms", () => LiveList(() => owner.QuerySelectorAll("form"), "HTMLCollection"));
Property(projected, "links", () => LiveList(() => owner.QuerySelectorAll("a[href],area[href]"), "HTMLCollection"));
}
private void InstallWindow()
{
Function(engine.Global, "getSelection", _ => DocumentSelection());
foreach (string name in new[] { "onload", "onunload", "onbeforeunload", "onerror", "onresize", "onscroll", "onfocus", "onblur", "onpageshow", "onpagehide", "onpopstate", "onhashchange", "onmessage", "onunhandledrejection" })
Data(engine.Global, name, JsValue.Null, true);
var navigator = new BrowserHostObject(engine); Data(navigator, "userAgent", BrowserIdentity.UserAgent); Data(navigator, "platform", "XFESystem");
Data(navigator, "language", "zh-CN"); Data(navigator, "languages", ListValues(["zh-CN", "en"])); Data(navigator, "hardwareConcurrency", 1); Data(navigator, "onLine", true); Data(navigator, "maxTouchPoints", 0); engine.SetValue("navigator", navigator);
var location = new BrowserHostObject(engine); Property(location, "href", () => address.AbsoluteUri, value => host.Navigate?.Invoke(LimitedText(value))); Data(location, "protocol", address.Scheme + ":"); Data(location, "host", address.Authority);
Data(location, "hostname", address.Host); Data(location, "port", address.IsDefaultPort ? string.Empty : address.Port.ToString(CultureInfo.InvariantCulture));
Data(location, "pathname", address.AbsolutePath); Data(location, "search", address.Query); Data(location, "hash", address.Fragment);
Data(location, "origin", address.IsAbsoluteUri && address.Scheme is "http" or "https" ? address.GetLeftPart(UriPartial.Authority) : "null");
Function(location, "assign", args => { host.Navigate?.Invoke(Argument(args, 0)); return JsValue.Undefined; });
Function(location, "replace", args => { host.Navigate?.Invoke(Argument(args, 0)); return JsValue.Undefined; });
Function(location, "reload", _ => { host.Navigate?.Invoke(address.AbsoluteUri); return JsValue.Undefined; });
Function(location, "toString", _ => address.AbsoluteUri);
Property(engine.Global, "location", () => location, value => host.Navigate?.Invoke(LimitedText(value)));
var history = new BrowserHostObject(engine); Data(history, "scrollRestoration", "auto", true); Data(history, "length", 1); Data(history, "state", JsValue.Null); engine.SetValue("history", history);
Property(engine.Global, "innerWidth", () => host.Viewport?.Invoke().Width ?? 800); Property(engine.Global, "innerHeight", () => host.Viewport?.Invoke().Height ?? 600);
Property(engine.Global, "scrollX", () => host.Viewport?.Invoke().ScrollX ?? 0); Property(engine.Global, "scrollY", () => host.Viewport?.Invoke().ScrollY ?? 0);
Property(engine.Global, "pageXOffset", () => host.Viewport?.Invoke().ScrollX ?? 0); Property(engine.Global, "pageYOffset", () => host.Viewport?.Invoke().ScrollY ?? 0);
Data(engine.Global, "devicePixelRatio", 1); Data(engine.Global, "isSecureContext", address.Scheme == "https");
var screen = new BrowserHostObject(engine); Property(screen, "width", () => host.Viewport?.Invoke().Width ?? 800); Property(screen, "height", () => host.Viewport?.Invoke().Height ?? 600); engine.SetValue("screen", screen);
}
private ObjectInstance Rectangle(INode node)
{
var rect = host.Measure?.Invoke(node) ?? default; var value = new BrowserHostObject(engine);
Data(value, "x", rect.X); Data(value, "y", rect.Y); Data(value, "width", rect.Width); Data(value, "height", rect.Height);
Data(value, "left", rect.X); Data(value, "top", rect.Y); Data(value, "right", rect.X + rect.Width); Data(value, "bottom", rect.Y + rect.Height); Function(value, "toJSON", _ => value); return value;
}
private JsValue Attributes(IElement element)
{
var values = new List<JsValue>(); foreach (var attribute in element.Attributes) { var value = new BrowserHostObject(engine); Data(value, "name", attribute.Name); Data(value, "value", attribute.Value); Data(value, "nodeName", attribute.Name); Data(value, "nodeValue", attribute.Value); values.Add(value); } return ListValues(values);
}
private JsValue AttributeNode(IElement element, string name)
{
var attribute = element.GetAttributeNode(name);
if (attribute is null) return JsValue.Null;
var value = new BrowserHostObject(engine);
Data(value, "name", attribute.Name); Data(value, "nodeName", attribute.Name); Data(value, "nodeType", 2); Data(value, "specified", true);
Property(value, "value", () => attribute.Value, text => SetAttribute(element, attribute.Name, text));
Property(value, "nodeValue", () => attribute.Value, text => SetAttribute(element, attribute.Name, text));
Property(value, "ownerElement", () => element.HasAttribute(attribute.Name) ? Wrap(element) : JsValue.Null);
return value;
}
private static string StyleGet(IElement element, string name)
{
foreach (var declaration in (element.GetAttribute("style") ?? string.Empty).Split(';').Reverse())
{ var colon = declaration.IndexOf(':'); if (colon > 0 && declaration[..colon].Trim().Equals(name, StringComparison.OrdinalIgnoreCase)) return declaration[(colon + 1)..].Trim().Replace("!important", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(); } return string.Empty;
}
private void StyleSet(IElement element, string name, string value, string priority)
{
if (name.Length is 0 or > 128 || name.Any(c => !char.IsAsciiLetterOrDigit(c) && c != '-')) throw new ArgumentException("Invalid CSS property name.");
var matching = (element.GetAttribute("style") ?? string.Empty).Split(';').Where(part => { int colon = part.IndexOf(':'); return colon > 0 && part[..colon].Trim().Equals(name, StringComparison.OrdinalIgnoreCase); }).ToArray();
if (matching.Length == 1 && value.Length != 0)
{
string previous = matching[0][(matching[0].IndexOf(':') + 1)..].Trim();
bool important = previous.EndsWith("!important", StringComparison.OrdinalIgnoreCase);
if (important) previous = previous[..^10].TrimEnd();
// CSSOM leaves an unchanged declaration alone, including its
// position in cssText and the absence of an attribute mutation.
if (previous == value.Trim() && important == priority.Equals("important", StringComparison.OrdinalIgnoreCase)) return;
}
var declarations = (element.GetAttribute("style") ?? string.Empty).Split(';').Where(part => { var colon = part.IndexOf(':'); return colon > 0 && !part[..colon].Trim().Equals(name, StringComparison.OrdinalIgnoreCase); }).ToList();
if (value.Length != 0) declarations.Add(name + ":" + value + (priority.Equals("important", StringComparison.OrdinalIgnoreCase) ? " !important" : string.Empty)); SetAttribute(element, "style", string.Join(';', declarations));
}
}