mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
spec compliance: missing validation guards
- Event.preventDefault() and returnValue respect cancelable=false - MutationObserver.observe() validates options per spec - detached checkbox/radio click suppresses input/change events - doctype insertion into non-document throws HierarchyRequestError - error.TypeError maps to JS TypeError (not generic Error) - enable dom_exception on Element/DocumentFragment mutation methods
This commit is contained in:
@@ -681,9 +681,10 @@ const ActivationState = struct {
|
||||
}
|
||||
|
||||
// Commit: fire input and change events only if state actually changed
|
||||
// and the element is connected to a document (detached elements don't fire).
|
||||
// For checkboxes, state always changes. For radios, only if was unchecked.
|
||||
const state_changed = (input._input_type == .checkbox) or !self.old_checked;
|
||||
if (state_changed) {
|
||||
if (state_changed and input.asElement().asNode().isConnected()) {
|
||||
fireEvent(page, input, "input") catch |err| {
|
||||
log.warn(.event, "input event", .{ .err = err });
|
||||
};
|
||||
|
||||
@@ -311,6 +311,7 @@ fn handleError(comptime T: type, comptime F: type, local: *const Local, err: any
|
||||
const js_err: *const v8.Value = switch (err) {
|
||||
error.TryCatchRethrow => return,
|
||||
error.InvalidArgument => isolate.createTypeError("invalid argument"),
|
||||
error.TypeError => isolate.createTypeError(""),
|
||||
error.OutOfMemory => isolate.createError("out of memory"),
|
||||
error.IllegalConstructor => isolate.createError("Illegal Contructor"),
|
||||
else => blk: {
|
||||
|
||||
@@ -256,9 +256,9 @@ pub const JsApi = struct {
|
||||
pub const childElementCount = bridge.accessor(DocumentFragment.getChildElementCount, null, .{});
|
||||
pub const firstElementChild = bridge.accessor(DocumentFragment.firstElementChild, null, .{});
|
||||
pub const lastElementChild = bridge.accessor(DocumentFragment.lastElementChild, null, .{});
|
||||
pub const append = bridge.function(DocumentFragment.append, .{});
|
||||
pub const prepend = bridge.function(DocumentFragment.prepend, .{});
|
||||
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{});
|
||||
pub const append = bridge.function(DocumentFragment.append, .{ .dom_exception = true });
|
||||
pub const prepend = bridge.function(DocumentFragment.prepend, .{ .dom_exception = true });
|
||||
pub const replaceChildren = bridge.function(DocumentFragment.replaceChildren, .{ .dom_exception = true });
|
||||
pub const innerHTML = bridge.accessor(_innerHTML, DocumentFragment.setInnerHTML, .{});
|
||||
|
||||
fn _innerHTML(self: *DocumentFragment, page: *Page) ![]const u8 {
|
||||
|
||||
@@ -1595,12 +1595,12 @@ pub const JsApi = struct {
|
||||
return self.attachShadow(init.mode, page);
|
||||
}
|
||||
pub const replaceChildren = bridge.function(Element.replaceChildren, .{});
|
||||
pub const replaceWith = bridge.function(Element.replaceWith, .{});
|
||||
pub const replaceWith = bridge.function(Element.replaceWith, .{ .dom_exception = true });
|
||||
pub const remove = bridge.function(Element.remove, .{});
|
||||
pub const append = bridge.function(Element.append, .{});
|
||||
pub const prepend = bridge.function(Element.prepend, .{});
|
||||
pub const before = bridge.function(Element.before, .{});
|
||||
pub const after = bridge.function(Element.after, .{});
|
||||
pub const append = bridge.function(Element.append, .{ .dom_exception = true });
|
||||
pub const prepend = bridge.function(Element.prepend, .{ .dom_exception = true });
|
||||
pub const before = bridge.function(Element.before, .{ .dom_exception = true });
|
||||
pub const after = bridge.function(Element.after, .{ .dom_exception = true });
|
||||
pub const firstElementChild = bridge.accessor(Element.firstElementChild, null, .{});
|
||||
pub const lastElementChild = bridge.accessor(Element.lastElementChild, null, .{});
|
||||
pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
|
||||
|
||||
@@ -189,7 +189,9 @@ pub fn getCurrentTarget(self: *const Event) ?*EventTarget {
|
||||
}
|
||||
|
||||
pub fn preventDefault(self: *Event) void {
|
||||
self._prevent_default = true;
|
||||
if (self._cancelable) {
|
||||
self._prevent_default = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stopPropagation(self: *Event) void {
|
||||
@@ -210,7 +212,12 @@ pub fn getReturnValue(self: *const Event) bool {
|
||||
}
|
||||
|
||||
pub fn setReturnValue(self: *Event, v: bool) void {
|
||||
self._prevent_default = !v;
|
||||
if (!v) {
|
||||
// Setting returnValue=false is equivalent to preventDefault()
|
||||
if (self._cancelable) {
|
||||
self._prevent_default = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getCancelBubble(self: *const Event) bool {
|
||||
|
||||
@@ -49,10 +49,11 @@ node: std.DoublyLinkedList.Node = .{},
|
||||
|
||||
const Observing = struct {
|
||||
target: *Node,
|
||||
options: ObserveOptions,
|
||||
options: ResolvedOptions,
|
||||
};
|
||||
|
||||
pub const ObserveOptions = struct {
|
||||
/// Internal options with all nullable bools resolved to concrete values.
|
||||
const ResolvedOptions = struct {
|
||||
attributes: bool = false,
|
||||
attributeOldValue: bool = false,
|
||||
childList: bool = false,
|
||||
@@ -62,6 +63,16 @@ pub const ObserveOptions = struct {
|
||||
attributeFilter: ?[]const []const u8 = null,
|
||||
};
|
||||
|
||||
pub const ObserveOptions = struct {
|
||||
attributes: ?bool = null,
|
||||
attributeOldValue: ?bool = null,
|
||||
childList: bool = false,
|
||||
characterData: ?bool = null,
|
||||
characterDataOldValue: ?bool = null,
|
||||
subtree: bool = false,
|
||||
attributeFilter: ?[]const []const u8 = null,
|
||||
};
|
||||
|
||||
pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
|
||||
const arena = try page.getArena(.{ .debug = "MutationObserver" });
|
||||
errdefer page.releaseArena(arena);
|
||||
@@ -88,24 +99,61 @@ pub fn deinit(self: *MutationObserver, shutdown: bool) void {
|
||||
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
||||
const arena = self._arena;
|
||||
|
||||
// Per spec: if attributeOldValue/attributeFilter present and attributes
|
||||
// not explicitly set, imply attributes=true. Same for characterData.
|
||||
var resolved = options;
|
||||
if (resolved.attributes == null and (resolved.attributeOldValue != null or resolved.attributeFilter != null)) {
|
||||
resolved.attributes = true;
|
||||
}
|
||||
if (resolved.characterData == null and resolved.characterDataOldValue != null) {
|
||||
resolved.characterData = true;
|
||||
}
|
||||
|
||||
const attributes = resolved.attributes orelse false;
|
||||
const character_data = resolved.characterData orelse false;
|
||||
|
||||
// Validate: at least one of childList/attributes/characterData must be true
|
||||
if (!resolved.childList and !attributes and !character_data) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
// Validate: attributeOldValue/attributeFilter require attributes != false
|
||||
if ((resolved.attributeOldValue orelse false) and !attributes) {
|
||||
return error.TypeError;
|
||||
}
|
||||
if (resolved.attributeFilter != null and !attributes) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
// Validate: characterDataOldValue requires characterData != false
|
||||
if ((resolved.characterDataOldValue orelse false) and !character_data) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
// Build resolved options with concrete bool values
|
||||
var store_options = ResolvedOptions{
|
||||
.attributes = attributes,
|
||||
.attributeOldValue = resolved.attributeOldValue orelse false,
|
||||
.childList = resolved.childList,
|
||||
.characterData = character_data,
|
||||
.characterDataOldValue = resolved.characterDataOldValue orelse false,
|
||||
.subtree = resolved.subtree,
|
||||
.attributeFilter = resolved.attributeFilter,
|
||||
};
|
||||
|
||||
// Deep copy attributeFilter if present
|
||||
var copied_options = options;
|
||||
if (options.attributeFilter) |filter| {
|
||||
const filter_copy = try arena.alloc([]const u8, filter.len);
|
||||
for (filter, 0..) |name, i| {
|
||||
filter_copy[i] = try arena.dupe(u8, name);
|
||||
}
|
||||
copied_options.attributeFilter = filter_copy;
|
||||
}
|
||||
|
||||
if (options.characterDataOldValue) {
|
||||
copied_options.characterData = true;
|
||||
store_options.attributeFilter = filter_copy;
|
||||
}
|
||||
|
||||
// Check if already observing this target
|
||||
for (self._observing.items) |*obs| {
|
||||
if (obs.target == target) {
|
||||
obs.options = copied_options;
|
||||
obs.options = store_options;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +166,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
||||
|
||||
try self._observing.append(arena, .{
|
||||
.target = target,
|
||||
.options = copied_options,
|
||||
.options = store_options,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +209,11 @@ fn validateNodeInsertion(parent: *Node, node: *Node) !void {
|
||||
if (node._type == .attribute) {
|
||||
return error.HierarchyError;
|
||||
}
|
||||
|
||||
// Doctype nodes can only be inserted into a Document
|
||||
if (node._type == .document_type and parent._type != .document) {
|
||||
return error.HierarchyError;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
|
||||
|
||||
Reference in New Issue
Block a user