Skip to content

Commit 2aa5eb8

Browse files
committed
Add element.dataset API
Uses the State to store the dataset, but, on first load, loads the data attributes from the DOM.
1 parent 2815f02 commit 2aa5eb8

File tree

5 files changed

+215
-31
lines changed

5 files changed

+215
-31
lines changed

src/browser/State.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
const Env = @import("env.zig").Env;
3030
const parser = @import("netsurf.zig");
31+
const DataSet = @import("html/DataSet.zig");
3132
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
3233

3334
// for HTMLScript (but probably needs to be added to more)
@@ -36,6 +37,7 @@ onerror: ?Env.Function = null,
3637

3738
// for HTMLElement
3839
style: CSSStyleDeclaration = .empty,
40+
dataset: ?DataSet = null,
3941

4042
// for html/document
4143
ready_state: ReadyState = .loading,

src/browser/html/DataSet.zig

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <francis@lightpanda.io>
4+
// Pierre Tachoire <pierre@lightpanda.io>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
const std = @import("std");
19+
const Page = @import("../page.zig").Page;
20+
const Allocator = std.mem.Allocator;
21+
22+
const DataSet = @This();
23+
24+
attributes: std.StringHashMapUnmanaged([]const u8),
25+
26+
pub const empty: DataSet = .{
27+
.attributes = .empty,
28+
};
29+
30+
const GetResult = union(enum) {
31+
value: []const u8,
32+
undefined: void,
33+
};
34+
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool) GetResult {
35+
if (self.attributes.get(name)) |value| {
36+
return .{ .value = value };
37+
}
38+
return .{ .undefined = {} };
39+
}
40+
41+
pub fn named_set(self: *DataSet, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
42+
const arena = page.arena;
43+
const gop = try self.attributes.getOrPut(arena, name);
44+
errdefer _ = self.attributes.remove(name);
45+
46+
if (!gop.found_existing) {
47+
gop.key_ptr.* = try arena.dupe(u8, name);
48+
}
49+
gop.value_ptr.* = try arena.dupe(u8, value);
50+
}
51+
52+
pub fn named_delete(self: *DataSet, name: []const u8, _: *bool) void {
53+
_ = self.attributes.remove(name);
54+
}
55+
56+
pub fn normalizeName(allocator: Allocator, name: []const u8) ![]const u8 {
57+
std.debug.assert(std.mem.startsWith(u8, name, "data-"));
58+
var owned = try allocator.alloc(u8, name.len - 5);
59+
60+
var pos: usize = 0;
61+
var capitalize = false;
62+
for (name[5..]) |c| {
63+
if (c == '-') {
64+
capitalize = true;
65+
continue;
66+
}
67+
68+
if (capitalize) {
69+
capitalize = false;
70+
owned[pos] = std.ascii.toUpper(c);
71+
} else {
72+
owned[pos] = c;
73+
}
74+
pos += 1;
75+
}
76+
return owned[0..pos];
77+
}
78+
79+
const testing = @import("../../testing.zig");
80+
test "Browser.HTML.DataSet" {
81+
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
82+
defer runner.deinit();
83+
84+
try runner.testCases(&.{
85+
.{ "let el1 = document.createElement('div')", null },
86+
.{ "el1.dataset.x", "undefined" },
87+
.{ "el1.dataset.x = '123'", "123" },
88+
.{ "delete el1.dataset.x", "true" },
89+
.{ "el1.dataset.x", "undefined" },
90+
.{ "delete el1.dataset.other", "true" }, // yes, this is right
91+
}, .{});
92+
}

src/browser/html/elements.zig

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const urlStitch = @import("../../url.zig").URL.stitch;
2727
const URL = @import("../url/url.zig").URL;
2828
const Node = @import("../dom/node.zig").Node;
2929
const Element = @import("../dom/element.zig").Element;
30+
const DataSet = @import("DataSet.zig");
3031

3132
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
3233

@@ -122,6 +123,36 @@ pub const HTMLElement = struct {
122123
return &state.style;
123124
}
124125

126+
pub fn get_dataset(e: *parser.ElementHTML, page: *Page) !*DataSet {
127+
const state = try page.getOrCreateNodeState(@ptrCast(e));
128+
if (state.dataset) |*ds| {
129+
return ds;
130+
}
131+
132+
// The first time this is called, load the data attributes from the DOM
133+
var ds: DataSet = .empty;
134+
135+
if (try parser.nodeGetAttributes(@ptrCast(e))) |map| {
136+
const arena = page.arena;
137+
const count = try parser.namedNodeMapGetLength(map);
138+
for (0..count) |i| {
139+
const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse continue;
140+
const name = try parser.attributeGetName(attr);
141+
if (!std.mem.startsWith(u8, name, "data-")) {
142+
continue;
143+
}
144+
const normalized_name = try DataSet.normalizeName(arena, name);
145+
const value = try parser.attributeGetValue(attr) orelse "";
146+
// I don't think we need to dupe value, It'll live in libdom for
147+
// as long as the page due to the fact that we're using an arena.
148+
try ds.attributes.put(arena, normalized_name, value);
149+
}
150+
}
151+
152+
state.dataset = ds;
153+
return &state.dataset.?;
154+
}
155+
125156
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
126157
const n = @as(*parser.Node, @ptrCast(e));
127158
return try parser.nodeTextContent(n) orelse "";
@@ -1561,6 +1592,13 @@ test "Browser.HTML.Element" {
15611592
}, .{});
15621593
}
15631594

1595+
test "Browser.HTML.Element.DataSet" {
1596+
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=x data-power='over 9000' data-empty data-some-long-key=ok></div>" });
1597+
defer runner.deinit();
1598+
1599+
try runner.testCases(&.{ .{ "let div = document.getElementById('x')", null }, .{ "div.dataset.nope", "undefined" }, .{ "div.dataset.power", "over 9000" }, .{ "div.dataset.empty", "" }, .{ "div.dataset.someLongKey", "ok" }, .{ "delete div.dataset.power", "true" }, .{ "div.dataset.power", "undefined" } }, .{});
1600+
}
1601+
15641602
test "Browser.HTML.HtmlInputElement.properties" {
15651603
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" });
15661604
defer runner.deinit();

src/browser/html/html.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub const Interfaces = .{
3636
History,
3737
Location,
3838
MediaQueryList,
39+
@import("DataSet.zig"),
3940
@import("screen.zig").Interfaces,
4041
@import("error_event.zig").ErrorEvent,
4142
};

src/runtime/js.zig

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,7 +1931,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
19311931
}
19321932

19331933
generateIndexer(Struct, template_proto);
1934-
generateNamedIndexer(Struct, template_proto);
1934+
generateNamedIndexer(Struct, template.getInstanceTemplate());
19351935
generateUndetectable(Struct, template.getInstanceTemplate());
19361936
}
19371937

@@ -2116,7 +2116,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
21162116
}
21172117
return;
21182118
}
2119-
const configuration = v8.NamedPropertyHandlerConfiguration{
2119+
2120+
var configuration = v8.NamedPropertyHandlerConfiguration{
21202121
.getter = struct {
21212122
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
21222123
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
@@ -2138,13 +2139,37 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
21382139
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
21392140
};
21402141

2141-
// If you're trying to implement setter, read:
2142-
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
2143-
// The issue I had was
2144-
// (a) where to attache it: does it go ont he instance_template
2145-
// instead of the prototype?
2146-
// (b) defining the getter or query to respond with the
2147-
// PropertyAttribute to indicate if the property can be set
2142+
if (@hasDecl(Struct, "named_set")) {
2143+
configuration.setter = struct {
2144+
fn callback(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
2145+
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
2146+
var caller = Caller(Self, State).init(info);
2147+
defer caller.deinit();
2148+
2149+
const named_function = comptime NamedFunction.init(Struct, "named_set");
2150+
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
2151+
caller.handleError(Struct, named_function, err, info);
2152+
break :blk v8.Intercepted.No;
2153+
};
2154+
}
2155+
}.callback;
2156+
}
2157+
2158+
if (@hasDecl(Struct, "named_delete")) {
2159+
configuration.deleter = struct {
2160+
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
2161+
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
2162+
var caller = Caller(Self, State).init(info);
2163+
defer caller.deinit();
2164+
2165+
const named_function = comptime NamedFunction.init(Struct, "named_delete");
2166+
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
2167+
caller.handleError(Struct, named_function, err, info);
2168+
break :blk v8.Intercepted.No;
2169+
};
2170+
}
2171+
}.callback;
2172+
}
21482173
template_proto.setNamedProperty(configuration, null);
21492174
}
21502175

@@ -2646,37 +2671,63 @@ fn Caller(comptime E: type, comptime State: type) type {
26462671
}
26472672

26482673
fn getNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
2649-
const js_context = self.js_context;
26502674
const func = @field(Struct, named_function.name);
2651-
const NamedGet = @TypeOf(func);
2652-
if (@typeInfo(NamedGet).@"fn".return_type == null) {
2653-
@compileError(named_function.full_name ++ " must have a return type");
2654-
}
2675+
comptime assertSelfReceiver(Struct, named_function);
26552676

26562677
var has_value = true;
2657-
var args: ParamterTypes(NamedGet) = undefined;
2658-
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
2659-
switch (arg_fields.len) {
2660-
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
2661-
3, 4 => {
2662-
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
2663-
comptime assertSelfReceiver(Struct, named_function);
2664-
@field(args, "0") = zig_instance;
2665-
@field(args, "1") = try self.nameToString(name);
2666-
@field(args, "2") = &has_value;
2667-
if (comptime arg_fields.len == 4) {
2668-
comptime assertIsStateArg(Struct, named_function, 3);
2669-
@field(args, "3") = js_context.state;
2670-
}
2671-
},
2672-
else => @compileError(named_function.full_name ++ " has too many parmaters"),
2678+
var args = try self.getArgs(Struct, named_function, 3, info);
2679+
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
2680+
@field(args, "0") = zig_instance;
2681+
@field(args, "1") = try self.nameToString(name);
2682+
@field(args, "2") = &has_value;
2683+
2684+
const res = @call(.auto, func, args);
2685+
if (has_value == false) {
2686+
return v8.Intercepted.No;
26732687
}
2688+
info.getReturnValue().set(try self.js_context.zigValueToJs(res));
2689+
return v8.Intercepted.Yes;
2690+
}
2691+
2692+
fn setNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
2693+
const js_context = self.js_context;
2694+
const func = @field(Struct, named_function.name);
2695+
comptime assertSelfReceiver(Struct, named_function);
2696+
2697+
var has_value = true;
2698+
var args = try self.getArgs(Struct, named_function, 4, info);
2699+
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
2700+
@field(args, "0") = zig_instance;
2701+
@field(args, "1") = try self.nameToString(name);
2702+
@field(args, "2") = try js_context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
2703+
@field(args, "3") = &has_value;
26742704

26752705
const res = @call(.auto, func, args);
2706+
return namedSetOrDeleteCall(res, has_value);
2707+
}
2708+
2709+
fn deleteNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
2710+
const func = @field(Struct, named_function.name);
2711+
comptime assertSelfReceiver(Struct, named_function);
2712+
2713+
var has_value = true;
2714+
var args = try self.getArgs(Struct, named_function, 3, info);
2715+
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
2716+
@field(args, "0") = zig_instance;
2717+
@field(args, "1") = try self.nameToString(name);
2718+
@field(args, "2") = &has_value;
2719+
2720+
const res = @call(.auto, func, args);
2721+
return namedSetOrDeleteCall(res, has_value);
2722+
}
2723+
2724+
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
2725+
if (@typeInfo(@TypeOf(res)) == .error_union) {
2726+
_ = try res;
2727+
}
26762728
if (has_value == false) {
26772729
return v8.Intercepted.No;
26782730
}
2679-
info.getReturnValue().set(try js_context.zigValueToJs(res));
26802731
return v8.Intercepted.Yes;
26812732
}
26822733

0 commit comments

Comments
 (0)