XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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));
    }
}