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)
This commit is contained in:
egrs
2026-02-21 10:36:04 +01:00
parent d38ded0f26
commit a90bcde38c
5 changed files with 88 additions and 14 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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 <legend> 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, .{});