mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Add element.dataset API
Uses the State to store the dataset, but, on first load, loads the data attributes from the DOM.
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const parser = @import("netsurf.zig");
|
||||
const DataSet = @import("html/DataSet.zig");
|
||||
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
|
||||
// for HTMLScript (but probably needs to be added to more)
|
||||
@@ -36,6 +37,7 @@ onerror: ?Env.Function = null,
|
||||
|
||||
// for HTMLElement
|
||||
style: CSSStyleDeclaration = .empty,
|
||||
dataset: ?DataSet = null,
|
||||
|
||||
// for html/document
|
||||
ready_state: ReadyState = .loading,
|
||||
|
||||
92
src/browser/html/DataSet.zig
Normal file
92
src/browser/html/DataSet.zig
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const Page = @import("../page.zig").Page;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const DataSet = @This();
|
||||
|
||||
attributes: std.StringHashMapUnmanaged([]const u8),
|
||||
|
||||
pub const empty: DataSet = .{
|
||||
.attributes = .empty,
|
||||
};
|
||||
|
||||
const GetResult = union(enum) {
|
||||
value: []const u8,
|
||||
undefined: void,
|
||||
};
|
||||
pub fn named_get(self: *const DataSet, name: []const u8, _: *bool) GetResult {
|
||||
if (self.attributes.get(name)) |value| {
|
||||
return .{ .value = value };
|
||||
}
|
||||
return .{ .undefined = {} };
|
||||
}
|
||||
|
||||
pub fn named_set(self: *DataSet, name: []const u8, value: []const u8, _: *bool, page: *Page) !void {
|
||||
const arena = page.arena;
|
||||
const gop = try self.attributes.getOrPut(arena, name);
|
||||
errdefer _ = self.attributes.remove(name);
|
||||
|
||||
if (!gop.found_existing) {
|
||||
gop.key_ptr.* = try arena.dupe(u8, name);
|
||||
}
|
||||
gop.value_ptr.* = try arena.dupe(u8, value);
|
||||
}
|
||||
|
||||
pub fn named_delete(self: *DataSet, name: []const u8, _: *bool) void {
|
||||
_ = self.attributes.remove(name);
|
||||
}
|
||||
|
||||
pub fn normalizeName(allocator: Allocator, name: []const u8) ![]const u8 {
|
||||
std.debug.assert(std.mem.startsWith(u8, name, "data-"));
|
||||
var owned = try allocator.alloc(u8, name.len - 5);
|
||||
|
||||
var pos: usize = 0;
|
||||
var capitalize = false;
|
||||
for (name[5..]) |c| {
|
||||
if (c == '-') {
|
||||
capitalize = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (capitalize) {
|
||||
capitalize = false;
|
||||
owned[pos] = std.ascii.toUpper(c);
|
||||
} else {
|
||||
owned[pos] = c;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
return owned[0..pos];
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.DataSet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
|
||||
defer runner.deinit();
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let el1 = document.createElement('div')", null },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "el1.dataset.x = '123'", "123" },
|
||||
.{ "delete el1.dataset.x", "true" },
|
||||
.{ "el1.dataset.x", "undefined" },
|
||||
.{ "delete el1.dataset.other", "true" }, // yes, this is right
|
||||
}, .{});
|
||||
}
|
||||
@@ -27,6 +27,7 @@ const urlStitch = @import("../../url.zig").URL.stitch;
|
||||
const URL = @import("../url/url.zig").URL;
|
||||
const Node = @import("../dom/node.zig").Node;
|
||||
const Element = @import("../dom/element.zig").Element;
|
||||
const DataSet = @import("DataSet.zig");
|
||||
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
|
||||
@@ -122,6 +123,36 @@ pub const HTMLElement = struct {
|
||||
return &state.style;
|
||||
}
|
||||
|
||||
pub fn get_dataset(e: *parser.ElementHTML, page: *Page) !*DataSet {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(e));
|
||||
if (state.dataset) |*ds| {
|
||||
return ds;
|
||||
}
|
||||
|
||||
// The first time this is called, load the data attributes from the DOM
|
||||
var ds: DataSet = .empty;
|
||||
|
||||
if (try parser.nodeGetAttributes(@ptrCast(e))) |map| {
|
||||
const arena = page.arena;
|
||||
const count = try parser.namedNodeMapGetLength(map);
|
||||
for (0..count) |i| {
|
||||
const attr = try parser.namedNodeMapItem(map, @intCast(i)) orelse continue;
|
||||
const name = try parser.attributeGetName(attr);
|
||||
if (!std.mem.startsWith(u8, name, "data-")) {
|
||||
continue;
|
||||
}
|
||||
const normalized_name = try DataSet.normalizeName(arena, name);
|
||||
const value = try parser.attributeGetValue(attr) orelse "";
|
||||
// I don't think we need to dupe value, It'll live in libdom for
|
||||
// as long as the page due to the fact that we're using an arena.
|
||||
try ds.attributes.put(arena, normalized_name, value);
|
||||
}
|
||||
}
|
||||
|
||||
state.dataset = ds;
|
||||
return &state.dataset.?;
|
||||
}
|
||||
|
||||
pub fn get_innerText(e: *parser.ElementHTML) ![]const u8 {
|
||||
const n = @as(*parser.Node, @ptrCast(e));
|
||||
return try parser.nodeTextContent(n) orelse "";
|
||||
@@ -1561,6 +1592,13 @@ test "Browser.HTML.Element" {
|
||||
}, .{});
|
||||
}
|
||||
|
||||
test "Browser.HTML.Element.DataSet" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=x data-power='over 9000' data-empty data-some-long-key=ok></div>" });
|
||||
defer runner.deinit();
|
||||
|
||||
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" } }, .{});
|
||||
}
|
||||
|
||||
test "Browser.HTML.HtmlInputElement.properties" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .url = "https://lightpanda.io/noslashattheend" });
|
||||
defer runner.deinit();
|
||||
|
||||
@@ -36,6 +36,7 @@ pub const Interfaces = .{
|
||||
History,
|
||||
Location,
|
||||
MediaQueryList,
|
||||
@import("DataSet.zig"),
|
||||
@import("screen.zig").Interfaces,
|
||||
@import("error_event.zig").ErrorEvent,
|
||||
};
|
||||
|
||||
@@ -1931,7 +1931,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
}
|
||||
|
||||
generateIndexer(Struct, template_proto);
|
||||
generateNamedIndexer(Struct, template_proto);
|
||||
generateNamedIndexer(Struct, template.getInstanceTemplate());
|
||||
generateUndetectable(Struct, template.getInstanceTemplate());
|
||||
}
|
||||
|
||||
@@ -2116,7 +2116,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const configuration = v8.NamedPropertyHandlerConfiguration{
|
||||
|
||||
var configuration = v8.NamedPropertyHandlerConfiguration{
|
||||
.getter = struct {
|
||||
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
@@ -2138,13 +2139,37 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
||||
};
|
||||
|
||||
// If you're trying to implement setter, read:
|
||||
// https://groups.google.com/g/v8-users/c/8tahYBsHpgY/m/IteS7Wn2AAAJ
|
||||
// The issue I had was
|
||||
// (a) where to attache it: does it go ont he instance_template
|
||||
// instead of the prototype?
|
||||
// (b) defining the getter or query to respond with the
|
||||
// PropertyAttribute to indicate if the property can be set
|
||||
if (@hasDecl(Struct, "named_set")) {
|
||||
configuration.setter = struct {
|
||||
fn callback(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(Self, State).init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "named_set");
|
||||
return caller.setNamedIndex(Struct, named_function, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info) catch |err| blk: {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
break :blk v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
}.callback;
|
||||
}
|
||||
|
||||
if (@hasDecl(Struct, "named_delete")) {
|
||||
configuration.deleter = struct {
|
||||
fn callback(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(Self, State).init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "named_delete");
|
||||
return caller.deleteNamedIndex(Struct, named_function, .{ .handle = c_name.? }, info) catch |err| blk: {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
break :blk v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
}.callback;
|
||||
}
|
||||
template_proto.setNamedProperty(configuration, null);
|
||||
}
|
||||
|
||||
@@ -2646,37 +2671,63 @@ fn Caller(comptime E: type, comptime State: type) type {
|
||||
}
|
||||
|
||||
fn getNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const js_context = self.js_context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
const NamedGet = @TypeOf(func);
|
||||
if (@typeInfo(NamedGet).@"fn".return_type == null) {
|
||||
@compileError(named_function.full_name ++ " must have a return type");
|
||||
}
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args: ParamterTypes(NamedGet) = undefined;
|
||||
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
|
||||
switch (arg_fields.len) {
|
||||
0, 1, 2 => @compileError(named_function.full_name ++ " must take at least a u32 and *bool parameter"),
|
||||
3, 4 => {
|
||||
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = &has_value;
|
||||
if (comptime arg_fields.len == 4) {
|
||||
comptime assertIsStateArg(Struct, named_function, 3);
|
||||
@field(args, "3") = js_context.state;
|
||||
}
|
||||
},
|
||||
else => @compileError(named_function.full_name ++ " has too many parmaters"),
|
||||
}
|
||||
var args = try self.getArgs(Struct, named_function, 3, info);
|
||||
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
if (has_value == false) {
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
info.getReturnValue().set(try js_context.zigValueToJs(res));
|
||||
info.getReturnValue().set(try self.js_context.zigValueToJs(res));
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
fn setNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const js_context = self.js_context;
|
||||
const func = @field(Struct, named_function.name);
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args = try self.getArgs(Struct, named_function, 4, info);
|
||||
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = try js_context.jsValueToZig(named_function, @TypeOf(@field(args, "2")), js_value);
|
||||
@field(args, "3") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
return namedSetOrDeleteCall(res, has_value);
|
||||
}
|
||||
|
||||
fn deleteNamedIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, name: v8.Name, info: v8.PropertyCallbackInfo) !u8 {
|
||||
const func = @field(Struct, named_function.name);
|
||||
comptime assertSelfReceiver(Struct, named_function);
|
||||
|
||||
var has_value = true;
|
||||
var args = try self.getArgs(Struct, named_function, 3, info);
|
||||
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
|
||||
@field(args, "0") = zig_instance;
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = &has_value;
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
return namedSetOrDeleteCall(res, has_value);
|
||||
}
|
||||
|
||||
fn namedSetOrDeleteCall(res: anytype, has_value: bool) !u8 {
|
||||
if (@typeInfo(@TypeOf(res)) == .error_union) {
|
||||
_ = try res;
|
||||
}
|
||||
if (has_value == false) {
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user