Merge pull request #911 from lightpanda-io/select_options
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled

Implement select.options
This commit is contained in:
Karl Seguin
2025-07-24 07:48:12 +08:00
committed by GitHub
8 changed files with 246 additions and 37 deletions

View File

@@ -149,7 +149,9 @@ pub const Document = struct {
tag_name: []const u8,
page: *Page,
) !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(
@@ -157,7 +159,9 @@ pub const Document = struct {
classNames: []const u8,
page: *Page,
) !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 {
@@ -201,7 +205,9 @@ pub const Document = struct {
// ParentNode
// https://dom.spec.whatwg.org/#parentnode
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 {

View File

@@ -79,7 +79,9 @@ pub const DocumentFragment = struct {
}
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,
parser.elementToNode(self),
tag_name,
false,
.{ .include_root = false },
);
}
@@ -307,14 +307,16 @@ pub const Element = struct {
page.arena,
parser.elementToNode(self),
classNames,
false,
.{ .include_root = false },
);
}
// ParentNode
// https://dom.spec.whatwg.org/#parentnode
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 {

View File

@@ -72,13 +72,14 @@ pub fn HTMLCollectionByTagName(
arena: Allocator,
root: ?*parser.Node,
tag_name: []const u8,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.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,
root: ?*parser.Node,
classNames: []const u8,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.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,
root: ?*parser.Node,
name: []const u8,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.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(
root: ?*parser.Node,
include_root: bool,
) !HTMLCollection {
opts: Opts,
) HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerChildren = .{} },
.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(
root: ?*parser.Node,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.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(
root: ?*parser.Node,
include_root: bool,
opts: Opts,
) !HTMLCollection {
return HTMLCollection{
.root = root,
.walker = .{ .walkerDepthFirst = .{} },
.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
// HTMLCollection is re implemented in zig here because libdom
// dom_html_collection expects a comparison function callback as arguement.
@@ -300,6 +311,8 @@ pub const HTMLCollection = struct {
// itself.
include_root: bool = false,
mutable: bool = false,
// save a state for the collection to improve the _item speed.
cur_idx: ?u32 = null,
cur_node: ?*parser.Node = null,
@@ -350,7 +363,7 @@ pub const HTMLCollection = struct {
var node: *parser.Node = undefined;
// 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.?;
node = self.cur_node.?;
} else {

View File

@@ -118,7 +118,9 @@ pub const HTMLDocument = struct {
if (name.len == 0) return list;
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();
var i: u32 = 0;
@@ -132,11 +134,15 @@ pub const HTMLDocument = struct {
}
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 {
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 {
@@ -144,11 +150,15 @@ pub const HTMLDocument = struct {
}
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 {
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 {
@@ -156,11 +166,15 @@ pub const HTMLDocument = struct {
}
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 {
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 {

View File

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

View File

@@ -18,8 +18,16 @@
const std = @import("std");
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 HTMLElement = @import("elements.zig").HTMLElement;
pub const Interfaces = .{
HTMLSelectElement,
HTMLOptionElement,
HTMLOptionsCollection,
};
pub const HTMLSelectElement = struct {
pub const Self = parser.Select;
@@ -89,6 +97,105 @@ pub const HTMLSelectElement = struct {
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");
@@ -140,5 +247,32 @@ test "Browser.HTML.Select" {
.{ "s.selectedIndex = -323", null },
.{ "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);
}
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 {
var selected: bool = false;
const err = c.dom_html_option_element_get_selected(option, &selected);
@@ -2597,11 +2615,38 @@ pub fn optionGetSelected(option: *Option) !bool {
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 {
const err = c.dom_html_option_element_set_selected(option, selected);
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
pub fn htmlCollectionGetLength(collection: *HTMLCollection) !u32 {
var len: u32 = 0;