Implement select.options

Add HTMLOptionsCollection and enhance HTMLOptionElement API.

Amazon.
This commit is contained in:
Karl Seguin
2025-07-23 07:39:53 +08:00
parent 7969e047c7
commit 3eb85da02c
8 changed files with 248 additions and 37 deletions

View File

@@ -149,7 +149,9 @@ pub const Document = struct {
tag_name: []const u8, tag_name: []const u8,
page: *Page, page: *Page,
) !collection.HTMLCollection { ) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, true); return try collection.HTMLCollectionByTagName(page.arena, parser.documentToNode(self), tag_name, .{
.include_root = true,
});
} }
pub fn _getElementsByClassName( pub fn _getElementsByClassName(
@@ -157,7 +159,9 @@ pub const Document = struct {
classNames: []const u8, classNames: []const u8,
page: *Page, page: *Page,
) !collection.HTMLCollection { ) !collection.HTMLCollection {
return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, true); return try collection.HTMLCollectionByClassName(page.arena, parser.documentToNode(self), classNames, .{
.include_root = true,
});
} }
pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment { pub fn _createDocumentFragment(self: *parser.Document) !*parser.DocumentFragment {
@@ -201,7 +205,9 @@ pub const Document = struct {
// ParentNode // ParentNode
// https://dom.spec.whatwg.org/#parentnode // https://dom.spec.whatwg.org/#parentnode
pub fn get_children(self: *parser.Document) !collection.HTMLCollection { pub fn get_children(self: *parser.Document) !collection.HTMLCollection {
return try collection.HTMLCollectionChildren(parser.documentToNode(self), false); return collection.HTMLCollectionChildren(parser.documentToNode(self), .{
.include_root = false,
});
} }
pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion { pub fn get_firstElementChild(self: *parser.Document) !?ElementUnion {

View File

@@ -79,7 +79,9 @@ pub const DocumentFragment = struct {
} }
pub fn get_children(self: *parser.DocumentFragment) !collection.HTMLCollection { pub fn get_children(self: *parser.DocumentFragment) !collection.HTMLCollection {
return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), false); return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), .{
.include_root = false,
});
} }
}; };

View File

@@ -294,7 +294,7 @@ pub const Element = struct {
page.arena, page.arena,
parser.elementToNode(self), parser.elementToNode(self),
tag_name, tag_name,
false, .{ .include_root = false },
); );
} }
@@ -307,14 +307,16 @@ pub const Element = struct {
page.arena, page.arena,
parser.elementToNode(self), parser.elementToNode(self),
classNames, classNames,
false, .{ .include_root = false },
); );
} }
// ParentNode // ParentNode
// https://dom.spec.whatwg.org/#parentnode // https://dom.spec.whatwg.org/#parentnode
pub fn get_children(self: *parser.Element) !collection.HTMLCollection { pub fn get_children(self: *parser.Element) !collection.HTMLCollection {
return try collection.HTMLCollectionChildren(parser.elementToNode(self), false); return collection.HTMLCollectionChildren(parser.elementToNode(self), .{
.include_root = false,
});
} }
pub fn get_firstElementChild(self: *parser.Element) !?Union { pub fn get_firstElementChild(self: *parser.Element) !?Union {

View File

@@ -72,13 +72,14 @@ pub fn HTMLCollectionByTagName(
arena: Allocator, arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
tag_name: []const u8, tag_name: []const u8,
include_root: bool, opts: Opts,
) !HTMLCollection { ) !HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) }, .matcher = .{ .matchByTagName = try MatchByTagName.init(arena, tag_name) },
.include_root = include_root, .mutable = opts.mutable,
.include_root = opts.include_root,
}; };
} }
@@ -109,13 +110,14 @@ pub fn HTMLCollectionByClassName(
arena: Allocator, arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
classNames: []const u8, classNames: []const u8,
include_root: bool, opts: Opts,
) !HTMLCollection { ) !HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) }, .matcher = .{ .matchByClassName = try MatchByClassName.init(arena, classNames) },
.include_root = include_root, .mutable = opts.mutable,
.include_root = opts.include_root,
}; };
} }
@@ -139,13 +141,14 @@ pub fn HTMLCollectionByName(
arena: Allocator, arena: Allocator,
root: ?*parser.Node, root: ?*parser.Node,
name: []const u8, name: []const u8,
include_root: bool, opts: Opts,
) !HTMLCollection { ) !HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByName = try MatchByName.init(arena, name) }, .matcher = .{ .matchByName = try MatchByName.init(arena, name) },
.include_root = include_root, .mutable = opts.mutable,
.include_root = opts.include_root,
}; };
} }
@@ -189,13 +192,14 @@ pub const HTMLAllCollection = struct {
pub fn HTMLCollectionChildren( pub fn HTMLCollectionChildren(
root: ?*parser.Node, root: ?*parser.Node,
include_root: bool, opts: Opts,
) !HTMLCollection { ) HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerChildren = .{} }, .walker = .{ .walkerChildren = .{} },
.matcher = .{ .matchTrue = .{} }, .matcher = .{ .matchTrue = .{} },
.include_root = include_root, .mutable = opts.mutable,
.include_root = opts.include_root,
}; };
} }
@@ -224,13 +228,14 @@ pub const MatchByLinks = struct {
pub fn HTMLCollectionByLinks( pub fn HTMLCollectionByLinks(
root: ?*parser.Node, root: ?*parser.Node,
include_root: bool, opts: Opts,
) !HTMLCollection { ) !HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByLinks = MatchByLinks{} }, .matcher = .{ .matchByLinks = MatchByLinks{} },
.include_root = include_root, .mutable = opts.mutable,
.include_root = opts.include_root,
}; };
} }
@@ -249,13 +254,14 @@ pub const MatchByAnchors = struct {
pub fn HTMLCollectionByAnchors( pub fn HTMLCollectionByAnchors(
root: ?*parser.Node, root: ?*parser.Node,
include_root: bool, opts: Opts,
) !HTMLCollection { ) !HTMLCollection {
return HTMLCollection{ return HTMLCollection{
.root = root, .root = root,
.walker = .{ .walkerDepthFirst = .{} }, .walker = .{ .walkerDepthFirst = .{} },
.matcher = .{ .matchByAnchors = MatchByAnchors{} }, .matcher = .{ .matchByAnchors = MatchByAnchors{} },
.include_root = include_root, .mutable = opts.mutable,
.include_root = opts.include_root,
}; };
} }
@@ -285,6 +291,11 @@ pub const HTMLCollectionIterator = struct {
} }
}; };
const Opts = struct {
include_root: bool,
mutable: bool = false,
};
// WEB IDL https://dom.spec.whatwg.org/#htmlcollection // WEB IDL https://dom.spec.whatwg.org/#htmlcollection
// HTMLCollection is re implemented in zig here because libdom // HTMLCollection is re implemented in zig here because libdom
// dom_html_collection expects a comparison function callback as arguement. // dom_html_collection expects a comparison function callback as arguement.
@@ -300,6 +311,8 @@ pub const HTMLCollection = struct {
// itself. // itself.
include_root: bool = false, include_root: bool = false,
mutable: bool = false,
// save a state for the collection to improve the _item speed. // save a state for the collection to improve the _item speed.
cur_idx: ?u32 = null, cur_idx: ?u32 = null,
cur_node: ?*parser.Node = null, cur_node: ?*parser.Node = null,
@@ -350,12 +363,14 @@ pub const HTMLCollection = struct {
var node: *parser.Node = undefined; var node: *parser.Node = undefined;
// Use the current state to improve speed if possible. // Use the current state to improve speed if possible.
if (self.cur_idx != null and index >= self.cur_idx.?) { if (self.mutable == false and self.cur_idx != null and index >= self.cur_idx.?) {
i = self.cur_idx.?; i = self.cur_idx.?;
node = self.cur_node.?; node = self.cur_node.?;
} else { } else {
node = try self.start() orelse return null; node = try self.start() orelse return null;
} }
// i = 0;
// node = try self.start() orelse return null;
while (true) { while (true) {
if (try parser.nodeType(node) == .element) { if (try parser.nodeType(node) == .element) {

View File

@@ -118,7 +118,9 @@ pub const HTMLDocument = struct {
if (name.len == 0) return list; if (name.len == 0) return list;
const root = parser.documentHTMLToNode(self); const root = parser.documentHTMLToNode(self);
var c = try collection.HTMLCollectionByName(arena, root, name, false); var c = try collection.HTMLCollectionByName(arena, root, name, .{
.include_root = false,
});
const ln = try c.get_length(); const ln = try c.get_length();
var i: u32 = 0; var i: u32 = 0;
@@ -132,11 +134,15 @@ pub const HTMLDocument = struct {
} }
pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_images(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", false); return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "img", .{
.include_root = false,
});
} }
pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_embeds(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", false); return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "embed", .{
.include_root = false,
});
} }
pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_plugins(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
@@ -144,11 +150,15 @@ pub const HTMLDocument = struct {
} }
pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_forms(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", false); return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "form", .{
.include_root = false,
});
} }
pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection { pub fn get_scripts(self: *parser.DocumentHTML, page: *Page) !collection.HTMLCollection {
return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", false); return try collection.HTMLCollectionByTagName(page.arena, parser.documentHTMLToNode(self), "script", .{
.include_root = false,
});
} }
pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection { pub fn get_applets(_: *parser.DocumentHTML) !collection.HTMLCollection {
@@ -156,11 +166,15 @@ pub const HTMLDocument = struct {
} }
pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection { pub fn get_links(self: *parser.DocumentHTML) !collection.HTMLCollection {
return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), false); return try collection.HTMLCollectionByLinks(parser.documentHTMLToNode(self), .{
.include_root = false,
});
} }
pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection { pub fn get_anchors(self: *parser.DocumentHTML) !collection.HTMLCollection {
return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), false); return try collection.HTMLCollectionByAnchors(parser.documentHTMLToNode(self), .{
.include_root = false,
});
} }
pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection { pub fn get_all(self: *parser.DocumentHTML) collection.HTMLAllCollection {

View File

@@ -75,7 +75,6 @@ pub const Interfaces = .{
HTMLOListElement, HTMLOListElement,
HTMLObjectElement, HTMLObjectElement,
HTMLOptGroupElement, HTMLOptGroupElement,
HTMLOptionElement,
HTMLOutputElement, HTMLOutputElement,
HTMLParagraphElement, HTMLParagraphElement,
HTMLParamElement, HTMLParamElement,
@@ -102,7 +101,7 @@ pub const Interfaces = .{
HTMLVideoElement, HTMLVideoElement,
@import("form.zig").HTMLFormElement, @import("form.zig").HTMLFormElement,
@import("select.zig").HTMLSelectElement, @import("select.zig").Interfaces,
}; };
pub const Union = generate.Union(Interfaces); pub const Union = generate.Union(Interfaces);
@@ -813,12 +812,6 @@ pub const HTMLOptGroupElement = struct {
pub const subtype = .node; pub const subtype = .node;
}; };
pub const HTMLOptionElement = struct {
pub const Self = parser.Option;
pub const prototype = *HTMLElement;
pub const subtype = .node;
};
pub const HTMLOutputElement = struct { pub const HTMLOutputElement = struct {
pub const Self = parser.Output; pub const Self = parser.Output;
pub const prototype = *HTMLElement; pub const prototype = *HTMLElement;

View File

@@ -18,8 +18,16 @@
const std = @import("std"); const std = @import("std");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const HTMLElement = @import("elements.zig").HTMLElement; const collection = @import("../dom/html_collection.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const HTMLElement = @import("elements.zig").HTMLElement;
pub const Interfaces = .{
HTMLSelectElement,
HTMLOptionElement,
HTMLOptionsCollection,
};
pub const HTMLSelectElement = struct { pub const HTMLSelectElement = struct {
pub const Self = parser.Select; pub const Self = parser.Select;
@@ -89,6 +97,105 @@ pub const HTMLSelectElement = struct {
try parser.optionSetSelected(option, true); try parser.optionSetSelected(option, true);
} }
} }
pub fn get_options(select: *parser.Select) HTMLOptionsCollection {
return .{
.select = select,
.proto = collection.HTMLCollectionChildren(@alignCast(@ptrCast(select)), .{
.mutable = true,
.include_root = false,
}),
};
}
};
pub const HTMLOptionElement = struct {
pub const Self = parser.Option;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn get_value(self: *parser.Option) ![]const u8 {
return parser.optionGetValue(self);
}
pub fn set_value(self: *parser.Option, value: []const u8) !void {
return parser.optionSetValue(self, value);
}
pub fn get_label(self: *parser.Option) ![]const u8 {
return parser.optionGetLabel(self);
}
pub fn set_label(self: *parser.Option, label: []const u8) !void {
return parser.optionSetLabel(self, label);
}
pub fn get_selected(self: *parser.Option) !bool {
return parser.optionGetSelected(self);
}
pub fn set_selected(self: *parser.Option, value: bool) !void {
return parser.optionSetSelected(self, value);
}
pub fn get_disabled(self: *parser.Option) !bool {
return parser.optionGetDisabled(self);
}
pub fn set_disabled(self: *parser.Option, value: bool) !void {
return parser.optionSetDisabled(self, value);
}
pub fn get_text(self: *parser.Option) ![]const u8 {
return parser.optionGetText(self);
}
pub fn get_form(self: *parser.Option) !?*parser.Form {
return parser.optionGetForm(self);
}
};
pub const HTMLOptionsCollection = struct {
pub const prototype = *collection.HTMLCollection;
proto: collection.HTMLCollection,
select: *parser.Select,
pub fn get_selectedIndex(self: *HTMLOptionsCollection, page: *Page) !i32 {
return HTMLSelectElement.get_selectedIndex(self.select, page);
}
pub fn set_selectedIndex(self: *HTMLOptionsCollection, index: i32, page: *Page) !void {
return HTMLSelectElement.set_selectedIndex(self.select, index, page);
}
const BeforeOpts = union(enum) {
index: u32,
option: *parser.Option,
};
pub fn _add(self: *HTMLOptionsCollection, option: *parser.Option, before_: ?BeforeOpts) !void {
const Node = @import("../dom/node.zig").Node;
const before = before_ orelse {
return self.appendOption(option);
};
const insert_before: *parser.Node = switch (before) {
.option => |o| @alignCast(@ptrCast(o)),
.index => |i| (try self.proto.item(i)) orelse return self.appendOption(option),
};
return Node.before(insert_before, &.{
.{ .node = @alignCast(@ptrCast(option)) },
});
}
pub fn _remove(self: *HTMLOptionsCollection, index: u32) !void {
const Node = @import("../dom/node.zig").Node;
const option = (try self.proto.item(index)) orelse return;
_ = try Node._removeChild(@alignCast(@ptrCast(self.select)), option);
}
fn appendOption(self: *HTMLOptionsCollection, option: *parser.Option) !void {
const Node = @import("../dom/node.zig").Node;
return Node.append(@alignCast(@ptrCast(self.select)), &.{
.{ .node = @alignCast(@ptrCast(option)) },
});
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
@@ -140,5 +247,32 @@ test "Browser.HTML.Select" {
.{ "s.selectedIndex = -323", null }, .{ "s.selectedIndex = -323", null },
.{ "s.selectedIndex", "-1" }, .{ "s.selectedIndex", "-1" },
.{ "let options = s.options", null },
.{ "options.length", "2" },
.{ "options.item(1).value", "o2" },
.{ "options.selectedIndex", "-1" },
.{ "let o3 = document.createElement('option');", null },
.{ "o3.value = 'o3';", null },
.{ "options.add(o3)", null },
.{ "options.length", "3" },
.{ "options.item(2).value", "o3" },
.{ "let o4 = document.createElement('option');", null },
.{ "o4.value = 'o4';", null },
.{ "options.add(o4, 1)", null },
.{ "options.length", "4" },
.{ "options.item(1).value", "o4" },
.{ "let o5 = document.createElement('option');", null },
.{ "o5.value = 'o5';", null },
.{ "options.add(o5, o3)", null },
.{ "options.length", "5" },
.{ "options.item(3).value", "o5" },
.{ "options.remove(3)", null },
.{ "options.length", "4" },
.{ "options.item(3).value", "o3" },
}, .{}); }, .{});
} }

View File

@@ -2590,6 +2590,24 @@ pub fn optionGetValue(option: *Option) ![]const u8 {
return strToData(s); return strToData(s);
} }
pub fn optionSetLabel(input: *Option, label: []const u8) !void {
const err = c.dom_html_option_element_set_label(input, try strFromData(label));
try DOMErr(err);
}
pub fn optionGetLabel(option: *Option) ![]const u8 {
var s_: ?*String = null;
const err = c.dom_html_option_element_get_label(option, &s_);
try DOMErr(err);
const s = s_ orelse return "";
return strToData(s);
}
pub fn optionSetValue(input: *Option, value: []const u8) !void {
const err = c.dom_html_option_element_set_value(input, try strFromData(value));
try DOMErr(err);
}
pub fn optionGetSelected(option: *Option) !bool { pub fn optionGetSelected(option: *Option) !bool {
var selected: bool = false; var selected: bool = false;
const err = c.dom_html_option_element_get_selected(option, &selected); const err = c.dom_html_option_element_get_selected(option, &selected);
@@ -2597,11 +2615,38 @@ pub fn optionGetSelected(option: *Option) !bool {
return selected; return selected;
} }
pub fn optionSetDisabled(option: *Option, disabled: bool) !void {
const err = c.dom_html_option_element_set_disabled(option, disabled);
try DOMErr(err);
}
pub fn optionGetDisabled(option: *Option) !bool {
var disabled: bool = false;
const err = c.dom_html_option_element_get_disabled(option, &disabled);
try DOMErr(err);
return disabled;
}
pub fn optionSetSelected(option: *Option, selected: bool) !void { pub fn optionSetSelected(option: *Option, selected: bool) !void {
const err = c.dom_html_option_element_set_selected(option, selected); const err = c.dom_html_option_element_set_selected(option, selected);
try DOMErr(err); try DOMErr(err);
} }
pub fn optionGetText(option: *Option) ![]const u8 {
var s_: ?*String = null;
const err = c.dom_html_option_element_get_text(option, &s_);
try DOMErr(err);
const s = s_ orelse return "";
return strToData(s);
}
pub fn optionGetForm(option: *Option) !?*Form {
var form: ?*Form = null;
const err = c.dom_html_option_element_get_form(option, &form);
try DOMErr(err);
return form;
}
// HtmlCollection // HtmlCollection
pub fn htmlCollectionGetLength(collection: *HTMLCollection) !u32 { pub fn htmlCollectionGetLength(collection: *HTMLCollection) !u32 {
var len: u32 = 0; var len: u32 = 0;