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 List<WeakReference<ObjectInstance>> ranges = [];
private JsValue WrapRange(IRange range)
{
if (ranges.Count >= limits.MaximumPendingTasks * 2)
{
ranges.RemoveAll(reference => !reference.TryGetTarget(out _));
if (ranges.Count >= limits.MaximumPendingTasks * 2) throw new BrowserLimitException("The live DOM Range limit was reached.");
}
var wrapper = new BrowserHostObject(engine); ranges.Add(new(wrapper));
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<BrowserRect> RangeRects(IRange range)
{
if (host.MeasureRange is not null) return host.MeasureRange(range.Head, range.Start, range.Tail, range.End);
var result = new List<BrowserRect>();
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<INode> 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;
}
}
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 List<WeakReference<ObjectInstance>> ranges = [];
private JsValue WrapRange(IRange range)
{
if (ranges.Count >= limits.MaximumPendingTasks * 2)
{
ranges.RemoveAll(reference => !reference.TryGetTarget(out _));
if (ranges.Count >= limits.MaximumPendingTasks * 2) throw new BrowserLimitException("The live DOM Range limit was reached.");
}
var wrapper = new BrowserHostObject(engine); ranges.Add(new(wrapper));
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<BrowserRect> RangeRects(IRange range)
{
if (host.MeasureRange is not null) return host.MeasureRange(range.Head, range.Start, range.Tail, range.End);
var result = new List<BrowserRect>();
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<INode> 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;
}
}