improve domexception

This commit is contained in:
Karl Seguin
2025-12-16 19:16:42 +08:00
parent 761b35b199
commit 8873e613d2
11 changed files with 56 additions and 47 deletions

View File

@@ -91,8 +91,7 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
const global_template = js_global.getInstanceTemplate(); const global_template = js_global.getInstanceTemplate();
global_template.setInternalFieldCount(1); global_template.setInternalFieldCount(1);
// Configure the missing property callback on the global // Configure the missing property callback on the global object.
// object.
if (global_callback != null) { if (global_callback != null) {
const configuration = v8.NamedPropertyHandlerConfiguration{ const configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct { .getter = struct {
@@ -162,15 +161,17 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
} }
errdefer if (enter) handle_scope.?.deinit(); errdefer if (enter) handle_scope.?.deinit();
const js_global = v8_context.getGlobal();
{ {
// If we want to overwrite the built-in console, we have to // If we want to overwrite the built-in console, we have to
// delete the built-in one. // delete the built-in one.
const js_obj = v8_context.getGlobal();
const console_key = v8.String.initUtf8(isolate, "console"); const console_key = v8.String.initUtf8(isolate, "console");
if (js_obj.deleteValue(v8_context, console_key) == false) { if (js_global.deleteValue(v8_context, console_key) == false) {
return error.ConsoleDeleteError; return error.ConsoleDeleteError;
} }
} }
const context_id = env.context_id; const context_id = env.context_id;
env.context_id = context_id + 1; env.context_id = context_id + 1;
@@ -195,17 +196,11 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool, global_cal
v8_context.setEmbedderData(1, data); v8_context.setEmbedderData(1, data);
} }
// @ZIGDOM
// Custom exception // Custom exception
// NOTE: there is no way in v8 to subclass the Error built-in type // TODO: this is an horrible hack, I can't figure out how to do this cleanly.
// TODO: this is an horrible hack {
// inline for (JsApi) |JsApi| { _ = try context.exec("DOMException.prototype.__proto__ = Error.prototype", "errorSubclass");
// const Struct = s.defaultValue().?; }
// if (@hasDecl(Struct, "ErrorSet")) {
// const script = comptime JsApi.Meta.name ++ ".prototype.__proto__ = Error.prototype";
// _ = try context.exec(script, "errorSubclass");
// }
// }
try context.setupGlobal(); try context.setupGlobal();
return context; return context;

View File

@@ -23,7 +23,7 @@
<main>Main content</main> <main>Main content</main>
<script id=byId name="test1"> <script id=byId name="test1">
testing.expectError("Syntax Error", () => document.querySelector('')); testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
testing.withError((err) => { testing.withError((err) => {
testing.expectEqual(12, err.code); testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name); testing.expectEqual("SyntaxError", err.name);

View File

@@ -32,7 +32,7 @@
</script> </script>
<script id=script1 name="test1"> <script id=script1 name="test1">
testing.expectError("Syntax Error", () => document.querySelectorAll('')); testing.expectError("SyntaxError: Syntax Error", () => document.querySelectorAll(''));
testing.withError((err) => { testing.withError((err) => {
testing.expectEqual(12, err.code); testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name); testing.expectEqual("SyntaxError", err.name);

View File

@@ -120,16 +120,15 @@
<div id="content"> <div id="content">
<a id="link" href="foo" class="ok">OK</a> <a id="link" href="foo" class="ok">OK</a>
</div> </div>
<!-- <script id=hierarchy_error>
<script id=hierarchy_error>
let link = $('#link'); let link = $('#link');
let content = $('#content'); let content = $('#content');
testing.withError((err) => { testing.withError((err) => {
const msg = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
testing.expectEqual(3, err.code); testing.expectEqual(3, err.code);
testing.expectEqual(msg, err.message); testing.expectEqual('Hierarchy Error', err.message);
testing.expectEqual('HierarchyRequestError: ' + msg, err.toString());
testing.expectEqual(true, err instanceof DOMException); testing.expectEqual(true, err instanceof DOMException);
testing.expectEqual(true, err instanceof Error); testing.expectEqual(true, err instanceof Error);
}, () => link.appendChild(content)); }, () => link.appendChild(content));
</script> --> </script>

View File

@@ -66,7 +66,7 @@
{ {
const container = $('#test-container'); const container = $('#test-container');
testing.expectError("Syntax Error", () => container.matches('')); testing.expectError("SyntaxError: Syntax Error", () => container.matches(''));
testing.withError((err) => { testing.withError((err) => {
testing.expectEqual(12, err.code); testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name); testing.expectEqual("SyntaxError", err.name);

View File

@@ -12,7 +12,7 @@
const p1 = $('#p1'); const p1 = $('#p1');
testing.expectEqual(null, p1.querySelector('#p1')); testing.expectEqual(null, p1.querySelector('#p1'));
testing.expectError("Syntax Error", () => p1.querySelector('')); testing.expectError("SyntaxError: Syntax Error", () => p1.querySelector(''));
testing.withError((err) => { testing.withError((err) => {
testing.expectEqual(12, err.code); testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name); testing.expectEqual("SyntaxError", err.name);

View File

@@ -24,7 +24,7 @@
<script id=errors> <script id=errors>
{ {
const root = $('#root'); const root = $('#root');
testing.expectError("Syntax Error", () => root.querySelectorAll('')); testing.expectError("SyntaxError: Syntax Error", () => root.querySelectorAll(''));
testing.withError((err) => { testing.withError((err) => {
testing.expectEqual(12, err.code); testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name); testing.expectEqual("SyntaxError", err.name);

View File

@@ -45,8 +45,8 @@
const container = $('#container'); const container = $('#container');
// Empty selectors // Empty selectors
testing.expectError("Syntax Error", () => container.querySelector('')); testing.expectError("SyntaxError: Syntax Error", () => container.querySelector(''));
testing.expectError("Syntax Error", () => document.querySelectorAll('')); testing.expectError("SyntaxError: Syntax Error", () => document.querySelectorAll(''));
} }
</script> </script>

View File

@@ -10,10 +10,9 @@
let link = $('#link'); let link = $('#link');
testing.withError((err) => { testing.withError((err) => {
const msg = "Failed to execute 'appendChild' on 'Node': The new child element contains the parent.";
testing.expectEqual(3, err.code); testing.expectEqual(3, err.code);
testing.expectEqual(msg, err.message); testing.expectEqual('Hierarchy Error', err.message);
testing.expectEqual('HierarchyRequestError: ' + msg, err.toString()); testing.expectEqual('HierarchyRequestError: Hierarchy Error', err.toString());
testing.expectEqual(true, err instanceof DOMException); testing.expectEqual(true, err instanceof DOMException);
testing.expectEqual(true, err instanceof Error); testing.expectEqual(true, err instanceof Error);
}, () => link.appendChild(content)); }, () => link.appendChild(content));

View File

@@ -23,16 +23,16 @@ const Page = @import("../Page.zig");
const DOMException = @This(); const DOMException = @This();
_code: Code = .none, _code: Code = .none,
_custom_message: ?[]const u8 = null,
_custom_name: ?[]const u8 = null, _custom_name: ?[]const u8 = null,
_custom_message: ?[]const u8 = null,
pub fn init(message: ?[]const u8, name: ?[]const u8) DOMException { pub fn init(message: ?[]const u8, name: ?[]const u8) DOMException {
// If name is provided, try to map it to a legacy code // If name is provided, try to map it to a legacy code
const code = if (name) |n| Code.fromName(n) else .none; const code = if (name) |n| Code.fromName(n) else .none;
return .{ return .{
._code = code, ._code = code,
._custom_message = message,
._custom_name = name, ._custom_name = name,
._custom_message = message,
}; };
} }
@@ -104,8 +104,8 @@ pub fn getMessage(self: *const DOMException) []const u8 {
} }
return switch (self._code) { return switch (self._code) {
.none => "", .none => "",
.invalid_character_error => "Error: Invalid Character", .invalid_character_error => "Invalid Character",
.index_size_error => "IndexSizeError: Index or size is negative or greater than the allowed amount", .index_size_error => "Index or size is negative or greater than the allowed amount",
.syntax_error => "Syntax Error", .syntax_error => "Syntax Error",
.not_supported => "Not Supported", .not_supported => "Not Supported",
.not_found => "Not Found", .not_found => "Not Found",
@@ -114,14 +114,17 @@ pub fn getMessage(self: *const DOMException) []const u8 {
}; };
} }
pub fn toString(self: *const DOMException) []const u8 { pub fn toString(self: *const DOMException, page: *Page) ![]const u8 {
if (self._custom_message) |msg| { const msg = blk: {
return msg; if (self._custom_message) |msg| {
} break :blk msg;
return switch (self._code) { }
.none => "Error", switch (self._code) {
else => self.getMessage(), .none => return "Error",
else => break :blk self.getMessage(),
}
}; };
return std.fmt.bufPrint(&page.buf, "{s}: {s}", .{ self.getName(), msg }) catch return msg;
} }
pub fn className(_: *const DOMException) []const u8 { pub fn className(_: *const DOMException) []const u8 {

View File

@@ -190,12 +190,27 @@ pub fn parentElement(self: *const Node) ?*Element {
return parent.is(Element); return parent.is(Element);
} }
// Validates that a node can be inserted as a child of parent.
fn validateNodeInsertion(parent: *Node, node: *Node) !void {
// Check if parent is a valid type to have children
if (parent._type != .document and parent._type != .element and parent._type != .document_fragment) {
return error.HierarchyError;
}
// Check if node contains parent (would create a cycle)
if (node.contains(parent)) {
return error.HierarchyError;
}
}
pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node { pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
if (child.is(DocumentFragment)) |_| { if (child.is(DocumentFragment)) |_| {
try page.appendAllChildren(child, self); try page.appendAllChildren(child, self);
return child; return child;
} }
try validateNodeInsertion(self, child);
page.domChanged(); page.domChanged();
// If the child is currently connected, and if its new parent is connected, // If the child is currently connected, and if its new parent is connected,
@@ -435,6 +450,8 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
return new_node; return new_node;
} }
try validateNodeInsertion(self, new_node);
const child_already_connected = new_node.isConnected(); const child_already_connected = new_node.isConnected();
// Check if we're adopting the node to a different document // Check if we're adopting the node to a different document
const child_root = new_node.getRootNode(null); const child_root = new_node.getRootNode(null);
@@ -464,12 +481,8 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, page: *Page
if (old_child._parent == null or old_child._parent.? != self) { if (old_child._parent == null or old_child._parent.? != self) {
return error.HierarchyError; return error.HierarchyError;
} }
if (self._type != .document and self._type != .element) {
return error.HierarchyError; try validateNodeInsertion(self, new_child);
}
if (new_child.contains(self)) {
return error.HierarchyError;
}
_ = try self.insertBefore(new_child, old_child, page); _ = try self.insertBefore(new_child, old_child, page);
page.removeNode(self, old_child, .{ .will_be_reconnected = false }); page.removeNode(self, old_child, .{ .will_be_reconnected = false });
@@ -840,7 +853,7 @@ pub const JsApi = struct {
pub const previousSibling = bridge.accessor(Node.previousSibling, null, .{}); pub const previousSibling = bridge.accessor(Node.previousSibling, null, .{});
pub const parentNode = bridge.accessor(Node.parentNode, null, .{}); pub const parentNode = bridge.accessor(Node.parentNode, null, .{});
pub const parentElement = bridge.accessor(Node.parentElement, null, .{}); pub const parentElement = bridge.accessor(Node.parentElement, null, .{});
pub const appendChild = bridge.function(Node.appendChild, .{}); pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true });
pub const childNodes = bridge.accessor(Node.childNodes, null, .{}); pub const childNodes = bridge.accessor(Node.childNodes, null, .{});
pub const isConnected = bridge.accessor(Node.isConnected, null, .{}); pub const isConnected = bridge.accessor(Node.isConnected, null, .{});
pub const ownerDocument = bridge.accessor(Node.ownerDocument, null, .{}); pub const ownerDocument = bridge.accessor(Node.ownerDocument, null, .{});