From 15358c1928188a6594158a8fb37a5ad4bc38618e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 6 Jan 2026 20:27:00 +0800 Subject: [PATCH] Improve Range, adding missing functions and more validation --- src/browser/tests/range.html | 444 +++++++++++++++++++++++++++ src/browser/webapi/AbstractRange.zig | 23 +- src/browser/webapi/Range.zig | 239 ++++++++++++-- 3 files changed, 685 insertions(+), 21 deletions(-) diff --git a/src/browser/tests/range.html b/src/browser/tests/range.html index 3a9f609f..73a43ff7 100644 --- a/src/browser/tests/range.html +++ b/src/browser/tests/range.html @@ -376,3 +376,447 @@ testing.expectEqual('Bold', fragment.childNodes[0].textContent); } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/AbstractRange.zig b/src/browser/webapi/AbstractRange.zig index b23fb8ca..5f3edc31 100644 --- a/src/browser/webapi/AbstractRange.zig +++ b/src/browser/webapi/AbstractRange.zig @@ -69,6 +69,19 @@ pub fn getCollapsed(self: *const AbstractRange) bool { self._start_offset == self._end_offset; } +pub fn getCommonAncestorContainer(self: *const AbstractRange) *Node { + // Let container be start container + var container = self._start_container; + + // While container is not an inclusive ancestor of end container + while (!isInclusiveAncestorOf(container, self._end_container)) { + // Let container be container's parent + container = container.parentNode() orelse break; + } + + return container; +} + pub fn isStartAfterEnd(self: *const AbstractRange) bool { return compareBoundaryPoints( self._start_container, @@ -84,7 +97,7 @@ const BoundaryComparison = enum { after, }; -fn compareBoundaryPoints( +pub fn compareBoundaryPoints( node_a: *Node, offset_a: u32, node_b: *Node, @@ -195,6 +208,13 @@ fn isAncestorOf(potential_ancestor: *Node, node: *Node) bool { return false; } +fn isInclusiveAncestorOf(potential_ancestor: *Node, node: *Node) bool { + if (potential_ancestor == node) { + return true; + } + return isAncestorOf(potential_ancestor, node); +} + pub const JsApi = struct { pub const bridge = js.Bridge(AbstractRange); @@ -209,4 +229,5 @@ pub const JsApi = struct { pub const endContainer = bridge.accessor(AbstractRange.getEndContainer, null, .{}); pub const endOffset = bridge.accessor(AbstractRange.getEndOffset, null, .{}); pub const collapsed = bridge.accessor(AbstractRange.getCollapsed, null, .{}); + pub const commonAncestorContainer = bridge.accessor(AbstractRange.getCommonAncestorContainer, null, .{}); }; diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index feca23c4..ce9b0d6d 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -37,22 +37,35 @@ pub fn init(page: *Page) !*Range { } pub fn setStart(self: *Range, node: *Node, offset: u32) !void { + if (offset > node.getLength()) { + return error.IndexSizeError; + } + self._proto._start_container = node; self._proto._start_offset = offset; - // If start is now after end, collapse to start - if (self._proto.isStartAfterEnd()) { + // If start is now after end, or nodes are in different trees, collapse to start + const end_root = self._proto._end_container.getRootNode(null); + const start_root = node.getRootNode(null); + if (end_root != start_root or self._proto.isStartAfterEnd()) { self._proto._end_container = self._proto._start_container; self._proto._end_offset = self._proto._start_offset; } } pub fn setEnd(self: *Range, node: *Node, offset: u32) !void { + // Validate offset + if (offset > node.getLength()) { + return error.IndexSizeError; + } + self._proto._end_container = node; self._proto._end_offset = offset; - // If end is now before start, collapse to end - if (self._proto.isStartAfterEnd()) { + // If end is now before start, or nodes are in different trees, collapse to end + const start_root = self._proto._start_container.getRootNode(null); + const end_root = node.getRootNode(null); + if (start_root != end_root or self._proto.isStartAfterEnd()) { self._proto._start_container = self._proto._end_container; self._proto._start_offset = self._proto._end_offset; } @@ -105,6 +118,181 @@ pub fn collapse(self: *Range, to_start: ?bool) void { } } +pub fn detach(_: *Range) void { + // Legacy no-op method kept for backwards compatibility + // Modern spec: "The detach() method must do nothing." +} + +pub fn compareBoundaryPoints(self: *const Range, how_raw: i32, source_range: *const Range) !i16 { + // Convert how parameter per WebIDL unsigned short conversion + // This handles negative numbers and out-of-range values + const how_mod = @mod(how_raw, 65536); + const how: u16 = if (how_mod < 0) @intCast(@as(i32, how_mod) + 65536) else @intCast(how_mod); + + // If how is not one of 0, 1, 2, or 3, throw NotSupportedError + if (how > 3) { + return error.NotSupported; + } + + // If the two ranges' root is different, throw WrongDocumentError + const this_root = self._proto._start_container.getRootNode(null); + const source_root = source_range._proto._start_container.getRootNode(null); + if (this_root != source_root) { + return error.WrongDocument; + } + + // Determine which boundary points to compare based on how parameter + const result = switch (how) { + 0 => AbstractRange.compareBoundaryPoints( // START_TO_START + self._proto._start_container, + self._proto._start_offset, + source_range._proto._start_container, + source_range._proto._start_offset, + ), + 1 => AbstractRange.compareBoundaryPoints( // START_TO_END + self._proto._start_container, + self._proto._start_offset, + source_range._proto._end_container, + source_range._proto._end_offset, + ), + 2 => AbstractRange.compareBoundaryPoints( // END_TO_END + self._proto._end_container, + self._proto._end_offset, + source_range._proto._end_container, + source_range._proto._end_offset, + ), + 3 => AbstractRange.compareBoundaryPoints( // END_TO_START + self._proto._end_container, + self._proto._end_offset, + source_range._proto._start_container, + source_range._proto._start_offset, + ), + else => unreachable, + }; + + return switch (result) { + .before => -1, + .equal => 0, + .after => 1, + }; +} + +pub fn comparePoint(self: *const Range, node: *Node, offset: u32) !i16 { + if (offset > node.getLength()) { + return error.IndexSizeError; + } + + // Check if node is in a different tree than the range + const node_root = node.getRootNode(null); + const start_root = self._proto._start_container.getRootNode(null); + if (node_root != start_root) { + return error.WrongDocument; + } + + // Compare point with start boundary + const cmp_start = AbstractRange.compareBoundaryPoints( + node, + offset, + self._proto._start_container, + self._proto._start_offset, + ); + + if (cmp_start == .before) { + return -1; + } + + const cmp_end = AbstractRange.compareBoundaryPoints( + node, + offset, + self._proto._end_container, + self._proto._end_offset, + ); + + return if (cmp_end == .after) 1 else 0; +} + +pub fn isPointInRange(self: *const Range, node: *Node, offset: u32) !bool { + // If node's root is different from the context object's root, return false + const node_root = node.getRootNode(null); + const start_root = self._proto._start_container.getRootNode(null); + if (node_root != start_root) { + return false; + } + + if (node._type == .document_type) { + return error.InvalidNodeType; + } + + // If offset is greater than node's length, throw IndexSizeError + if (offset > node.getLength()) { + return error.IndexSizeError; + } + + // If (node, offset) is before start or after end, return false + const cmp_start = AbstractRange.compareBoundaryPoints( + node, + offset, + self._proto._start_container, + self._proto._start_offset, + ); + + if (cmp_start == .before) { + return false; + } + + const cmp_end = AbstractRange.compareBoundaryPoints( + node, + offset, + self._proto._end_container, + self._proto._end_offset, + ); + + return cmp_end != .after; +} + +pub fn intersectsNode(self: *const Range, node: *Node) bool { + // If node's root is different from the context object's root, return false + const node_root = node.getRootNode(null); + const start_root = self._proto._start_container.getRootNode(null); + if (node_root != start_root) { + return false; + } + + // Let parent be node's parent + const parent = node.parentNode() orelse { + // If parent is null, return true + return true; + }; + + // Let offset be node's index + const offset = parent.getChildIndex(node) orelse { + // Should not happen if node has a parent + return false; + }; + + // If (parent, offset) is before end and (parent, offset + 1) is after start, return true + const before_end = AbstractRange.compareBoundaryPoints( + parent, + offset, + self._proto._end_container, + self._proto._end_offset, + ); + + const after_start = AbstractRange.compareBoundaryPoints( + parent, + offset + 1, + self._proto._start_container, + self._proto._start_offset, + ); + + if (before_end == .before and after_start == .after) { + return true; + } + + // Return false + return false; +} + pub fn cloneRange(self: *const Range, page: *Page) !*Range { const clone = try page._factory.abstractRange(Range{ ._proto = undefined }, page); clone._proto._end_offset = self._proto._end_offset; @@ -308,24 +496,35 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + // Constants for compareBoundaryPoints + pub const START_TO_START = bridge.property(0); + pub const START_TO_END = bridge.property(1); + pub const END_TO_END = bridge.property(2); + pub const END_TO_START = bridge.property(3); + pub const constructor = bridge.constructor(Range.init, .{}); - pub const setStart = bridge.function(Range.setStart, .{}); - pub const setEnd = bridge.function(Range.setEnd, .{}); - pub const setStartBefore = bridge.function(Range.setStartBefore, .{}); - pub const setStartAfter = bridge.function(Range.setStartAfter, .{}); - pub const setEndBefore = bridge.function(Range.setEndBefore, .{}); - pub const setEndAfter = bridge.function(Range.setEndAfter, .{}); - pub const selectNode = bridge.function(Range.selectNode, .{}); + pub const setStart = bridge.function(Range.setStart, .{ .dom_exception = true }); + pub const setEnd = bridge.function(Range.setEnd, .{ .dom_exception = true }); + pub const setStartBefore = bridge.function(Range.setStartBefore, .{ .dom_exception = true }); + pub const setStartAfter = bridge.function(Range.setStartAfter, .{ .dom_exception = true }); + pub const setEndBefore = bridge.function(Range.setEndBefore, .{ .dom_exception = true }); + pub const setEndAfter = bridge.function(Range.setEndAfter, .{ .dom_exception = true }); + pub const selectNode = bridge.function(Range.selectNode, .{ .dom_exception = true }); pub const selectNodeContents = bridge.function(Range.selectNodeContents, .{}); - pub const collapse = bridge.function(Range.collapse, .{}); - pub const cloneRange = bridge.function(Range.cloneRange, .{}); - pub const insertNode = bridge.function(Range.insertNode, .{}); - pub const deleteContents = bridge.function(Range.deleteContents, .{}); - pub const cloneContents = bridge.function(Range.cloneContents, .{}); - pub const extractContents = bridge.function(Range.extractContents, .{}); - pub const surroundContents = bridge.function(Range.surroundContents, .{}); - pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{}); - pub const toString = bridge.function(Range.toString, .{}); + pub const collapse = bridge.function(Range.collapse, .{ .dom_exception = true }); + pub const detach = bridge.function(Range.detach, .{}); + pub const compareBoundaryPoints = bridge.function(Range.compareBoundaryPoints, .{ .dom_exception = true }); + pub const comparePoint = bridge.function(Range.comparePoint, .{ .dom_exception = true }); + pub const isPointInRange = bridge.function(Range.isPointInRange, .{ .dom_exception = true }); + pub const intersectsNode = bridge.function(Range.intersectsNode, .{}); + pub const cloneRange = bridge.function(Range.cloneRange, .{ .dom_exception = true }); + pub const insertNode = bridge.function(Range.insertNode, .{ .dom_exception = true }); + pub const deleteContents = bridge.function(Range.deleteContents, .{ .dom_exception = true }); + pub const cloneContents = bridge.function(Range.cloneContents, .{ .dom_exception = true }); + pub const extractContents = bridge.function(Range.extractContents, .{ .dom_exception = true }); + pub const surroundContents = bridge.function(Range.surroundContents, .{ .dom_exception = true }); + pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{ .dom_exception = true }); + pub const toString = bridge.function(Range.toString, .{ .dom_exception = true }); }; const testing = @import("../../testing.zig");