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;