From 5f459c090127a60f9d02132dd5d250c15e482b3e Mon Sep 17 00:00:00 2001 From: egrs Date: Sat, 21 Feb 2026 14:43:41 +0100 Subject: [PATCH 1/2] cache document.implementation for object identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getImplementation() now returns a cached *DOMImplementation pointer per Document, matching the getStyleSheets() pattern. This ensures document.implementation === document.implementation holds true. Flips dom/nodes/Document-implementation.html (1/2 → 2/2). --- src/browser/webapi/Document.zig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 85f9c35f..4ee70ded 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -52,6 +52,7 @@ _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, _removed_ids: std.StringHashMapUnmanaged(void) = .empty, _active_element: ?*Element = null, _style_sheets: ?*StyleSheetList = null, +_implementation: ?*DOMImplementation = null, _write_insertion_point: ?*Node = null, _script_created_parser: ?Parser.Streaming = null, _adopted_style_sheets: ?js.Object.Global = null, @@ -272,8 +273,11 @@ pub fn querySelectorAll(self: *Document, input: String, page: *Page) !*Selector. return Selector.querySelectorAll(self.asNode(), input.str(), page); } -pub fn getImplementation(_: *const Document) DOMImplementation { - return .{}; +pub fn getImplementation(self: *Document, page: *Page) !*DOMImplementation { + if (self._implementation) |impl| return impl; + const impl = try page._factory.create(DOMImplementation{}); + self._implementation = impl; + return impl; } pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment { @@ -726,6 +730,7 @@ pub fn open(self: *Document, page: *Page) !*Document { self._elements_by_id.clearAndFree(page.arena); self._active_element = null; self._style_sheets = null; + self._implementation = null; self._ready_state = .loading; self._script_created_parser = Parser.Streaming.init(page.arena, doc_node, page); From 39f9209374afbe22c518b5ee6c58fcb0646d93d4 Mon Sep 17 00:00:00 2001 From: egrs Date: Sat, 21 Feb 2026 14:43:45 +0100 Subject: [PATCH 2/2] fix file input value getter/setter per spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getValue() returns "" for file inputs regardless of value attribute - setValue("") is a no-op for file inputs (was throwing) - setValue(non-empty) still throws InvalidStateError - add _setValue bridge wrapper for [LegacyNullToEmptyString]: null → "" Flips html/semantics/forms/the-input-element/valueMode.html (38/40 → 40/40). --- src/browser/webapi/element/html/Input.zig | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index 0928a9c9..831c5383 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -125,6 +125,7 @@ pub fn setType(self: *Input, typ: []const u8, page: *Page) !void { } pub fn getValue(self: *const Input) []const u8 { + if (self._input_type == .file) return ""; return self._value orelse self._default_value orelse switch (self._input_type) { .checkbox, .radio => "on", else => "", @@ -132,8 +133,9 @@ pub fn getValue(self: *const Input) []const u8 { } pub fn setValue(self: *Input, value: []const u8, page: *Page) !void { - // File inputs cannot have their value set programmatically for security reasons + // File inputs: setting to empty string is a no-op, anything else throws if (self._input_type == .file) { + if (value.len == 0) return; return error.InvalidStateError; } // This should _not_ call setAttribute. It updates the current state only @@ -846,9 +848,17 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; + /// Handles [LegacyNullToEmptyString]: null → "" per HTML spec. + fn setValueFromJS(self: *Input, js_value: js.Value, page: *Page) !void { + if (js_value.isNull()) { + return self.setValue("", page); + } + return self.setValue(try js_value.toZig([]const u8), page); + } + pub const onselectionchange = bridge.accessor(Input.getOnSelectionChange, Input.setOnSelectionChange, .{}); pub const @"type" = bridge.accessor(Input.getType, Input.setType, .{}); - pub const value = bridge.accessor(Input.getValue, Input.setValue, .{ .dom_exception = true }); + pub const value = bridge.accessor(Input.getValue, setValueFromJS, .{ .dom_exception = true }); pub const defaultValue = bridge.accessor(Input.getDefaultValue, Input.setDefaultValue, .{}); pub const checked = bridge.accessor(Input.getChecked, Input.setChecked, .{}); pub const defaultChecked = bridge.accessor(Input.getDefaultChecked, Input.setDefaultChecked, .{});