using AngleSharp.Dom; using Jint; using Jint.Native; namespace XFESystem.Browser; public sealed partial class DomBridge { private JsValue CreateTreeWalker(IDocument owner, JsValue[] arguments) { var root = RequireNode(Value(arguments, 0)); uint mask = Value(arguments, 1).IsUndefined() ? uint.MaxValue : Jint.Runtime.TypeConverter.ToUint32(Value(arguments, 1)); var filter = Value(arguments, 2); bool filtering = false; int visits = 0; FilterResult Accept(INode node) { if (++visits > limits.MaximumDomNodes) throw new BrowserLimitException("TreeWalker exceeds its traversal budget."); if (filter.IsNull() || filter.IsUndefined()) return FilterResult.Accept; if (filtering) throw AdjacentError("InvalidStateError", "The TreeWalker filter is already active.", 11); filtering = true; JsValue result; try { result = CallInternal(bootstrap.Get("filterNode"), [filter, Wrap(node)]); } catch { filtering = false; throw; } filtering = false; return (FilterResult)(ushort)result.AsNumber(); } var walker = owner.CreateTreeWalker(root, unchecked((FilterSettings)mask), Accept); INode? Previous() { // AngleSharp 1.7.1's backward loop does not advance a rejected // sibling or assign its parent. Use bounded DOM links here. INode? candidate = walker.Current; int steps = 0; FilterResult Test(INode node) => (mask & (1u << ((int)node.NodeType - 1))) == 0 ? FilterResult.Skip : Accept(node); while (candidate is not null && candidate != root) { if (++steps > limits.MaximumDomNodes) throw new BrowserLimitException("TreeWalker exceeds its traversal budget."); if (candidate.PreviousSibling is { } sibling) { candidate = sibling; var decision = Test(candidate); while (decision != FilterResult.Reject && candidate.LastChild is { } child) { if (++steps > limits.MaximumDomNodes) throw new BrowserLimitException("TreeWalker exceeds its traversal budget."); candidate = child; decision = Test(candidate); } if (decision == FilterResult.Accept) { walker.Current = candidate; return candidate; } } else { candidate = candidate.Parent; if (candidate is not null && Test(candidate) == FilterResult.Accept) { walker.Current = candidate; return candidate; } } } return null; } var wrapper = new BrowserHostObject(engine); CallInternal(bootstrap.Get("initTreeWalker"), [wrapper]); Property(wrapper, "root", () => Wrap(root)); Property(wrapper, "whatToShow", () => (double)mask); Property(wrapper, "filter", () => filter.IsUndefined() ? JsValue.Null : filter); Property(wrapper, "currentNode", () => Wrap(walker.Current), value => walker.Current = RequireNode(value)); void Method(string name, Func action) => Function(wrapper, name, _ => { if (filtering) throw AdjacentError("InvalidStateError", "The TreeWalker filter is already active.", 11); visits = 0; return Wrap(action()); }); Method("nextNode", walker.ToNext); Method("previousNode", Previous); Method("parentNode", walker.ToParent); Method("firstChild", walker.ToFirst); Method("lastChild", walker.ToLast); Method("nextSibling", walker.ToNextSibling); Method("previousSibling", walker.ToPreviousSibling); return wrapper; } private JsValue SplitTextNode(IText text, JsValue value) { uint offset = Jint.Runtime.TypeConverter.ToUint32(value); string old = text.TextContent; if (offset > old.Length) throw AdjacentError("IndexSizeError", "The text offset is outside its UTF-16 data.", 1); // Splitting conserves all characters and adds exactly one node. Admit // that node before mutation so rejected budgets leave ranges untouched. ValidateBudget([text.Owner!.CreateTextNode("")]); var result = text.Split((int)offset); Changed(text, "characterData", oldValue: old); if (text.Parent is { } parent) Changed(parent, "childList", added: [result]); return Wrap(result); } }