mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-30 17:18:57 +00:00
Expands bridge.property to work as a getter. This previously only worked by
setting a value directly on the TemplatePrototype. This is what you want for
something like Node.TEXT_NODE which is accessible on both Node and an instance
(e.g. document.createElement('div').TEXT_NODE).
Now the property can be configured with .{.template = false}. It essentially
becomes an optimized: bridge.accessor(comptime scalar, null, .{});
There are other accessor that can be converted to this type, but I'll do that
after this is merged to keep this PR manageable.
602 lines
23 KiB
Zig
602 lines
23 KiB
Zig
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
//
|
|
// Francis Bouvier <francis@lightpanda.io>
|
|
// Pierre Tachoire <pierre@lightpanda.io>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const std = @import("std");
|
|
const js = @import("../js/js.zig");
|
|
|
|
const Page = @import("../Page.zig");
|
|
const Node = @import("Node.zig");
|
|
const DocumentFragment = @import("DocumentFragment.zig");
|
|
const AbstractRange = @import("AbstractRange.zig");
|
|
|
|
const Range = @This();
|
|
|
|
_proto: *AbstractRange,
|
|
|
|
pub fn asAbstractRange(self: *Range) *AbstractRange {
|
|
return self._proto;
|
|
}
|
|
|
|
pub fn init(page: *Page) !*Range {
|
|
return page._factory.abstractRange(Range{ ._proto = undefined }, page);
|
|
}
|
|
|
|
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, 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, 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;
|
|
}
|
|
}
|
|
|
|
pub fn setStartBefore(self: *Range, node: *Node) !void {
|
|
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
|
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
|
try self.setStart(parent, offset);
|
|
}
|
|
|
|
pub fn setStartAfter(self: *Range, node: *Node) !void {
|
|
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
|
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
|
try self.setStart(parent, offset + 1);
|
|
}
|
|
|
|
pub fn setEndBefore(self: *Range, node: *Node) !void {
|
|
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
|
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
|
try self.setEnd(parent, offset);
|
|
}
|
|
|
|
pub fn setEndAfter(self: *Range, node: *Node) !void {
|
|
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
|
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
|
try self.setEnd(parent, offset + 1);
|
|
}
|
|
|
|
pub fn selectNode(self: *Range, node: *Node) !void {
|
|
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
|
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
|
try self.setStart(parent, offset);
|
|
try self.setEnd(parent, offset + 1);
|
|
}
|
|
|
|
pub fn selectNodeContents(self: *Range, node: *Node) !void {
|
|
const length = node.getLength();
|
|
try self.setStart(node, 0);
|
|
try self.setEnd(node, length);
|
|
}
|
|
|
|
pub fn collapse(self: *Range, to_start: ?bool) void {
|
|
if (to_start orelse true) {
|
|
self._proto._end_container = self._proto._start_container;
|
|
self._proto._end_offset = self._proto._start_offset;
|
|
} else {
|
|
self._proto._start_container = self._proto._end_container;
|
|
self._proto._start_offset = self._proto._end_offset;
|
|
}
|
|
}
|
|
|
|
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;
|
|
clone._proto._start_offset = self._proto._start_offset;
|
|
clone._proto._end_container = self._proto._end_container;
|
|
clone._proto._start_container = self._proto._start_container;
|
|
return clone;
|
|
}
|
|
|
|
pub fn insertNode(self: *Range, node: *Node, page: *Page) !void {
|
|
// Insert node at the start of the range
|
|
const container = self._proto._start_container;
|
|
const offset = self._proto._start_offset;
|
|
|
|
if (container.is(Node.CData)) |_| {
|
|
// If container is a text node, we need to split it
|
|
const parent = container.parentNode() orelse return error.InvalidNodeType;
|
|
|
|
if (offset == 0) {
|
|
_ = try parent.insertBefore(node, container, page);
|
|
} else {
|
|
const text_data = container.getData();
|
|
if (offset >= text_data.len) {
|
|
_ = try parent.insertBefore(node, container.nextSibling(), page);
|
|
} else {
|
|
// Split the text node into before and after parts
|
|
const before_text = text_data[0..offset];
|
|
const after_text = text_data[offset..];
|
|
|
|
const before = try page.createTextNode(before_text);
|
|
const after = try page.createTextNode(after_text);
|
|
|
|
_ = try parent.replaceChild(before, container, page);
|
|
_ = try parent.insertBefore(node, before.nextSibling(), page);
|
|
_ = try parent.insertBefore(after, node.nextSibling(), page);
|
|
}
|
|
}
|
|
} else {
|
|
// Container is an element, insert at offset
|
|
const ref_child = container.getChildAt(offset);
|
|
_ = try container.insertBefore(node, ref_child, page);
|
|
}
|
|
|
|
// Update range to be after the inserted node
|
|
if (self._proto._start_container == self._proto._end_container) {
|
|
self._proto._end_offset += 1;
|
|
}
|
|
}
|
|
|
|
pub fn deleteContents(self: *Range, page: *Page) !void {
|
|
if (self._proto.getCollapsed()) {
|
|
return;
|
|
}
|
|
|
|
// Simple case: same container
|
|
if (self._proto._start_container == self._proto._end_container) {
|
|
if (self._proto._start_container.is(Node.CData)) |_| {
|
|
// Delete part of text node
|
|
const text_data = self._proto._start_container.getData();
|
|
const new_text = try std.mem.concat(
|
|
page.arena,
|
|
u8,
|
|
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
|
|
);
|
|
try self._proto._start_container.setData(new_text, page);
|
|
} else {
|
|
// Delete child nodes in range
|
|
var offset = self._proto._start_offset;
|
|
while (offset < self._proto._end_offset) : (offset += 1) {
|
|
if (self._proto._start_container.getChildAt(self._proto._start_offset)) |child| {
|
|
_ = try self._proto._start_container.removeChild(child, page);
|
|
}
|
|
}
|
|
}
|
|
self.collapse(true);
|
|
return;
|
|
}
|
|
|
|
// Complex case: different containers
|
|
// Handle start container - if it's a text node, truncate it
|
|
if (self._proto._start_container.is(Node.CData)) |_| {
|
|
const text_data = self._proto._start_container.getData();
|
|
if (self._proto._start_offset < text_data.len) {
|
|
// Keep only the part before start_offset
|
|
const new_text = text_data[0..self._proto._start_offset];
|
|
try self._proto._start_container.setData(new_text, page);
|
|
}
|
|
}
|
|
|
|
// Handle end container - if it's a text node, truncate it
|
|
if (self._proto._end_container.is(Node.CData)) |_| {
|
|
const text_data = self._proto._end_container.getData();
|
|
if (self._proto._end_offset < text_data.len) {
|
|
// Keep only the part from end_offset onwards
|
|
const new_text = text_data[self._proto._end_offset..];
|
|
try self._proto._end_container.setData(new_text, page);
|
|
} else if (self._proto._end_offset == text_data.len) {
|
|
// If we're at the end, set to empty (will be removed if needed)
|
|
try self._proto._end_container.setData("", page);
|
|
}
|
|
}
|
|
|
|
// Remove nodes between start and end containers
|
|
// For now, handle the common case where they're siblings
|
|
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
|
|
var current = self._proto._start_container.nextSibling();
|
|
while (current != null and current != self._proto._end_container) {
|
|
const next = current.?.nextSibling();
|
|
if (current.?.parentNode()) |parent| {
|
|
_ = try parent.removeChild(current.?, page);
|
|
}
|
|
current = next;
|
|
}
|
|
}
|
|
|
|
self.collapse(true);
|
|
}
|
|
|
|
pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
|
|
const fragment = try DocumentFragment.init(page);
|
|
|
|
if (self._proto.getCollapsed()) return fragment;
|
|
|
|
// Simple case: same container
|
|
if (self._proto._start_container == self._proto._end_container) {
|
|
if (self._proto._start_container.is(Node.CData)) |_| {
|
|
// Clone part of text node
|
|
const text_data = self._proto._start_container.getData();
|
|
if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) {
|
|
const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset];
|
|
const text_node = try page.createTextNode(cloned_text);
|
|
_ = try fragment.asNode().appendChild(text_node, page);
|
|
}
|
|
} else {
|
|
// Clone child nodes in range
|
|
var offset = self._proto._start_offset;
|
|
while (offset < self._proto._end_offset) : (offset += 1) {
|
|
if (self._proto._start_container.getChildAt(offset)) |child| {
|
|
const cloned = try child.cloneNode(true, page);
|
|
_ = try fragment.asNode().appendChild(cloned, page);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Complex case: different containers
|
|
// Clone partial start container
|
|
if (self._proto._start_container.is(Node.CData)) |_| {
|
|
const text_data = self._proto._start_container.getData();
|
|
if (self._proto._start_offset < text_data.len) {
|
|
// Clone from start_offset to end of text
|
|
const cloned_text = text_data[self._proto._start_offset..];
|
|
const text_node = try page.createTextNode(cloned_text);
|
|
_ = try fragment.asNode().appendChild(text_node, page);
|
|
}
|
|
}
|
|
|
|
// Clone nodes between start and end containers (siblings case)
|
|
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
|
|
var current = self._proto._start_container.nextSibling();
|
|
while (current != null and current != self._proto._end_container) {
|
|
const cloned = try current.?.cloneNode(true, page);
|
|
_ = try fragment.asNode().appendChild(cloned, page);
|
|
current = current.?.nextSibling();
|
|
}
|
|
}
|
|
|
|
// Clone partial end container
|
|
if (self._proto._end_container.is(Node.CData)) |_| {
|
|
const text_data = self._proto._end_container.getData();
|
|
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
|
|
// Clone from start to end_offset
|
|
const cloned_text = text_data[0..self._proto._end_offset];
|
|
const text_node = try page.createTextNode(cloned_text);
|
|
_ = try fragment.asNode().appendChild(text_node, page);
|
|
}
|
|
}
|
|
}
|
|
|
|
return fragment;
|
|
}
|
|
|
|
pub fn extractContents(self: *Range, page: *Page) !*DocumentFragment {
|
|
const fragment = try self.cloneContents(page);
|
|
try self.deleteContents(page);
|
|
return fragment;
|
|
}
|
|
|
|
pub fn surroundContents(self: *Range, new_parent: *Node, page: *Page) !void {
|
|
// Extract contents
|
|
const contents = try self.extractContents(page);
|
|
|
|
// Insert the new parent
|
|
try self.insertNode(new_parent, page);
|
|
|
|
// Move contents into new parent
|
|
_ = try new_parent.appendChild(contents.asNode(), page);
|
|
|
|
// Select the new parent's contents
|
|
try self.selectNodeContents(new_parent);
|
|
}
|
|
|
|
pub fn createContextualFragment(self: *const Range, html: []const u8, page: *Page) !*DocumentFragment {
|
|
var context_node = self._proto._start_container;
|
|
|
|
// If start container is a text node, use its parent as context
|
|
if (context_node.is(Node.CData)) |_| {
|
|
context_node = context_node.parentNode() orelse context_node;
|
|
}
|
|
|
|
const fragment = try DocumentFragment.init(page);
|
|
|
|
if (html.len == 0) {
|
|
return fragment;
|
|
}
|
|
|
|
// Create a temporary element of the same type as the context for parsing
|
|
// This preserves the parsing context without modifying the original node
|
|
const temp_node = if (context_node.is(Node.Element)) |el|
|
|
try page.createElementNS(el._namespace, el.getTagNameLower(), null)
|
|
else
|
|
try page.createElementNS(.html, "div", null);
|
|
|
|
try page.parseHtmlAsChildren(temp_node, html);
|
|
|
|
// Move all parsed children to the fragment
|
|
// Keep removing first child until temp element is empty
|
|
const fragment_node = fragment.asNode();
|
|
while (temp_node.firstChild()) |child| {
|
|
page.removeNode(temp_node, child, .{ .will_be_reconnected = true });
|
|
try page.appendNode(fragment_node, child, .{ .child_already_connected = false });
|
|
}
|
|
|
|
return fragment;
|
|
}
|
|
|
|
pub fn toString(self: *const Range, page: *Page) ![]const u8 {
|
|
// Simplified implementation: just extract text content
|
|
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
|
try self.writeTextContent(&buf.writer);
|
|
return buf.written();
|
|
}
|
|
|
|
fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
|
if (self._proto.getCollapsed()) {
|
|
return;
|
|
}
|
|
|
|
if (self._proto._start_container == self._proto._end_container) {
|
|
if (self._proto._start_container.is(Node.CData)) |cdata| {
|
|
const data = cdata.getData();
|
|
if (self._proto._start_offset < data.len and self._proto._end_offset <= data.len) {
|
|
try writer.writeAll(data[self._proto._start_offset..self._proto._end_offset]);
|
|
}
|
|
}
|
|
// For elements, would need to iterate children
|
|
return;
|
|
}
|
|
|
|
// Complex case: different containers - would need proper tree walking
|
|
// For now, just return empty
|
|
}
|
|
|
|
pub const JsApi = struct {
|
|
pub const bridge = js.Bridge(Range);
|
|
|
|
pub const Meta = struct {
|
|
pub const name = "Range";
|
|
pub const prototype_chain = bridge.prototypeChain();
|
|
pub var class_id: bridge.ClassId = undefined;
|
|
};
|
|
|
|
// Constants for compareBoundaryPoints
|
|
pub const START_TO_START = bridge.property(0, .{ .template = true });
|
|
pub const START_TO_END = bridge.property(1, .{ .template = true });
|
|
pub const END_TO_END = bridge.property(2, .{ .template = true });
|
|
pub const END_TO_START = bridge.property(3, .{ .template = true });
|
|
|
|
pub const constructor = bridge.constructor(Range.init, .{});
|
|
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, .{ .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");
|
|
test "WebApi: Range" {
|
|
try testing.htmlRunner("range.html", .{});
|
|
}
|