using System.Globalization; using System.Text; using AngleSharp.Dom; using Jint; using Jint.Native; using Jint.Native.Object; namespace XFESystem.Browser; public sealed partial class DomBridge { private readonly DependentIdentityMap ranges = new(); private JsValue WrapRange(IRange range) { if (ranges.Count >= limits.MaximumPendingTasks * 2) throw new BrowserLimitException("The live DOM Range limit was reached."); var wrapper = new BrowserHostObject(engine); ranges.Add(wrapper,new()); Property(wrapper, "startContainer", () => Wrap(range.Head)); Property(wrapper, "endContainer", () => Wrap(range.Tail)); Property(wrapper, "startOffset", () => range.Start); Property(wrapper, "endOffset", () => range.End); Property(wrapper, "collapsed", () => range.IsCollapsed); Property(wrapper, "commonAncestorContainer", () => Wrap(range.CommonAncestor)); Function(wrapper, "setStart", args => { range.StartWith(RequireNode(Value(args, 0)), NumberIndex(Value(args, 1))); return JsValue.Undefined; }); Function(wrapper, "setEnd", args => { range.EndWith(RequireNode(Value(args, 0)), NumberIndex(Value(args, 1))); return JsValue.Undefined; }); Function(wrapper, "setStartBefore", args => { range.StartBefore(RequireNode(Value(args, 0))); return JsValue.Undefined; }); Function(wrapper, "setStartAfter", args => { range.StartAfter(RequireNode(Value(args, 0))); return JsValue.Undefined; }); Function(wrapper, "setEndBefore", args => { range.EndBefore(RequireNode(Value(args, 0))); return JsValue.Undefined; }); Function(wrapper, "setEndAfter", args => { range.EndAfter(RequireNode(Value(args, 0))); return JsValue.Undefined; }); Function(wrapper, "selectNodeContents", args => { range.SelectContent(RequireNode(Value(args, 0))); return JsValue.Undefined; }); Function(wrapper, "selectNode", args => { range.Select(RequireNode(Value(args, 0))); return JsValue.Undefined; }); Function(wrapper, "collapse", args => { range.Collapse(Bool(Value(args, 0))); return JsValue.Undefined; }); Function(wrapper, "cloneRange", _ => WrapRange(range.Clone())); Function(wrapper, "detach", _ => JsValue.Undefined); Function(wrapper, "toString", _ => RangeText(range)); Function(wrapper, "getClientRects", _ => ListValues(RangeRects(range).Select(rect => (JsValue)Rectangle(rect)))); Function(wrapper, "getBoundingClientRect", _ => Rectangle(RangeBounds(range))); Function(wrapper, "cloneContents", _ => Created(range.CopyContent())); Function(wrapper, "extractContents", _ => { var result = Created(range.ExtractContent()); Changed(range.CommonAncestor, "childList"); return result; }); Function(wrapper, "deleteContents", _ => { var ancestor = range.CommonAncestor; range.ClearContent(); Changed(ancestor, "childList"); return JsValue.Undefined; }); Function(wrapper, "insertNode", args => { var ancestor = range.CommonAncestor; range.Insert(RequireNode(Value(args, 0))); ValidateBudget(); Changed(ancestor, "childList"); return JsValue.Undefined; }); Function(wrapper, "intersectsNode", args => range.Intersects(RequireNode(Value(args, 0)))); return wrapper; } private IReadOnlyList RangeRects(IRange range) { if (host.MeasureRange is not null) return host.MeasureRange(range.Head, range.Start, range.Tail, range.End); var result = new List(); foreach (var node in TextNodes(range.CommonAncestor)) { if (node != range.Head && node != range.Tail && !range.Intersects(node)) continue; var start = node == range.Head ? Math.Clamp(range.Start, 0, node.TextContent.Length) : 0; var end = node == range.Tail ? Math.Clamp(range.End, start, node.TextContent.Length) : node.TextContent.Length; var rect = host.Measure?.Invoke(node) ?? default; if (rect.Height <= 0) continue; var font = RangeFont(node); var prefix = Measure(node.TextContent[..start], font); var width = Measure(node.TextContent[start..end], font); result.Add(new(rect.X + prefix, rect.Y, width, rect.Height)); } return result; } private BrowserRect RangeBounds(IRange range) { var rects = RangeRects(range); if (rects.Count == 0) return default; double left = rects[0].Left, top = rects[0].Top, right = rects[0].Right, bottom = rects[0].Bottom; foreach (var rect in rects.Skip(1)) { left = Math.Min(left, rect.Left); top = Math.Min(top, rect.Top); right = Math.Max(right, rect.Right); bottom = Math.Max(bottom, rect.Bottom); } return new(left, top, right - left, bottom - top); } private static string RangeText(IRange range) { var text = new StringBuilder(); foreach (var node in TextNodes(range.CommonAncestor)) { if (node != range.Head && node != range.Tail && !range.Intersects(node)) continue; var start = node == range.Head ? Math.Clamp(range.Start, 0, node.TextContent.Length) : 0; var end = node == range.Tail ? Math.Clamp(range.End, start, node.TextContent.Length) : node.TextContent.Length; text.Append(node.TextContent.AsSpan(start, end - start)); } return text.ToString(); } private JsValue CaretFromPoint(double x, double y, bool position) { var hit = host.HitTest?.Invoke(x, y); INode? best = null; double bestDistance = double.MaxValue; foreach (var node in TextNodes(hit ?? document)) { var rect = host.Measure?.Invoke(node) ?? default; if (rect.Height <= 0) continue; var dx = x < rect.Left ? rect.Left - x : x > rect.Right ? x - rect.Right : 0; var dy = y < rect.Top ? rect.Top - y : y > rect.Bottom ? y - rect.Bottom : 0; var distance = dy * dy * 4 + dx * dx; if (distance < bestDistance) { bestDistance = distance; best = node; } } if (best is null && hit is not null) foreach (var node in TextNodes(document)) { var rect = host.Measure?.Invoke(node) ?? default; if (rect.Height > 0 && y >= rect.Top && y <= rect.Bottom) { best = node; break; } } if (best is null) return JsValue.Null; var bounds = host.Measure?.Invoke(best) ?? default; var font = RangeFont(best); var text = best.TextContent; int low = 0, high = text.Length; double localX = x - bounds.Left; while (low < high) { var middle = (low + high + 1) / 2; if (Measure(text[..middle], font) <= localX) low = middle; else high = middle - 1; } if (low < text.Length && localX - Measure(text[..low], font) > (Measure(text[..(low + 1)], font) - Measure(text[..low], font)) / 2) ++low; if (low > 0 && low < text.Length && char.IsLowSurrogate(text[low]) && char.IsHighSurrogate(text[low - 1])) --low; if (position) { var result = new BrowserHostObject(engine); Data(result, "offsetNode", Wrap(best)); Data(result, "offset", low); return result; } var range = document.CreateRange(); range.StartWith(best, low); range.EndWith(best, low); return WrapRange(range); } private BrowserFont RangeFont(INode node) { var size = host.ComputedStyle?.Invoke(node, "font-size") ?? "16px"; if (size.EndsWith("px", StringComparison.Ordinal)) size = size[..^2]; var fontSize = double.TryParse(size, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : 16; int weight = int.TryParse(host.ComputedStyle?.Invoke(node, "font-weight"), out var parsed) ? parsed : 400; return new(fontSize, host.ComputedStyle?.Invoke(node, "font-family") ?? "monospace", weight); } private double Measure(string text, BrowserFont font) => host.MeasureText?.Invoke(text, font) ?? new BrowserFallbackTextMetrics().Measure(text, font); private static IEnumerable TextNodes(INode node) { if (node.NodeType == NodeType.Text) yield return node; else foreach (var child in node.ChildNodes) foreach (var text in TextNodes(child)) yield return text; } private ObjectInstance Rectangle(BrowserRect rect) { 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.Left); Data(value, "right", rect.Right); Data(value, "top", rect.Top); Data(value, "bottom", rect.Bottom); Function(value, "toJSON", _ => value); return value; } }