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. // the return of elements.attributes.
_attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap), _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap),
// element.dataset -> DOMStringMap
_element_datasets: std.AutoHashMapUnmanaged(*Element, *Element.DOMStringMap),
_script_manager: ScriptManager, _script_manager: ScriptManager,
_polyfill_loader: polyfill.Loader = .{}, _polyfill_loader: polyfill.Loader = .{},
@@ -152,6 +155,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._load_state = .parsing; self._load_state = .parsing;
self._attribute_lookup = .empty; self._attribute_lookup = .empty;
self._attribute_named_node_map_lookup = .empty; self._attribute_named_node_map_lookup = .empty;
self._element_datasets = .empty;
self._event_manager = EventManager.init(self); self._event_manager = EventManager.init(self);
self._script_manager = ScriptManager.init(self); self._script_manager = ScriptManager.init(self);

View File

@@ -707,7 +707,6 @@ const Script = struct {
.cacheable = cacheable, .cacheable = cacheable,
}); });
// Handle importmap special case here: the content is a JSON containing // Handle importmap special case here: the content is a JSON containing
// imports. // imports.
if (self.kind == .importmap) { 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, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = idx; @field(args, "1") = idx;
const ret = @call(.auto, func, args); 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 { 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, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
@field(args, "1") = try self.nameToString(name); @field(args, "1") = try self.nameToString(name);
const ret = @call(.auto, func, args); 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 // need to unwrap this error immediately for when opts.null_as_undefined == true
// and we need to compare it to null; // and we need to compare it to null;
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) { 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, 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; 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); template_proto.setIndexedProperty(configuration, null);
}, },
bridge.NamedIndexed => { bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
const configuration = v8.NamedPropertyHandlerConfiguration{ .getter = value.getter,
.getter = value.getter, .setter = value.setter,
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking, .deleter = value.deleter,
}; .flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
template_proto.setNamedProperty(configuration, null); }, null),
},
bridge.Iterator => { bridge.Iterator => {
// Same as a function, but with a specific name // Same as a function, but with a specific name
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func); 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 (has_js_call_as_function) {
// if (@hasDecl(Struct, "htmldda") and Struct.htmldda) { // if (@hasDecl(Struct, "htmldda") and Struct.htmldda) {
// if (!has_js_call_as_function) { // 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."); // @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); return Indexed.init(T, getter_func, opts);
} }
pub fn namedIndexed(comptime getter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed { pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
return NamedIndexed.init(T, getter_func, opts); return NamedIndexed.init(T, getter_func, setter_func, deleter_func, opts);
} }
pub fn iterator(comptime func: anytype, comptime opts: Iterator.Opts) Iterator { pub fn iterator(comptime func: anytype, comptime opts: Iterator.Opts) Iterator {
@@ -221,14 +221,16 @@ pub const Indexed = struct {
pub const NamedIndexed = struct { pub const NamedIndexed = struct {
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8, 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 { const Opts = struct {
as_typed_array: bool = false, as_typed_array: bool = false,
null_as_undefined: bool = false, null_as_undefined: bool = false,
}; };
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) NamedIndexed { fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
return .{ .getter = struct { const getter_fn = struct {
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info); var caller = Caller.init(info);
@@ -238,7 +240,39 @@ pub const NamedIndexed = struct {
.null_as_undefined = opts.null_as_undefined, .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 { pub const Callable = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void, 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 { 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 { fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info); const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller.init(info); var caller = Caller.init(info);
@@ -286,8 +319,8 @@ pub const Callable = struct {
caller.method(T, func, info, .{ caller.method(T, func, info, .{
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
}}.wrap }
}; }.wrap };
} }
}; };
@@ -457,6 +490,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/DOMNodeIterator.zig"), @import("../webapi/DOMNodeIterator.zig"),
@import("../webapi/NodeFilter.zig"), @import("../webapi/NodeFilter.zig"),
@import("../webapi/Element.zig"), @import("../webapi/Element.zig"),
@import("../webapi/element/DOMStringMap.zig"),
@import("../webapi/element/Attribute.zig"), @import("../webapi/element/Attribute.zig"),
@import("../webapi/element/Html.zig"), @import("../webapi/element/Html.zig"),
@import("../webapi/element/html/IFrame.zig"), @import("../webapi/element/html/IFrame.zig"),

View File

@@ -83,3 +83,22 @@
assertAttributes([{name: 'id', value: 'attr1'}, {name: 'class', value: 'sHow'}]); assertAttributes([{name: 'id', value: 'attr1'}, {name: 'class', value: 'sHow'}]);
</script> </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

@@ -195,11 +195,11 @@ pub const JsApi = struct {
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{}); pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{}); 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") { fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
return page.window; return page.window;
} }
}.defaultView, null, .{.cache = "defaultView"}); }.defaultView, null, .{ .cache = "defaultView" });
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -12,6 +12,7 @@ const collections = @import("collections.zig");
const Selector = @import("selector/Selector.zig"); const Selector = @import("selector/Selector.zig");
pub const Attribute = @import("element/Attribute.zig"); pub const Attribute = @import("element/Attribute.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
pub const DOMStringMap = @import("element/DOMStringMap.zig");
pub const Svg = @import("element/Svg.zig"); pub const Svg = @import("element/Svg.zig");
pub const Html = @import("element/Html.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); 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 { pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attribute {
const attributes = self._attributes orelse return null; const attributes = self._attributes orelse return null;
return attributes.getAttribute(name, self, page); 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 { pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
page.domChanged(); page.domChanged();
var parent = self.asNode(); var parent = self.asNode();
@@ -438,6 +455,10 @@ pub fn getChildElementCount(self: *Element) usize {
return count; 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 { pub fn querySelector(self: *Element, selector: []const u8, page: *Page) !?*Element {
return Selector.querySelector(self.asNode(), selector, page); 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 id = bridge.accessor(Element.getId, Element.setId, .{});
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{}); pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
pub const classList = bridge.accessor(Element.getClassList, null, .{}); 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 style = bridge.accessor(Element.getStyle, null, .{});
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, 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 getAttribute = bridge.function(Element.getAttribute, .{});
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{}); pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
pub const setAttribute = bridge.function(Element.setAttribute, .{}); 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 nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{}); pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{});
pub const childElementCount = bridge.accessor(Element.getChildElementCount, 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 querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});

View File

@@ -45,7 +45,7 @@ pub fn get(self: *const KeyValueList, name: []const u8) ?[]const u8 {
return null; 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; const arena = page.call_arena;
var arr: std.ArrayList([]const u8) = .empty; var arr: std.ArrayList([]const u8) = .empty;
for (self._entries.items) |*entry| { for (self._entries.items) |*entry| {

View File

@@ -614,9 +614,7 @@ pub const JsApi = struct {
pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{}); pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{});
fn _textContext(self: *Node, page: *const Page) !?[]const u8 { fn _textContext(self: *Node, page: *const Page) !?[]const u8 {
// can't call node.getTextContent directly, because // cdata and attributes can return value directly, avoiding the copy
// 1 - document should return null, not empty
// 2 - cdata and attributes can return value directly, avoiding the copy
switch (self._type) { switch (self._type) {
.element => |el| { .element => |el| {
var buf = std.Io.Writer.Allocating.init(page.call_arena); var buf = std.Io.Writer.Allocating.init(page.call_arena);

View File

@@ -149,7 +149,7 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void {
pub fn reportError(self: *Window, err: js.Object, page: *Page) !void { pub fn reportError(self: *Window, err: js.Object, page: *Page) !void {
const error_event = try ErrorEvent.init("error", .{ const error_event = try ErrorEvent.init("error", .{
.@"error" = err, .@"error" = err,
.message = err.toString() catch "Unknown error", .message = err.toString() catch "Unknown error",
.bubbles = false, .bubbles = false,
.cancelable = true, .cancelable = true,

View File

@@ -152,7 +152,7 @@ pub const JsApi = struct {
pub const length = bridge.accessor(HTMLAllCollection.length, null, .{}); pub const length = bridge.accessor(HTMLAllCollection.length, null, .{});
pub const @"[int]" = bridge.indexed(HTMLAllCollection.getAtIndex, .{ .null_as_undefined = true }); 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, .{}); pub const item = bridge.function(_item, .{});
fn _item(self: *HTMLAllCollection, index: i32, page: *Page) ?*Element { 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 length = bridge.accessor(HTMLCollection.length, null, .{});
pub const @"[int]" = bridge.indexed(HTMLCollection.getAtIndex, .{ .null_as_undefined = true }); 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, .{}); pub const item = bridge.function(_item, .{});
fn _item(self: *HTMLCollection, index: i32, page: *Page) ?*Element { 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 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(.{ const method_names = std.StaticStringMap(void).initComptime(.{
.{ "getPropertyValue", {} }, .{ "getPropertyValue", {} },

View File

@@ -31,7 +31,7 @@ pub const JsApi = struct {
pub const Meta = struct { pub const Meta = struct {
pub const name = "MediaQueryList"; pub const name = "MediaQueryList";
pub const prototype_chain = bridge.prototypeChain(); 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, .{}); pub const media = bridge.accessor(MediaQueryList.getMedia, null, .{});

View File

@@ -386,7 +386,7 @@ pub const NamedNodeMap = struct {
pub const length = bridge.accessor(NamedNodeMap.length, null, .{}); pub const length = bridge.accessor(NamedNodeMap.length, null, .{});
pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, .{ .null_as_undefined = true }); 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 getNamedItem = bridge.function(NamedNodeMap.getByName, .{});
pub const item = bridge.function(_item, .{}); pub const item = bridge.function(_item, .{});
fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute { 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

@@ -33,7 +33,7 @@ pub const Build = struct {
const el = node.as(Element); const el = node.as(Element);
const on_load = el.getAttributeSafe("onload") orelse return; const on_load = el.getAttributeSafe("onload") orelse return;
page.window._on_load = page.js.stringToFunction(on_load) catch |err| blk: { 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; break :blk null;
}; };
} }

View File

@@ -80,14 +80,14 @@ pub const Build = struct {
if (element.getAttributeSafe("onload")) |on_load| { if (element.getAttributeSafe("onload")) |on_load| {
self._on_load = page.js.stringToFunction(on_load) catch |err| blk: { 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; break :blk null;
}; };
} }
if (element.getAttributeSafe("onerror")) |on_error| { if (element.getAttributeSafe("onerror")) |on_error| {
self._on_error = page.js.stringToFunction(on_error) catch |err| blk: { 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; break :blk null;
}; };
} }

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 segments: std.ArrayList(Segment) = .empty;
var current_compound: std.ArrayList(Part) = .empty; var current_compound: std.ArrayList(Part) = .empty;
// Parse the first compound (no combinator before it) // Parse the first compound (no combinator before it)
while (parser.skipSpaces()) { while (parser.skipSpaces()) {
if (parser.peek() == 0) break; if (parser.peek() == 0) break;