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
	 Karl Seguin
					Karl Seguin