Element.matches, Element.hasAttributes and DOMStringMap (Element.dataset)

This commit is contained in:
Karl Seguin
2025-11-13 20:09:38 +08:00
parent 5ec5647395
commit 32bad5f8bb
22 changed files with 467 additions and 38 deletions

View File

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

View File

@@ -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) {

View File

@@ -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,
};
if (comptime getter) {
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
}
return v8.Intercepted.Yes;
}

View File

@@ -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{
bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
.getter = value.getter,
.setter = value.setter,
.deleter = value.deleter,
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
};
template_proto.setNamedProperty(configuration, null);
},
}, 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.");

View File

@@ -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,
@@ -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"),

View File

@@ -83,3 +83,22 @@
assertAttributes([{name: 'id', value: 'attr1'}, {name: 'class', value: 'sHow'}]);
</script>
<script id=hasAttribute>
{
const el1 = $('#attr1');
testing.expectEqual(true, el1.hasAttribute('id'));
testing.expectEqual(true, el1.hasAttribute('ID'));
testing.expectEqual(true, el1.hasAttribute('class'));
testing.expectEqual(true, el1.hasAttribute('CLASS'));
testing.expectEqual(false, el1.hasAttribute('other'));
testing.expectEqual(false, el1.hasAttribute('nope'));
el1.setAttribute('data-test', 'value');
testing.expectEqual(true, el1.hasAttribute('data-test'));
el1.removeAttribute('data-test');
testing.expectEqual(false, el1.hasAttribute('data-test'));
}
</script>

View File

@@ -0,0 +1,150 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="test" data-foo="bar" data-hello-world="test"></div>
<div id="test-write"></div>
<div id="test-bracket"></div>
<script id=basic>
{
const el = document.getElementById('test');
testing.expectEqual('object', typeof el.dataset);
}
</script>
<script id=readExistingAttributes>
{
const el = document.getElementById('test');
testing.expectEqual('bar', el.dataset.foo);
testing.expectEqual('test', el.dataset.helloWorld);
}
</script>
<script id=camelCaseConversion>
{
const el = document.getElementById('test');
// Reading kebab-case as camelCase
testing.expectEqual('test', el.dataset.helloWorld);
}
</script>
<script id=writeNewAttribute>
{
const el = document.getElementById('test-write');
el.dataset.newAttr = 'value';
testing.expectEqual('value', el.dataset.newAttr);
testing.expectEqual('value', el.getAttribute('data-new-attr'));
}
</script>
<script id=writeExistingAttribute>
{
const el = document.getElementById('test-write');
el.setAttribute('data-foo', 'original');
el.dataset.foo = 'updated';
testing.expectEqual('updated', el.dataset.foo);
testing.expectEqual('updated', el.getAttribute('data-foo'));
}
</script>
<script id=writeCamelCaseConversion>
{
const el = document.getElementById('test-write');
// Writing camelCase creates kebab-case attribute
el.dataset.fooBarBaz = 'qux';
testing.expectEqual('qux', el.getAttribute('data-foo-bar-baz'));
}
</script>
<script id=undefinedForNonExistent>
{
const el = document.getElementById('test');
testing.expectEqual(undefined, el.dataset.nonExistent);
}
</script>
<script id=bracketNotation>
{
const el = document.getElementById('test-bracket');
el.setAttribute('data-foo', 'bar');
el.setAttribute('data-hello-world', 'test');
// Bracket notation should work the same as dot notation
testing.expectEqual('bar', el.dataset['foo']);
testing.expectEqual('test', el.dataset['helloWorld']);
// Non-existent should return undefined
testing.expectEqual(undefined, el.dataset['nonExistent']);
}
</script>
<script id=edgeCases>
{
const el = document.createElement('div');
el.setAttribute('data-x', 'single-letter');
el.setAttribute('data-foo-bar-baz', 'multiple-dashes');
// Single letter key
testing.expectEqual('single-letter', el.dataset['x']);
testing.expectEqual('single-letter', el.dataset.x);
// Multiple dashes
testing.expectEqual('multiple-dashes', el.dataset['fooBarBaz']);
testing.expectEqual('multiple-dashes', el.dataset.fooBarBaz);
// Empty string key (data- attribute) - should be accessible as empty string
el.setAttribute('data-', 'empty');
testing.expectEqual('empty', el.dataset['']);
}
</script>
<script id=identityCheck>
{
const el = document.getElementById('test');
const ds1 = el.dataset;
const ds2 = el.dataset;
// Should return the same object instance
testing.expectEqual(true, ds1 === ds2);
}
</script>
<script id=deleteProperty>
{
const el = document.createElement('div');
el.setAttribute('data-foo', 'bar');
el.setAttribute('data-hello-world', 'test');
testing.expectEqual('bar', el.dataset.foo);
testing.expectEqual('test', el.dataset.helloWorld);
// Delete using dot notation
delete el.dataset.foo;
testing.expectEqual(undefined, el.dataset.foo);
testing.expectEqual(null, el.getAttribute('data-foo'));
// Delete using bracket notation
delete el.dataset['helloWorld'];
testing.expectEqual(undefined, el.dataset.helloWorld);
testing.expectEqual(null, el.getAttribute('data-hello-world'));
}
</script>
<script id=independentInstances>
{
const el1 = document.getElementById('test');
const el2 = document.createElement('div');
el1.dataset.test1 = 'value1';
el2.dataset.test2 = 'value2';
testing.expectEqual('value1', el1.dataset.test1);
testing.expectEqual(undefined, el1.dataset.test2);
testing.expectEqual(undefined, el2.dataset.test1);
testing.expectEqual('value2', el2.dataset.test2);
}
</script>

View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="test-container" class="container main">
<p class="text highlight">Paragraph 1</p>
<div class="nested">
<p class="text">Paragraph 2</p>
<span id="special" class="wrapper">
<p class="deep">Paragraph 3</p>
</span>
</div>
</div>
<script id=basicMatches>
{
const container = $('#test-container');
testing.expectEqual(true, container.matches('#test-container'));
testing.expectEqual(false, container.matches('#other'));
testing.expectEqual(true, container.matches('.container'));
testing.expectEqual(true, container.matches('.main'));
testing.expectEqual(false, container.matches('.nested'));
testing.expectEqual(true, container.matches('div'));
testing.expectEqual(false, container.matches('p'));
testing.expectEqual(true, container.matches('div.container'));
testing.expectEqual(true, container.matches('div#test-container'));
testing.expectEqual(true, container.matches('.container.main'));
testing.expectEqual(false, container.matches('div.nested'));
testing.expectEqual(true, container.matches('*'));
}
</script>
<script id=childMatches>
{
const paragraph = $('#test-container > p');
testing.expectEqual(true, paragraph.matches('p'));
testing.expectEqual(true, paragraph.matches('.text'));
testing.expectEqual(true, paragraph.matches('.highlight'));
testing.expectEqual(true, paragraph.matches('p.text.highlight'));
testing.expectEqual(false, paragraph.matches('#test-container'));
testing.expectEqual(false, paragraph.matches('div'));
}
</script>
<script id=specialSpan>
{
const span = $('#special');
testing.expectEqual(true, span.matches('#special'));
testing.expectEqual(true, span.matches('span'));
testing.expectEqual(true, span.matches('.wrapper'));
testing.expectEqual(true, span.matches('span.wrapper'));
testing.expectEqual(true, span.matches('span#special.wrapper'));
testing.expectEqual(false, span.matches('#other'));
testing.expectEqual(false, span.matches('div'));
}
</script>
<script id=errorHandling>
{
const container = $('#test-container');
testing.expectError("Syntax Error", () => container.matches(''));
testing.withError((err) => {
testing.expectEqual(12, err.code);
testing.expectEqual("SyntaxError", err.name);
testing.expectEqual("Syntax Error", err.message);
}, () => container.matches(''));
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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", {} },

View File

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

View File

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

View File

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