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(); 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)); } }