From 32bad5f8bb63a4ee92c046ddf5935294df89ebec Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 13 Nov 2025 20:09:38 +0800 Subject: [PATCH] Element.matches, Element.hasAttributes and DOMStringMap (Element.dataset) --- src/browser/Page.zig | 4 + src/browser/ScriptManager.zig | 1 - src/browser/js/Caller.zig | 49 +++++- src/browser/js/Env.zig | 14 +- src/browser/js/bridge.zig | 52 ++++-- src/browser/tests/element/attributes.html | 19 +++ src/browser/tests/element/dataset.html | 150 ++++++++++++++++++ src/browser/tests/element/matches.html | 76 +++++++++ src/browser/webapi/Document.zig | 4 +- src/browser/webapi/Element.zig | 24 +++ src/browser/webapi/KeyValueList.zig | 2 +- src/browser/webapi/Node.zig | 4 +- src/browser/webapi/Window.zig | 2 +- .../webapi/collections/HTMLAllCollection.zig | 2 +- .../webapi/collections/HTMLCollection.zig | 2 +- src/browser/webapi/css/CSSStyleProperties.zig | 2 +- src/browser/webapi/css/MediaQueryList.zig | 2 +- src/browser/webapi/element/Attribute.zig | 2 +- src/browser/webapi/element/DOMStringMap.zig | 87 ++++++++++ src/browser/webapi/element/html/Body.zig | 2 +- src/browser/webapi/element/html/Script.zig | 4 +- src/browser/webapi/selector/Parser.zig | 1 - 22 files changed, 467 insertions(+), 38 deletions(-) create mode 100644 src/browser/tests/element/dataset.html create mode 100644 src/browser/tests/element/matches.html create mode 100644 src/browser/webapi/element/DOMStringMap.zig diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 6f0afe85..fa1c9fdd 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -63,6 +63,9 @@ _attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute), // the return of elements.attributes. _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap), +// element.dataset -> DOMStringMap +_element_datasets: std.AutoHashMapUnmanaged(*Element, *Element.DOMStringMap), + _script_manager: ScriptManager, _polyfill_loader: polyfill.Loader = .{}, @@ -152,6 +155,7 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._load_state = .parsing; self._attribute_lookup = .empty; self._attribute_named_node_map_lookup = .empty; + self._element_datasets = .empty; self._event_manager = EventManager.init(self); self._script_manager = ScriptManager.init(self); diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 4a394f65..29e9f683 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -707,7 +707,6 @@ const Script = struct { .cacheable = cacheable, }); - // Handle importmap special case here: the content is a JSON containing // imports. if (self.kind == .importmap) { diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index aea3a1af..0b1b5e4a 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -157,7 +157,7 @@ pub fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); @field(args, "1") = idx; const ret = @call(.auto, func, args); - return self.handleIndexedReturn(T, F, ret, info, opts); + return self.handleIndexedReturn(T, F, true, ret, info, opts); } pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { @@ -173,10 +173,49 @@ pub fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.N @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); @field(args, "1") = try self.nameToString(name); const ret = @call(.auto, func, args); - return self.handleIndexedReturn(T, F, ret, info, opts); + return self.handleIndexedReturn(T, F, true, ret, info, opts); } -fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 { +pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { + return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + return v8.Intercepted.No; + }; +} + +pub fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 { + const F = @TypeOf(func); + var args: ParameterTypes(F) = undefined; + @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); + @field(args, "1") = try self.nameToString(name); + @field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js_value); + if (@typeInfo(F).@"fn".params.len == 4) { + @field(args, "3") = self.context.page; + } + const ret = @call(.auto, func, args); + return self.handleIndexedReturn(T, F, false, ret, info, opts); +} + +pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 { + return self._deleteNamedIndex(T, func, name, info, opts) catch |err| { + self.handleError(T, @TypeOf(func), err, info, opts); + return v8.Intercepted.No; + }; +} + +pub fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 { + const F = @TypeOf(func); + var args: ParameterTypes(F) = undefined; + @field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis()); + @field(args, "1") = try self.nameToString(name); + if (@typeInfo(F).@"fn".params.len == 3) { + @field(args, "2") = self.context.page; + } + const ret = @call(.auto, func, args); + return self.handleIndexedReturn(T, F, false, ret, info, opts); +} + +fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 { // need to unwrap this error immediately for when opts.null_as_undefined == true // and we need to compare it to null; const non_error_ret = switch (@typeInfo(@TypeOf(ret))) { @@ -197,7 +236,9 @@ fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, ret: a else => ret, }; - info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts)); + if (comptime getter) { + info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts)); + } return v8.Intercepted.Yes; } diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index fa4595e3..71bed313 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -253,13 +253,12 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct }; template_proto.setIndexedProperty(configuration, null); }, - bridge.NamedIndexed => { - const configuration = v8.NamedPropertyHandlerConfiguration{ - .getter = value.getter, - .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking, - }; - template_proto.setNamedProperty(configuration, null); - }, + bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{ + .getter = value.getter, + .setter = value.setter, + .deleter = value.deleter, + .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking, + }, null), bridge.Iterator => { // Same as a function, but with a specific name const function_template = v8.FunctionTemplate.initCallback(isolate, value.func); @@ -326,7 +325,6 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem // if (has_js_call_as_function) { - // if (@hasDecl(Struct, "htmldda") and Struct.htmldda) { // if (!has_js_call_as_function) { // @compileError(@typeName(Struct) ++ ": htmldda required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable."); diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 4a313b6b..fe1d4ec1 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -45,8 +45,8 @@ pub fn Builder(comptime T: type) type { return Indexed.init(T, getter_func, opts); } - pub fn namedIndexed(comptime getter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed { - return NamedIndexed.init(T, getter_func, opts); + pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed { + return NamedIndexed.init(T, getter_func, setter_func, deleter_func, opts); } pub fn iterator(comptime func: anytype, comptime opts: Iterator.Opts) Iterator { @@ -221,14 +221,16 @@ pub const Indexed = struct { pub const NamedIndexed = struct { getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8, + setter: ?*const fn (c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null, + deleter: ?*const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null, const Opts = struct { as_typed_array: bool = false, null_as_undefined: bool = false, }; - fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) NamedIndexed { - return .{ .getter = struct { + fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed { + const getter_fn = struct { fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { const info = v8.PropertyCallbackInfo.initFromV8(raw_info); var caller = Caller.init(info); @@ -238,7 +240,39 @@ pub const NamedIndexed = struct { .null_as_undefined = opts.null_as_undefined, }); } - }.wrap }; + }.wrap; + + const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct { + fn wrap(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + var caller = Caller.init(info); + defer caller.deinit(); + + return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{ + .as_typed_array = opts.as_typed_array, + .null_as_undefined = opts.null_as_undefined, + }); + } + }.wrap; + + const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct { + fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { + const info = v8.PropertyCallbackInfo.initFromV8(raw_info); + var caller = Caller.init(info); + defer caller.deinit(); + + return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{ + .as_typed_array = opts.as_typed_array, + .null_as_undefined = opts.null_as_undefined, + }); + } + }.wrap; + + return .{ + .getter = getter_fn, + .setter = setter_fn, + .deleter = deleter_fn, + }; } }; @@ -269,7 +303,6 @@ pub const Iterator = struct { } }; - pub const Callable = struct { func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void, @@ -278,7 +311,7 @@ pub const Callable = struct { }; fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable { - return .{.func = struct { + return .{ .func = struct { fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void { const info = v8.FunctionCallbackInfo.initFromV8(raw_info); var caller = Caller.init(info); @@ -286,8 +319,8 @@ pub const Callable = struct { caller.method(T, func, info, .{ .null_as_undefined = opts.null_as_undefined, }); - }}.wrap - }; + } + }.wrap }; } }; @@ -457,6 +490,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/DOMNodeIterator.zig"), @import("../webapi/NodeFilter.zig"), @import("../webapi/Element.zig"), + @import("../webapi/element/DOMStringMap.zig"), @import("../webapi/element/Attribute.zig"), @import("../webapi/element/Html.zig"), @import("../webapi/element/html/IFrame.zig"), diff --git a/src/browser/tests/element/attributes.html b/src/browser/tests/element/attributes.html index 4f557676..d4d416f6 100644 --- a/src/browser/tests/element/attributes.html +++ b/src/browser/tests/element/attributes.html @@ -83,3 +83,22 @@ assertAttributes([{name: 'id', value: 'attr1'}, {name: 'class', value: 'sHow'}]); + + diff --git a/src/browser/tests/element/dataset.html b/src/browser/tests/element/dataset.html new file mode 100644 index 00000000..c9178c74 --- /dev/null +++ b/src/browser/tests/element/dataset.html @@ -0,0 +1,150 @@ + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/element/matches.html b/src/browser/tests/element/matches.html new file mode 100644 index 00000000..324453cb --- /dev/null +++ b/src/browser/tests/element/matches.html @@ -0,0 +1,76 @@ + + + +
+

Paragraph 1

+
+

Paragraph 2

+ +

Paragraph 3

+
+
+
+ + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 70a40767..bbdd267c 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -195,11 +195,11 @@ pub const JsApi = struct { pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true }); pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{}); pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{}); - pub const defaultView = bridge.accessor(struct{ + pub const defaultView = bridge.accessor(struct { fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") { return page.window; } - }.defaultView, null, .{.cache = "defaultView"}); + }.defaultView, null, .{ .cache = "defaultView" }); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index e0089076..0ca65757 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -12,6 +12,7 @@ const collections = @import("collections.zig"); const Selector = @import("selector/Selector.zig"); pub const Attribute = @import("element/Attribute.zig"); const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); +pub const DOMStringMap = @import("element/DOMStringMap.zig"); pub const Svg = @import("element/Svg.zig"); pub const Html = @import("element/Html.zig"); @@ -247,6 +248,12 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con return attributes.get(name, page); } +pub fn hasAttribute(self: *const Element, name: []const u8, page: *Page) !bool { + const attributes = self._attributes orelse return false; + const value = try attributes.get(name, page); + return value != null; +} + pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attribute { const attributes = self._attributes orelse return null; return attributes.getAttribute(name, self, page); @@ -342,6 +349,16 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList { }; } +pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap { + const gop = try page._element_datasets.getOrPut(page.arena, self); + if (!gop.found_existing) { + gop.value_ptr.* = try page._factory.create(DOMStringMap{ + ._element = self, + }); + } + return gop.value_ptr.*; +} + pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { page.domChanged(); var parent = self.asNode(); @@ -438,6 +455,10 @@ pub fn getChildElementCount(self: *Element) usize { return count; } +pub fn matches(self: *Element, selector: []const u8, page: *Page) !bool { + return Selector.matches(self, selector, page); +} + pub fn querySelector(self: *Element, selector: []const u8, page: *Page) !?*Element { return Selector.querySelector(self.asNode(), selector, page); } @@ -658,8 +679,10 @@ pub const JsApi = struct { pub const id = bridge.accessor(Element.getId, Element.setId, .{}); pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{}); pub const classList = bridge.accessor(Element.getClassList, null, .{}); + pub const dataset = bridge.accessor(Element.getDataset, null, .{}); pub const style = bridge.accessor(Element.getStyle, null, .{}); pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{}); + pub const hasAttribute = bridge.function(Element.hasAttribute, .{}); pub const getAttribute = bridge.function(Element.getAttribute, .{}); pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{}); pub const setAttribute = bridge.function(Element.setAttribute, .{}); @@ -676,6 +699,7 @@ pub const JsApi = struct { pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{}); pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{}); pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{}); + pub const matches = bridge.function(Element.matches, .{ .dom_exception = true }); pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true }); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); diff --git a/src/browser/webapi/KeyValueList.zig b/src/browser/webapi/KeyValueList.zig index 9f0cca7c..3b105bd8 100644 --- a/src/browser/webapi/KeyValueList.zig +++ b/src/browser/webapi/KeyValueList.zig @@ -45,7 +45,7 @@ pub fn get(self: *const KeyValueList, name: []const u8) ?[]const u8 { return null; } -pub fn getAll(self: *const KeyValueList, name: []const u8, page: *Page) ![]const []const u8 { +pub fn getAll(self: *const KeyValueList, name: []const u8, page: *Page) ![]const []const u8 { const arena = page.call_arena; var arr: std.ArrayList([]const u8) = .empty; for (self._entries.items) |*entry| { diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index cdec332a..7af7c4fa 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -614,9 +614,7 @@ pub const JsApi = struct { pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{}); fn _textContext(self: *Node, page: *const Page) !?[]const u8 { - // can't call node.getTextContent directly, because - // 1 - document should return null, not empty - // 2 - cdata and attributes can return value directly, avoiding the copy + // cdata and attributes can return value directly, avoiding the copy switch (self._type) { .element => |el| { var buf = std.Io.Writer.Allocating.init(page.call_arena); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 0ea5fc06..605b9fdc 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -149,7 +149,7 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void { pub fn reportError(self: *Window, err: js.Object, page: *Page) !void { const error_event = try ErrorEvent.init("error", .{ - .@"error" = err, + .@"error" = err, .message = err.toString() catch "Unknown error", .bubbles = false, .cancelable = true, diff --git a/src/browser/webapi/collections/HTMLAllCollection.zig b/src/browser/webapi/collections/HTMLAllCollection.zig index 9e883ad5..60aba31c 100644 --- a/src/browser/webapi/collections/HTMLAllCollection.zig +++ b/src/browser/webapi/collections/HTMLAllCollection.zig @@ -152,7 +152,7 @@ pub const JsApi = struct { pub const length = bridge.accessor(HTMLAllCollection.length, null, .{}); pub const @"[int]" = bridge.indexed(HTMLAllCollection.getAtIndex, .{ .null_as_undefined = true }); - pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, .{ .null_as_undefined = true }); + pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, null, null, .{ .null_as_undefined = true }); pub const item = bridge.function(_item, .{}); fn _item(self: *HTMLAllCollection, index: i32, page: *Page) ?*Element { diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index 134ac5cc..34d2f071 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -83,7 +83,7 @@ pub const JsApi = struct { pub const length = bridge.accessor(HTMLCollection.length, null, .{}); pub const @"[int]" = bridge.indexed(HTMLCollection.getAtIndex, .{ .null_as_undefined = true }); - pub const @"[str]" = bridge.namedIndexed(HTMLCollection.getByName, .{ .null_as_undefined = true }); + pub const @"[str]" = bridge.namedIndexed(HTMLCollection.getByName, null, null, .{ .null_as_undefined = true }); pub const item = bridge.function(_item, .{}); fn _item(self: *HTMLCollection, index: i32, page: *Page) ?*Element { diff --git a/src/browser/webapi/css/CSSStyleProperties.zig b/src/browser/webapi/css/CSSStyleProperties.zig index d0b4a608..2eeefff6 100644 --- a/src/browser/webapi/css/CSSStyleProperties.zig +++ b/src/browser/webapi/css/CSSStyleProperties.zig @@ -120,7 +120,7 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const @"[]" = bridge.namedIndexed(_getPropertyIndexed, .{}); + pub const @"[]" = bridge.namedIndexed(_getPropertyIndexed, null, null, .{}); const method_names = std.StaticStringMap(void).initComptime(.{ .{ "getPropertyValue", {} }, diff --git a/src/browser/webapi/css/MediaQueryList.zig b/src/browser/webapi/css/MediaQueryList.zig index 25f813b3..f67e754c 100644 --- a/src/browser/webapi/css/MediaQueryList.zig +++ b/src/browser/webapi/css/MediaQueryList.zig @@ -31,7 +31,7 @@ pub const JsApi = struct { pub const Meta = struct { pub const name = "MediaQueryList"; pub const prototype_chain = bridge.prototypeChain(); - pub var class_id: bridge.ClassId = undefined; + pub var class_id: bridge.ClassId = undefined; }; pub const media = bridge.accessor(MediaQueryList.getMedia, null, .{}); diff --git a/src/browser/webapi/element/Attribute.zig b/src/browser/webapi/element/Attribute.zig index dce7655c..85d6bf6d 100644 --- a/src/browser/webapi/element/Attribute.zig +++ b/src/browser/webapi/element/Attribute.zig @@ -386,7 +386,7 @@ pub const NamedNodeMap = struct { pub const length = bridge.accessor(NamedNodeMap.length, null, .{}); pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, .{ .null_as_undefined = true }); - pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, .{ .null_as_undefined = true }); + pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true }); pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{}); pub const item = bridge.function(_item, .{}); fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute { diff --git a/src/browser/webapi/element/DOMStringMap.zig b/src/browser/webapi/element/DOMStringMap.zig new file mode 100644 index 00000000..b2aa8bab --- /dev/null +++ b/src/browser/webapi/element/DOMStringMap.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); + +const Element = @import("../Element.zig"); +const Page = @import("../../Page.zig"); + +const Allocator = std.mem.Allocator; + +const DOMStringMap = @This(); + +_element: *Element, + +fn _getProperty(self: *DOMStringMap, name: []const u8, page: *Page) !?[]const u8 { + const attr_name = try camelToKebab(page.call_arena, name); + return try self._element.getAttribute(attr_name, page); +} + +fn _setProperty(self: *DOMStringMap, name: []const u8, value: []const u8, page: *Page) !void { + const attr_name = try camelToKebab(page.call_arena, name); + return self._element.setAttributeSafe(attr_name, value, page); +} + +fn _deleteProperty(self: *DOMStringMap, name: []const u8, page: *Page) !void { + const attr_name = try camelToKebab(page.call_arena, name); + try self._element.removeAttribute(attr_name, page); +} + +// fooBar -> foo-bar +fn camelToKebab(arena: Allocator, camel: []const u8) ![]const u8 { + var result: std.ArrayList(u8) = .empty; + try result.ensureTotalCapacity(arena, 5 + camel.len * 2); + result.appendSliceAssumeCapacity("data-"); + + for (camel, 0..) |c, i| { + if (std.ascii.isUpper(c)) { + if (i > 0) { + result.appendAssumeCapacity('-'); + } + result.appendAssumeCapacity(std.ascii.toLower(c)); + } else { + result.appendAssumeCapacity(c); + } + } + + return result.items; +} + +// data-foo-bar -> fooBar +fn kebabToCamel(arena: Allocator, kebab: []const u8) !?[]const u8 { + if (!std.mem.startsWith(u8, kebab, "data-")) { + return null; + } + + const data_part = kebab[5..]; // Skip "data-" + if (data_part.len == 0) { + return null; + } + + var result: std.ArrayList(u8) = .empty; + try result.ensureTotalCapacity(arena, data_part.len); + + var capitalize_next = false; + for (data_part) |c| { + if (c == '-') { + capitalize_next = true; + } else if (capitalize_next) { + result.appendAssumeCapacity(std.ascii.toUpper(c)); + capitalize_next = false; + } else { + result.appendAssumeCapacity(c); + } + } + + return result.items; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(DOMStringMap); + + pub const Meta = struct { + pub const name = "DOMStringMap"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const @"[]" = bridge.namedIndexed(_getProperty, _setProperty, _deleteProperty, .{.null_as_undefined = true}); +}; diff --git a/src/browser/webapi/element/html/Body.zig b/src/browser/webapi/element/html/Body.zig index 585e74d6..cf04a939 100644 --- a/src/browser/webapi/element/html/Body.zig +++ b/src/browser/webapi/element/html/Body.zig @@ -33,7 +33,7 @@ pub const Build = struct { const el = node.as(Element); const on_load = el.getAttributeSafe("onload") orelse return; page.window._on_load = page.js.stringToFunction(on_load) catch |err| blk: { - log.err(.js, "body.onload", .{.err = err, .str = on_load}); + log.err(.js, "body.onload", .{ .err = err, .str = on_load }); break :blk null; }; } diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index df224ae2..6bf306d3 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -80,14 +80,14 @@ pub const Build = struct { if (element.getAttributeSafe("onload")) |on_load| { self._on_load = page.js.stringToFunction(on_load) catch |err| blk: { - log.err(.js, "script.onload", .{.err = err, .str = on_load}); + log.err(.js, "script.onload", .{ .err = err, .str = on_load }); break :blk null; }; } if (element.getAttributeSafe("onerror")) |on_error| { self._on_error = page.js.stringToFunction(on_error) catch |err| blk: { - log.err(.js, "script.onerror", .{.err = err, .str = on_error}); + log.err(.js, "script.onerror", .{ .err = err, .str = on_error }); break :blk null; }; } diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index 0e88df0d..335f2245 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -77,7 +77,6 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select var segments: std.ArrayList(Segment) = .empty; var current_compound: std.ArrayList(Part) = .empty; - // Parse the first compound (no combinator before it) while (parser.skipSpaces()) { if (parser.peek() == 0) break;