From a90bcde38cc195acc0607a068dafd23f5e7bbfc0 Mon Sep 17 00:00:00 2001 From: egrs Date: Sat, 21 Feb 2026 10:36:04 +0100 Subject: [PATCH] fix WPT failures: nodeName prefix case, PI validation, willValidate, maxLength - uppercase entire qualified name in tagName (including prefix) - validate PI data for "?>" and use proper XML Name production with Unicode - implement willValidate on HTMLInputElement - throw IndexSizeError DOMException for negative maxLength assignment flips: Node-nodeName, Document-createProcessingInstruction, button, maxlength, input-willvalidate (+6 subtests) --- src/browser/Page.zig | 66 +++++++++++++++++++++-- src/browser/tests/element/html/input.html | 2 +- src/browser/tests/legacy/html/input.html | 2 +- src/browser/webapi/Element.zig | 7 --- src/browser/webapi/element/html/Input.zig | 25 ++++++++- 5 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index c17eae62..50ec0efc 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -2346,13 +2346,16 @@ pub fn createCDATASection(self: *Page, data: []const u8) !*Node { } pub fn createProcessingInstruction(self: *Page, target: []const u8, data: []const u8) !*Node { - // Validate target doesn't contain "?>" + // Validate neither target nor data contain "?>" if (std.mem.indexOf(u8, target, "?>") != null) { return error.InvalidCharacterError; } + if (std.mem.indexOf(u8, data, "?>") != null) { + return error.InvalidCharacterError; + } - // Validate target follows XML name rules (similar to attribute name validation) - try Element.Attribute.validateAttributeName(.wrap(target)); + // Validate target follows XML Name production + try validateXmlName(target); const owned_target = try self.dupeString(target); const owned_data = try self.dupeString(data); @@ -2374,6 +2377,63 @@ pub fn createProcessingInstruction(self: *Page, target: []const u8, data: []cons return cd.asNode(); } +/// Validate a string against the XML Name production. +/// https://www.w3.org/TR/xml/#NT-Name +fn validateXmlName(name: []const u8) !void { + if (name.len == 0) return error.InvalidCharacterError; + + var i: usize = 0; + + // First character must be a NameStartChar. + const first_len = std.unicode.utf8ByteSequenceLength(name[0]) catch + return error.InvalidCharacterError; + if (first_len > name.len) return error.InvalidCharacterError; + const first_cp = std.unicode.utf8Decode(name[0..][0..first_len]) catch + return error.InvalidCharacterError; + if (!isXmlNameStartChar(first_cp)) return error.InvalidCharacterError; + i = first_len; + + // Subsequent characters must be NameChars. + while (i < name.len) { + const cp_len = std.unicode.utf8ByteSequenceLength(name[i]) catch + return error.InvalidCharacterError; + if (i + cp_len > name.len) return error.InvalidCharacterError; + const cp = std.unicode.utf8Decode(name[i..][0..cp_len]) catch + return error.InvalidCharacterError; + if (!isXmlNameChar(cp)) return error.InvalidCharacterError; + i += cp_len; + } +} + +fn isXmlNameStartChar(c: u21) bool { + return c == ':' or + (c >= 'A' and c <= 'Z') or + c == '_' or + (c >= 'a' and c <= 'z') or + (c >= 0xC0 and c <= 0xD6) or + (c >= 0xD8 and c <= 0xF6) or + (c >= 0xF8 and c <= 0x2FF) or + (c >= 0x370 and c <= 0x37D) or + (c >= 0x37F and c <= 0x1FFF) or + (c >= 0x200C and c <= 0x200D) or + (c >= 0x2070 and c <= 0x218F) or + (c >= 0x2C00 and c <= 0x2FEF) or + (c >= 0x3001 and c <= 0xD7FF) or + (c >= 0xF900 and c <= 0xFDCF) or + (c >= 0xFDF0 and c <= 0xFFFD) or + (c >= 0x10000 and c <= 0xEFFFF); +} + +fn isXmlNameChar(c: u21) bool { + return isXmlNameStartChar(c) or + c == '-' or + c == '.' or + (c >= '0' and c <= '9') or + c == 0xB7 or + (c >= 0x300 and c <= 0x36F) or + (c >= 0x203F and c <= 0x2040); +} + pub fn dupeString(self: *Page, value: []const u8) ![]const u8 { if (String.intern(value)) |v| { return v; diff --git a/src/browser/tests/element/html/input.html b/src/browser/tests/element/html/input.html index 2b071e88..ede10c00 100644 --- a/src/browser/tests/element/html/input.html +++ b/src/browser/tests/element/html/input.html @@ -46,7 +46,7 @@ testing.expectEqual(5, input.maxLength); input.maxLength = 'banana'; testing.expectEqual(0, input.maxLength); - testing.expectError('Error: NegativeValueNotAllowed', () => { input.maxLength = -45;}); + testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => { input.maxLength = -45;}); testing.expectEqual(20, input.size); input.size = 5; diff --git a/src/browser/tests/legacy/html/input.html b/src/browser/tests/legacy/html/input.html index 45963d81..0232ddbf 100644 --- a/src/browser/tests/legacy/html/input.html +++ b/src/browser/tests/legacy/html/input.html @@ -43,7 +43,7 @@ testing.expectEqual(5, input.maxLength); input.maxLength = 'banana'; testing.expectEqual(0, input.maxLength); - testing.expectError('Error: NegativeValueNotAllowed', () => { input.maxLength = -45;}); + testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => { input.maxLength = -45;}); testing.expectEqual(20, input.size); input.size = 5; diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 753d50a7..f37be4da 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -1374,13 +1374,6 @@ fn upperTagName(tag_name: *String, buf: []u8) []const u8 { return tag_name.str(); } const tag = tag_name.str(); - // If the tag_name has a prefix, we must uppercase only the suffix part. - // example: te:st should be returned as te:ST. - if (std.mem.indexOfPos(u8, tag, 0, ":")) |pos| { - @memcpy(buf[0 .. pos + 1], tag[0 .. pos + 1]); - _ = std.ascii.upperString(buf[pos..tag.len], tag[pos..tag.len]); - return buf[0..tag.len]; - } return std.ascii.upperString(buf, tag); } diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index 84dd727f..5793c169 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -182,6 +182,26 @@ pub fn setDefaultChecked(self: *Input, checked: bool, page: *Page) !void { } } +pub fn getWillValidate(self: *const Input) bool { + // An input element is barred from constraint validation if: + // - type is hidden, button, or reset + // - element is disabled + // - element has a datalist ancestor + return switch (self._input_type) { + .hidden, .button, .reset => false, + else => !self.getDisabled() and !self.hasDatalistAncestor(), + }; +} + +fn hasDatalistAncestor(self: *const Input) bool { + var node = self.asConstElement().asConstNode().parentElement(); + while (node) |parent| { + if (parent.is(HtmlElement.DataList) != null) return true; + node = parent.asConstNode().parentElement(); + } + return false; +} + pub fn getDisabled(self: *const Input) bool { // TODO: Also check for disabled fieldset ancestors // (but not if we're inside a of that fieldset) @@ -227,7 +247,7 @@ pub fn getMaxLength(self: *const Input) i32 { pub fn setMaxLength(self: *Input, max_length: i32, page: *Page) !void { if (max_length < 0) { - return error.NegativeValueNotAllowed; + return error.IndexSizeError; } var buf: [32]u8 = undefined; const value = std.fmt.bufPrint(&buf, "{d}", .{max_length}) catch unreachable; @@ -855,7 +875,7 @@ pub const JsApi = struct { pub const accept = bridge.accessor(Input.getAccept, Input.setAccept, .{}); pub const readOnly = bridge.accessor(Input.getReadonly, Input.setReadonly, .{}); pub const alt = bridge.accessor(Input.getAlt, Input.setAlt, .{}); - pub const maxLength = bridge.accessor(Input.getMaxLength, Input.setMaxLength, .{}); + pub const maxLength = bridge.accessor(Input.getMaxLength, Input.setMaxLength, .{ .dom_exception = true }); pub const size = bridge.accessor(Input.getSize, Input.setSize, .{}); pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{}); pub const form = bridge.accessor(Input.getForm, null, .{}); @@ -866,6 +886,7 @@ pub const JsApi = struct { pub const step = bridge.accessor(Input.getStep, Input.setStep, .{}); pub const multiple = bridge.accessor(Input.getMultiple, Input.setMultiple, .{}); pub const autocomplete = bridge.accessor(Input.getAutocomplete, Input.setAutocomplete, .{}); + pub const willValidate = bridge.accessor(Input.getWillValidate, null, .{}); pub const select = bridge.function(Input.select, .{}); pub const selectionStart = bridge.accessor(Input.getSelectionStart, Input.setSelectionStart, .{});