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.Native;
using Jint.Native.Object;

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 (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]); 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));
        Property(wrapper, "parentNode", () => Wrap(node.Parent)); Property(wrapper, "parentElement", () => Wrap(node.ParentElement));
        Property(wrapper, "ownerDocument", () => node == document ? JsValue.Null : Wrap(document));
        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, "appendChild", args => Insert(node, RequireNode(Value(args, 0)), null));
        Function(wrapper, "insertBefore", args => Insert(node, RequireNode(Value(args, 0)), OptionalNode(Value(args, 1))));
        Function(wrapper, "removeChild", args =>
        {
            var child = RequireNode(Value(args, 0)); if (child.Parent != node) throw new ArgumentException("The node is not a child of this parent.");
            node.RemoveChild(child); Changed(node, "childList", removed: [child]); return Wrap(child);
        });
        Function(wrapper, "replaceChild", args =>
        {
            var child = RequireNode(Value(args, 0)); var old = RequireNode(Value(args, 1)); if (old.Parent != node) throw new ArgumentException("The node is not a child of this parent.");
            Insert(node, child, old); node.RemoveChild(old); Changed(node, "childList", removed: [old]); return Wrap(old);
        });
        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 => Created(node.Clone(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);
            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))); });
            Function(wrapper, "getElementsByClassName", args => { var tokens = Argument(args, 0).Split(' ', StringSplitOptions.RemoveEmptyEntries); return LiveList(() => Descendants(node).Where(element => tokens.All(element.ClassList.Contains))); });
        }
        if (node is IElement element) InstallElement(wrapper, element);
        if (node is IDocument) InstallDocument(wrapper);
        return wrapper;
    }

    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)
    {
        Property(wrapper, "innerHTML", () => element.InnerHtml, value => MutateHtml(element, value)); Property(wrapper, "outerHTML", () => element.OuterHtml);
        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" })
        { 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));
        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, "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)
    {
        Function(projected, "getElementById", args => Wrap(document.GetElementById(Argument(args, 0))));
        Function(projected, "createElement", args => Created(document.CreateElement(ShortArgument(args, 0))));
        Function(projected, "createElementNS", args => Created(document.CreateElement(Argument(args, 0), ShortArgument(args, 1))));
        Function(projected, "createTextNode", args => Created(document.CreateTextNode(Argument(args, 0)))); Function(projected, "createComment", args => Created(document.CreateComment(Argument(args, 0))));
        Function(projected, "createDocumentFragment", _ => Created(document.CreateDocumentFragment())); Function(projected, "createEvent", _ => CallInternal(bootstrap.Get("event"), [string.Empty, new BrowserHostObject(engine)]));
        Function(projected, "createRange", _ => WrapRange(document.CreateRange()));
        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(document.Body)); Property(projected, "head", () => Wrap(document.Head)); Property(projected, "documentElement", () => Wrap(document.DocumentElement));
        Property(projected, "activeElement", () => Wrap(focused ?? document.Body)); Property(projected, "defaultView", () => engine.Global); Property(projected, "readyState", () => readyState);
        Property(projected, "title", () => document.Title, SetTitle); Property(projected, "URL", () => address.AbsoluteUri); Property(projected, "baseURI", () => address.AbsoluteUri);
        Property(projected, "compatMode", () => "CSS1Compat"); Property(projected, "characterSet", () => "UTF-8"); Property(projected, "currentScript", () => Wrap(CurrentScript));
    }

    private void InstallWindow()
    {
        var navigator = new BrowserHostObject(engine); Data(navigator, "userAgent", "XFESystem/0.3 Browser Jint"); 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); Data(location, "href", address.AbsoluteUri); 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"); engine.SetValue("location", location);
        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 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 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));
    }
}