mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge branch 'main' into semantic-tree
This commit is contained in:
@@ -373,6 +373,29 @@ fn getFunction(handler: anytype, local: *const js.Local) ?js.Function {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if there are any listeners for a direct dispatch (non-DOM target).
|
||||||
|
/// Use this to avoid creating an event when there are no listeners.
|
||||||
|
pub fn hasDirectListeners(self: *EventManager, target: *EventTarget, typ: []const u8, handler: anytype) bool {
|
||||||
|
if (hasHandler(handler)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return self.lookup.get(.{
|
||||||
|
.event_target = @intFromPtr(target),
|
||||||
|
.type_string = .wrap(typ),
|
||||||
|
}) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasHandler(handler: anytype) bool {
|
||||||
|
const ti = @typeInfo(@TypeOf(handler));
|
||||||
|
if (ti == .null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ti == .optional) {
|
||||||
|
return handler != null;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void {
|
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void {
|
||||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||||
|
|
||||||
|
|||||||
@@ -791,24 +791,19 @@ fn _documentIsComplete(self: *Page) !void {
|
|||||||
try self.dispatchLoad();
|
try self.dispatchLoad();
|
||||||
|
|
||||||
// Dispatch window.load event.
|
// Dispatch window.load event.
|
||||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
const window_target = self.window.asEventTarget();
|
||||||
// This event is weird, it's dispatched directly on the window, but
|
if (self._event_manager.hasDirectListeners(window_target, "load", self.window._on_load)) {
|
||||||
// with the document as the target.
|
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||||
event._target = self.document.asEventTarget();
|
// This event is weird, it's dispatched directly on the window, but
|
||||||
try self._event_manager.dispatchDirect(
|
// with the document as the target.
|
||||||
self.window.asEventTarget(),
|
event._target = self.document.asEventTarget();
|
||||||
event,
|
try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = "page load" });
|
||||||
self.window._on_load,
|
}
|
||||||
.{ .inject_target = false, .context = "page load" },
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
|
if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) {
|
||||||
try self._event_manager.dispatchDirect(
|
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
|
||||||
self.window.asEventTarget(),
|
try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" });
|
||||||
pageshow_event,
|
}
|
||||||
self.window._on_pageshow,
|
|
||||||
.{ .context = "page show" },
|
|
||||||
);
|
|
||||||
|
|
||||||
self.notifyParentLoadComplete();
|
self.notifyParentLoadComplete();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,17 +167,17 @@ pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
|
|||||||
const query_end = if (query_start) |_| (fragment_start orelse url.len) else path_end;
|
const query_end = if (query_start) |_| (fragment_start orelse url.len) else path_end;
|
||||||
|
|
||||||
const path_to_encode = url[path_start..path_end];
|
const path_to_encode = url[path_start..path_end];
|
||||||
const encoded_path = try percentEncodeSegment(allocator, path_to_encode, true);
|
const encoded_path = try percentEncodeSegment(allocator, path_to_encode, .path);
|
||||||
|
|
||||||
const encoded_query = if (query_start) |qs| blk: {
|
const encoded_query = if (query_start) |qs| blk: {
|
||||||
const query_to_encode = url[qs + 1 .. query_end];
|
const query_to_encode = url[qs + 1 .. query_end];
|
||||||
const encoded = try percentEncodeSegment(allocator, query_to_encode, false);
|
const encoded = try percentEncodeSegment(allocator, query_to_encode, .query);
|
||||||
break :blk encoded;
|
break :blk encoded;
|
||||||
} else null;
|
} else null;
|
||||||
|
|
||||||
const encoded_fragment = if (fragment_start) |fs| blk: {
|
const encoded_fragment = if (fragment_start) |fs| blk: {
|
||||||
const fragment_to_encode = url[fs + 1 ..];
|
const fragment_to_encode = url[fs + 1 ..];
|
||||||
const encoded = try percentEncodeSegment(allocator, fragment_to_encode, false);
|
const encoded = try percentEncodeSegment(allocator, fragment_to_encode, .query);
|
||||||
break :blk encoded;
|
break :blk encoded;
|
||||||
} else null;
|
} else null;
|
||||||
|
|
||||||
@@ -204,11 +204,13 @@ pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
|
|||||||
return buf.items[0 .. buf.items.len - 1 :0];
|
return buf.items[0 .. buf.items.len - 1 :0];
|
||||||
}
|
}
|
||||||
|
|
||||||
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime is_path: bool) ![]const u8 {
|
const EncodeSet = enum { path, query, userinfo };
|
||||||
|
|
||||||
|
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime encode_set: EncodeSet) ![]const u8 {
|
||||||
// Check if encoding is needed
|
// Check if encoding is needed
|
||||||
var needs_encoding = false;
|
var needs_encoding = false;
|
||||||
for (segment) |c| {
|
for (segment) |c| {
|
||||||
if (shouldPercentEncode(c, is_path)) {
|
if (shouldPercentEncode(c, encode_set)) {
|
||||||
needs_encoding = true;
|
needs_encoding = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -235,7 +237,7 @@ fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime is_p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldPercentEncode(c, is_path)) {
|
if (shouldPercentEncode(c, encode_set)) {
|
||||||
try buf.writer(allocator).print("%{X:0>2}", .{c});
|
try buf.writer(allocator).print("%{X:0>2}", .{c});
|
||||||
} else {
|
} else {
|
||||||
try buf.append(allocator, c);
|
try buf.append(allocator, c);
|
||||||
@@ -245,16 +247,17 @@ fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime is_p
|
|||||||
return buf.items;
|
return buf.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shouldPercentEncode(c: u8, comptime is_path: bool) bool {
|
fn shouldPercentEncode(c: u8, comptime encode_set: EncodeSet) bool {
|
||||||
return switch (c) {
|
return switch (c) {
|
||||||
// Unreserved characters (RFC 3986)
|
// Unreserved characters (RFC 3986)
|
||||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => false,
|
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => false,
|
||||||
// sub-delims allowed in both path and query
|
// sub-delims allowed in path/query but some must be encoded in userinfo
|
||||||
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => false,
|
'!', '$', '&', '\'', '(', ')', '*', '+', ',' => false,
|
||||||
// Separators allowed in both path and query
|
';', '=' => encode_set == .userinfo,
|
||||||
'/', ':', '@' => false,
|
// Separators: userinfo must encode these
|
||||||
// Query-specific: '?' is allowed in queries but not in paths
|
'/', ':', '@' => encode_set == .userinfo,
|
||||||
'?' => comptime is_path,
|
// '?' is allowed in queries but not in paths or userinfo
|
||||||
|
'?' => encode_set != .query,
|
||||||
// Everything else needs encoding (including space)
|
// Everything else needs encoding (including space)
|
||||||
else => true,
|
else => true,
|
||||||
};
|
};
|
||||||
@@ -514,7 +517,7 @@ pub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
|||||||
const search = getSearch(current);
|
const search = getSearch(current);
|
||||||
const hash = getHash(current);
|
const hash = getHash(current);
|
||||||
|
|
||||||
// Check if the host includes a port
|
// Check if the new value includes a port
|
||||||
const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':');
|
const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':');
|
||||||
const clean_host = if (colon_pos) |pos| blk: {
|
const clean_host = if (colon_pos) |pos| blk: {
|
||||||
const port_str = value[pos + 1 ..];
|
const port_str = value[pos + 1 ..];
|
||||||
@@ -526,7 +529,14 @@ pub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
|||||||
break :blk value[0..pos];
|
break :blk value[0..pos];
|
||||||
}
|
}
|
||||||
break :blk value;
|
break :blk value;
|
||||||
} else value;
|
} else blk: {
|
||||||
|
// No port in new value - preserve existing port
|
||||||
|
const current_port = getPort(current);
|
||||||
|
if (current_port.len > 0) {
|
||||||
|
break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ value, current_port });
|
||||||
|
}
|
||||||
|
break :blk value;
|
||||||
|
};
|
||||||
|
|
||||||
return buildUrl(allocator, protocol, clean_host, pathname, search, hash);
|
return buildUrl(allocator, protocol, clean_host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
@@ -544,6 +554,9 @@ pub fn setHostname(current: [:0]const u8, value: []const u8, allocator: Allocato
|
|||||||
pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 {
|
pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 {
|
||||||
const hostname = getHostname(current);
|
const hostname = getHostname(current);
|
||||||
const protocol = getProtocol(current);
|
const protocol = getProtocol(current);
|
||||||
|
const pathname = getPathname(current);
|
||||||
|
const search = getSearch(current);
|
||||||
|
const hash = getHash(current);
|
||||||
|
|
||||||
// Handle null or default ports
|
// Handle null or default ports
|
||||||
const new_host = if (value) |port_str| blk: {
|
const new_host = if (value) |port_str| blk: {
|
||||||
@@ -560,7 +573,7 @@ pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator)
|
|||||||
break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ hostname, port_str });
|
break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ hostname, port_str });
|
||||||
} else hostname;
|
} else hostname;
|
||||||
|
|
||||||
return setHost(current, new_host, allocator);
|
return buildUrl(allocator, protocol, new_host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||||
@@ -608,6 +621,64 @@ pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) !
|
|||||||
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setUsername(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||||
|
const protocol = getProtocol(current);
|
||||||
|
const host = getHost(current);
|
||||||
|
const pathname = getPathname(current);
|
||||||
|
const search = getSearch(current);
|
||||||
|
const hash = getHash(current);
|
||||||
|
const password = getPassword(current);
|
||||||
|
|
||||||
|
const encoded_username = try percentEncodeSegment(allocator, value, .userinfo);
|
||||||
|
return buildUrlWithUserInfo(allocator, protocol, encoded_username, password, host, pathname, search, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setPassword(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
|
||||||
|
const protocol = getProtocol(current);
|
||||||
|
const host = getHost(current);
|
||||||
|
const pathname = getPathname(current);
|
||||||
|
const search = getSearch(current);
|
||||||
|
const hash = getHash(current);
|
||||||
|
const username = getUsername(current);
|
||||||
|
|
||||||
|
const encoded_password = try percentEncodeSegment(allocator, value, .userinfo);
|
||||||
|
return buildUrlWithUserInfo(allocator, protocol, username, encoded_password, host, pathname, search, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildUrlWithUserInfo(
|
||||||
|
allocator: Allocator,
|
||||||
|
protocol: []const u8,
|
||||||
|
username: []const u8,
|
||||||
|
password: []const u8,
|
||||||
|
host: []const u8,
|
||||||
|
pathname: []const u8,
|
||||||
|
search: []const u8,
|
||||||
|
hash: []const u8,
|
||||||
|
) ![:0]const u8 {
|
||||||
|
if (username.len == 0 and password.len == 0) {
|
||||||
|
return buildUrl(allocator, protocol, host, pathname, search, hash);
|
||||||
|
} else if (password.len == 0) {
|
||||||
|
return std.fmt.allocPrintSentinel(allocator, "{s}//{s}@{s}{s}{s}{s}", .{
|
||||||
|
protocol,
|
||||||
|
username,
|
||||||
|
host,
|
||||||
|
pathname,
|
||||||
|
search,
|
||||||
|
hash,
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
return std.fmt.allocPrintSentinel(allocator, "{s}//{s}:{s}@{s}{s}{s}{s}", .{
|
||||||
|
protocol,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
host,
|
||||||
|
pathname,
|
||||||
|
search,
|
||||||
|
hash,
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 {
|
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 {
|
||||||
if (query_string.len == 0) {
|
if (query_string.len == 0) {
|
||||||
return arena.dupeZ(u8, url);
|
return arena.dupeZ(u8, url);
|
||||||
|
|||||||
@@ -734,7 +734,7 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info:
|
|||||||
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
||||||
const slice_type = last_parameter_type_info.pointer.child;
|
const slice_type = last_parameter_type_info.pointer.child;
|
||||||
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
|
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
|
||||||
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
if (slice_type == js.Value or (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8)) {
|
||||||
is_variadic = true;
|
is_variadic = true;
|
||||||
if (js_parameter_count == 0) {
|
if (js_parameter_count == 0) {
|
||||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||||
|
|||||||
489
src/browser/structured_data.zig
Normal file
489
src/browser/structured_data.zig
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
// Copyright (C) 2023-2026 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");
|
||||||
|
const URL = @import("URL.zig");
|
||||||
|
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Node = @import("webapi/Node.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// Key-value pair for structured data properties.
|
||||||
|
pub const Property = struct {
|
||||||
|
key: []const u8,
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const AlternateLink = struct {
|
||||||
|
href: []const u8,
|
||||||
|
hreflang: ?[]const u8,
|
||||||
|
type: ?[]const u8,
|
||||||
|
title: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const StructuredData = struct {
|
||||||
|
json_ld: []const []const u8,
|
||||||
|
open_graph: []const Property,
|
||||||
|
twitter_card: []const Property,
|
||||||
|
meta: []const Property,
|
||||||
|
links: []const Property,
|
||||||
|
alternate: []const AlternateLink,
|
||||||
|
|
||||||
|
pub fn jsonStringify(self: *const StructuredData, jw: anytype) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
|
||||||
|
try jw.objectField("jsonLd");
|
||||||
|
try jw.write(self.json_ld);
|
||||||
|
|
||||||
|
try jw.objectField("openGraph");
|
||||||
|
try writeProperties(jw, self.open_graph);
|
||||||
|
|
||||||
|
try jw.objectField("twitterCard");
|
||||||
|
try writeProperties(jw, self.twitter_card);
|
||||||
|
|
||||||
|
try jw.objectField("meta");
|
||||||
|
try writeProperties(jw, self.meta);
|
||||||
|
|
||||||
|
try jw.objectField("links");
|
||||||
|
try writeProperties(jw, self.links);
|
||||||
|
|
||||||
|
if (self.alternate.len > 0) {
|
||||||
|
try jw.objectField("alternate");
|
||||||
|
try jw.beginArray();
|
||||||
|
for (self.alternate) |alt| {
|
||||||
|
try jw.beginObject();
|
||||||
|
try jw.objectField("href");
|
||||||
|
try jw.write(alt.href);
|
||||||
|
if (alt.hreflang) |v| {
|
||||||
|
try jw.objectField("hreflang");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
if (alt.type) |v| {
|
||||||
|
try jw.objectField("type");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
if (alt.title) |v| {
|
||||||
|
try jw.objectField("title");
|
||||||
|
try jw.write(v);
|
||||||
|
}
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Serializes properties as a JSON object. When a key appears multiple times
|
||||||
|
/// (e.g. multiple og:image tags), values are grouped into an array.
|
||||||
|
/// Alternatives considered: always-array values (verbose), or an array of
|
||||||
|
/// {key, value} pairs (preserves order but less ergonomic for consumers).
|
||||||
|
fn writeProperties(jw: anytype, properties: []const Property) !void {
|
||||||
|
try jw.beginObject();
|
||||||
|
for (properties, 0..) |prop, i| {
|
||||||
|
// Skip keys already written by an earlier occurrence.
|
||||||
|
var already_written = false;
|
||||||
|
for (properties[0..i]) |prev| {
|
||||||
|
if (std.mem.eql(u8, prev.key, prop.key)) {
|
||||||
|
already_written = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (already_written) continue;
|
||||||
|
|
||||||
|
// Count total occurrences to decide string vs array.
|
||||||
|
var count: usize = 0;
|
||||||
|
for (properties) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, prop.key)) count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try jw.objectField(prop.key);
|
||||||
|
if (count == 1) {
|
||||||
|
try jw.write(prop.value);
|
||||||
|
} else {
|
||||||
|
try jw.beginArray();
|
||||||
|
for (properties) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, prop.key)) {
|
||||||
|
try jw.write(p.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try jw.endArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try jw.endObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract all structured data from the page.
|
||||||
|
pub fn collectStructuredData(
|
||||||
|
root: *Node,
|
||||||
|
arena: Allocator,
|
||||||
|
page: *Page,
|
||||||
|
) !StructuredData {
|
||||||
|
var json_ld: std.ArrayList([]const u8) = .empty;
|
||||||
|
var open_graph: std.ArrayList(Property) = .empty;
|
||||||
|
var twitter_card: std.ArrayList(Property) = .empty;
|
||||||
|
var meta: std.ArrayList(Property) = .empty;
|
||||||
|
var links: std.ArrayList(Property) = .empty;
|
||||||
|
var alternate: std.ArrayList(AlternateLink) = .empty;
|
||||||
|
|
||||||
|
// Extract language from the root <html> element.
|
||||||
|
if (root.is(Element)) |root_el| {
|
||||||
|
if (root_el.getAttributeSafe(comptime .wrap("lang"))) |lang| {
|
||||||
|
try meta.append(arena, .{ .key = "language", .value = lang });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Root is document — check documentElement.
|
||||||
|
var children = root.childrenIterator();
|
||||||
|
while (children.next()) |child| {
|
||||||
|
const el = child.is(Element) orelse continue;
|
||||||
|
if (el.getTag() == .html) {
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("lang"))) |lang| {
|
||||||
|
try meta.append(arena, .{ .key = "language", .value = lang });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tw = TreeWalker.Full.init(root, .{});
|
||||||
|
while (tw.next()) |node| {
|
||||||
|
const el = node.is(Element) orelse continue;
|
||||||
|
|
||||||
|
switch (el.getTag()) {
|
||||||
|
.script => {
|
||||||
|
try collectJsonLd(el, arena, &json_ld);
|
||||||
|
tw.skipChildren();
|
||||||
|
},
|
||||||
|
.meta => collectMeta(el, &open_graph, &twitter_card, &meta, arena) catch {},
|
||||||
|
.title => try collectTitle(node, arena, &meta),
|
||||||
|
.link => try collectLink(el, arena, page, &links, &alternate),
|
||||||
|
// Skip body subtree for non-JSON-LD — all other metadata is in <head>.
|
||||||
|
// JSON-LD can appear in <body> so we don't skip the whole body.
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.json_ld = json_ld.items,
|
||||||
|
.open_graph = open_graph.items,
|
||||||
|
.twitter_card = twitter_card.items,
|
||||||
|
.meta = meta.items,
|
||||||
|
.links = links.items,
|
||||||
|
.alternate = alternate.items,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectJsonLd(
|
||||||
|
el: *Element,
|
||||||
|
arena: Allocator,
|
||||||
|
json_ld: *std.ArrayList([]const u8),
|
||||||
|
) !void {
|
||||||
|
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
||||||
|
if (!std.ascii.eqlIgnoreCase(type_attr, "application/ld+json")) return;
|
||||||
|
|
||||||
|
var buf: std.Io.Writer.Allocating = .init(arena);
|
||||||
|
try el.asNode().getTextContent(&buf.writer);
|
||||||
|
const text = buf.written();
|
||||||
|
if (text.len > 0) {
|
||||||
|
try json_ld.append(arena, std.mem.trim(u8, text, &std.ascii.whitespace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectMeta(
|
||||||
|
el: *Element,
|
||||||
|
open_graph: *std.ArrayList(Property),
|
||||||
|
twitter_card: *std.ArrayList(Property),
|
||||||
|
meta: *std.ArrayList(Property),
|
||||||
|
arena: Allocator,
|
||||||
|
) !void {
|
||||||
|
// charset: <meta charset="..."> (no content attribute needed).
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("charset"))) |charset| {
|
||||||
|
try meta.append(arena, .{ .key = "charset", .value = charset });
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = el.getAttributeSafe(comptime .wrap("content")) orelse return;
|
||||||
|
|
||||||
|
// Open Graph: <meta property="og:...">
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("property"))) |property| {
|
||||||
|
if (std.mem.startsWith(u8, property, "og:")) {
|
||||||
|
try open_graph.append(arena, .{ .key = property[3..], .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Article, profile, etc. are OG sub-namespaces.
|
||||||
|
if (std.mem.startsWith(u8, property, "article:") or
|
||||||
|
std.mem.startsWith(u8, property, "profile:") or
|
||||||
|
std.mem.startsWith(u8, property, "book:") or
|
||||||
|
std.mem.startsWith(u8, property, "music:") or
|
||||||
|
std.mem.startsWith(u8, property, "video:"))
|
||||||
|
{
|
||||||
|
try open_graph.append(arena, .{ .key = property, .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitter Cards: <meta name="twitter:...">
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("name"))) |name| {
|
||||||
|
if (std.mem.startsWith(u8, name, "twitter:")) {
|
||||||
|
try twitter_card.append(arena, .{ .key = name[8..], .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard meta tags by name.
|
||||||
|
const known_names = [_][]const u8{
|
||||||
|
"description", "author", "keywords", "robots",
|
||||||
|
"viewport", "generator", "theme-color",
|
||||||
|
};
|
||||||
|
for (known_names) |known| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, known)) {
|
||||||
|
try meta.append(arena, .{ .key = known, .value = content });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// http-equiv (e.g. Content-Type, refresh)
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("http-equiv"))) |http_equiv| {
|
||||||
|
try meta.append(arena, .{ .key = http_equiv, .value = content });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectTitle(
|
||||||
|
node: *Node,
|
||||||
|
arena: Allocator,
|
||||||
|
meta: *std.ArrayList(Property),
|
||||||
|
) !void {
|
||||||
|
var buf: std.Io.Writer.Allocating = .init(arena);
|
||||||
|
try node.getTextContent(&buf.writer);
|
||||||
|
const text = std.mem.trim(u8, buf.written(), &std.ascii.whitespace);
|
||||||
|
if (text.len > 0) {
|
||||||
|
try meta.append(arena, .{ .key = "title", .value = text });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collectLink(
|
||||||
|
el: *Element,
|
||||||
|
arena: Allocator,
|
||||||
|
page: *Page,
|
||||||
|
links: *std.ArrayList(Property),
|
||||||
|
alternate: *std.ArrayList(AlternateLink),
|
||||||
|
) !void {
|
||||||
|
const rel = el.getAttributeSafe(comptime .wrap("rel")) orelse return;
|
||||||
|
const raw_href = el.getAttributeSafe(comptime .wrap("href")) orelse return;
|
||||||
|
const href = URL.resolve(arena, page.base(), raw_href, .{ .encode = true }) catch raw_href;
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(rel, "alternate")) {
|
||||||
|
try alternate.append(arena, .{
|
||||||
|
.href = href,
|
||||||
|
.hreflang = el.getAttributeSafe(comptime .wrap("hreflang")),
|
||||||
|
.type = el.getAttributeSafe(comptime .wrap("type")),
|
||||||
|
.title = el.getAttributeSafe(comptime .wrap("title")),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relevant_rels = [_][]const u8{
|
||||||
|
"canonical", "icon", "manifest", "shortcut icon",
|
||||||
|
"apple-touch-icon", "search", "author", "license",
|
||||||
|
"dns-prefetch", "preconnect",
|
||||||
|
};
|
||||||
|
for (relevant_rels) |known| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(rel, known)) {
|
||||||
|
try links.append(arena, .{ .key = known, .value = href });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
const testing = @import("../testing.zig");
|
||||||
|
|
||||||
|
fn testStructuredData(html: []const u8) !StructuredData {
|
||||||
|
const page = try testing.test_session.createPage();
|
||||||
|
defer testing.test_session.removePage();
|
||||||
|
|
||||||
|
const doc = page.window._document;
|
||||||
|
const div = try doc.createElement("div", null, page);
|
||||||
|
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||||
|
|
||||||
|
return collectStructuredData(div.asNode(), page.call_arena, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findProperty(props: []const Property, key: []const u8) ?[]const u8 {
|
||||||
|
for (props) |p| {
|
||||||
|
if (std.mem.eql(u8, p.key, key)) return p.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: json-ld" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<script type="application/ld+json">
|
||||||
|
\\{"@context":"https://schema.org","@type":"Article","headline":"Test"}
|
||||||
|
\\</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, data.json_ld.len);
|
||||||
|
try testing.expect(std.mem.indexOf(u8, data.json_ld[0], "Article") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: multiple json-ld" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<script type="application/ld+json">{"@type":"Organization"}</script>
|
||||||
|
\\<script type="application/ld+json">{"@type":"BreadcrumbList"}</script>
|
||||||
|
\\<script type="text/javascript">var x = 1;</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, data.json_ld.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: open graph" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta property="og:title" content="My Page">
|
||||||
|
\\<meta property="og:description" content="A description">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img.jpg">
|
||||||
|
\\<meta property="og:url" content="https://example.com">
|
||||||
|
\\<meta property="og:type" content="article">
|
||||||
|
\\<meta property="article:published_time" content="2026-03-10">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(6, data.open_graph.len);
|
||||||
|
try testing.expectEqual("My Page", findProperty(data.open_graph, "title").?);
|
||||||
|
try testing.expectEqual("article", findProperty(data.open_graph, "type").?);
|
||||||
|
try testing.expectEqual("2026-03-10", findProperty(data.open_graph, "article:published_time").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: open graph duplicate keys" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta property="og:title" content="My Page">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img1.jpg">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img2.jpg">
|
||||||
|
\\<meta property="og:image" content="https://example.com/img3.jpg">
|
||||||
|
);
|
||||||
|
// Duplicate keys are preserved as separate Property entries.
|
||||||
|
try testing.expectEqual(4, data.open_graph.len);
|
||||||
|
|
||||||
|
// Verify serialization groups duplicates into arrays.
|
||||||
|
const json = try std.json.Stringify.valueAlloc(testing.allocator, data, .{});
|
||||||
|
defer testing.allocator.free(json);
|
||||||
|
|
||||||
|
const parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, json, .{});
|
||||||
|
defer parsed.deinit();
|
||||||
|
const og = parsed.value.object.get("openGraph").?.object;
|
||||||
|
// "title" appears once → string.
|
||||||
|
switch (og.get("title").?) {
|
||||||
|
.string => {},
|
||||||
|
else => return error.TestUnexpectedResult,
|
||||||
|
}
|
||||||
|
// "image" appears 3 times → array.
|
||||||
|
switch (og.get("image").?) {
|
||||||
|
.array => |arr| try testing.expectEqual(3, arr.items.len),
|
||||||
|
else => return error.TestUnexpectedResult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: twitter card" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta name="twitter:card" content="summary_large_image">
|
||||||
|
\\<meta name="twitter:site" content="@example">
|
||||||
|
\\<meta name="twitter:title" content="My Page">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(3, data.twitter_card.len);
|
||||||
|
try testing.expectEqual("summary_large_image", findProperty(data.twitter_card, "card").?);
|
||||||
|
try testing.expectEqual("@example", findProperty(data.twitter_card, "site").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: meta tags" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<title>Page Title</title>
|
||||||
|
\\<meta name="description" content="A test page">
|
||||||
|
\\<meta name="author" content="Test Author">
|
||||||
|
\\<meta name="keywords" content="test, example">
|
||||||
|
\\<meta name="robots" content="index, follow">
|
||||||
|
);
|
||||||
|
try testing.expectEqual("Page Title", findProperty(data.meta, "title").?);
|
||||||
|
try testing.expectEqual("A test page", findProperty(data.meta, "description").?);
|
||||||
|
try testing.expectEqual("Test Author", findProperty(data.meta, "author").?);
|
||||||
|
try testing.expectEqual("test, example", findProperty(data.meta, "keywords").?);
|
||||||
|
try testing.expectEqual("index, follow", findProperty(data.meta, "robots").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: link elements" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<link rel="canonical" href="https://example.com/page">
|
||||||
|
\\<link rel="icon" href="/favicon.ico">
|
||||||
|
\\<link rel="manifest" href="/manifest.json">
|
||||||
|
\\<link rel="stylesheet" href="/style.css">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(3, data.links.len);
|
||||||
|
try testing.expectEqual("https://example.com/page", findProperty(data.links, "canonical").?);
|
||||||
|
// stylesheet should be filtered out
|
||||||
|
try testing.expectEqual(null, findProperty(data.links, "stylesheet"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: alternate links" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<link rel="alternate" href="https://example.com/fr" hreflang="fr" title="French">
|
||||||
|
\\<link rel="alternate" href="https://example.com/de" hreflang="de">
|
||||||
|
);
|
||||||
|
try testing.expectEqual(2, data.alternate.len);
|
||||||
|
try testing.expectEqual("fr", data.alternate[0].hreflang.?);
|
||||||
|
try testing.expectEqual("French", data.alternate[0].title.?);
|
||||||
|
try testing.expectEqual("de", data.alternate[1].hreflang.?);
|
||||||
|
try testing.expectEqual(null, data.alternate[1].title);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: non-metadata elements ignored" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<div>Just text</div>
|
||||||
|
\\<p>More text</p>
|
||||||
|
\\<a href="/link">Link</a>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(0, data.json_ld.len);
|
||||||
|
try testing.expectEqual(0, data.open_graph.len);
|
||||||
|
try testing.expectEqual(0, data.twitter_card.len);
|
||||||
|
try testing.expectEqual(0, data.meta.len);
|
||||||
|
try testing.expectEqual(0, data.links.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: charset and http-equiv" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<meta charset="utf-8">
|
||||||
|
\\<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
);
|
||||||
|
try testing.expectEqual("utf-8", findProperty(data.meta, "charset").?);
|
||||||
|
try testing.expectEqual("text/html; charset=utf-8", findProperty(data.meta, "Content-Type").?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "structured_data: mixed content" {
|
||||||
|
const data = try testStructuredData(
|
||||||
|
\\<title>My Site</title>
|
||||||
|
\\<meta property="og:title" content="OG Title">
|
||||||
|
\\<meta name="twitter:card" content="summary">
|
||||||
|
\\<meta name="description" content="A page">
|
||||||
|
\\<link rel="canonical" href="https://example.com">
|
||||||
|
\\<script type="application/ld+json">{"@type":"WebSite"}</script>
|
||||||
|
);
|
||||||
|
try testing.expectEqual(1, data.json_ld.len);
|
||||||
|
try testing.expectEqual(1, data.open_graph.len);
|
||||||
|
try testing.expectEqual(1, data.twitter_card.len);
|
||||||
|
try testing.expectEqual("My Site", findProperty(data.meta, "title").?);
|
||||||
|
try testing.expectEqual("A page", findProperty(data.meta, "description").?);
|
||||||
|
try testing.expectEqual(1, data.links.len);
|
||||||
|
}
|
||||||
@@ -98,6 +98,64 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=mime_parsing>
|
||||||
|
// MIME types are lowercased
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "TEXT/HTML" });
|
||||||
|
testing.expectEqual("text/html", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "Application/JSON" });
|
||||||
|
testing.expectEqual("application/json", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIME with parameters - lowercased
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/html; charset=UTF-8" });
|
||||||
|
testing.expectEqual("text/html; charset=utf-8", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any ASCII string is accepted and lowercased (no MIME structure validation)
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "invalid" });
|
||||||
|
testing.expectEqual("invalid", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "/" });
|
||||||
|
testing.expectEqual("/", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-ASCII characters cause empty string (chars outside U+0020-U+007E)
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "ý/x" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/plàin" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control characters cause empty string
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "text/html\x00" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty type stays empty
|
||||||
|
{
|
||||||
|
const blob = new Blob([]);
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const blob = new Blob([], { type: "" });
|
||||||
|
testing.expectEqual("", blob.type);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=slice>
|
<script id=slice>
|
||||||
{
|
{
|
||||||
const parts = ["la", "symphonie", "des", "éclairs"];
|
const parts = ["la", "symphonie", "des", "éclairs"];
|
||||||
|
|||||||
@@ -275,3 +275,109 @@
|
|||||||
testing.expectEqual('red', div.style.getPropertyValue('color'));
|
testing.expectEqual('red', div.style.getPropertyValue('color'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_zero_to_0px">
|
||||||
|
{
|
||||||
|
// Per CSSOM spec, unitless zero in length properties should serialize as "0px"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.width = '0';
|
||||||
|
testing.expectEqual('0px', div.style.width);
|
||||||
|
|
||||||
|
div.style.margin = '0';
|
||||||
|
testing.expectEqual('0px', div.style.margin);
|
||||||
|
|
||||||
|
div.style.padding = '0';
|
||||||
|
testing.expectEqual('0px', div.style.padding);
|
||||||
|
|
||||||
|
div.style.top = '0';
|
||||||
|
testing.expectEqual('0px', div.style.top);
|
||||||
|
|
||||||
|
// Non-length properties should not be affected
|
||||||
|
div.style.opacity = '0';
|
||||||
|
testing.expectEqual('0', div.style.opacity);
|
||||||
|
|
||||||
|
div.style.zIndex = '0';
|
||||||
|
testing.expectEqual('0', div.style.zIndex);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_first_baseline">
|
||||||
|
{
|
||||||
|
// "first baseline" should serialize canonically as "baseline"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.alignItems = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.alignItems);
|
||||||
|
|
||||||
|
div.style.alignContent = 'first baseline';
|
||||||
|
testing.expectEqual('baseline', div.style.alignContent);
|
||||||
|
|
||||||
|
// "last baseline" should remain unchanged
|
||||||
|
div.style.alignItems = 'last baseline';
|
||||||
|
testing.expectEqual('last baseline', div.style.alignItems);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_duplicate_values">
|
||||||
|
{
|
||||||
|
// For 2-value shorthand properties, "X X" should collapse to "X"
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
div.style.placeContent = 'center center';
|
||||||
|
testing.expectEqual('center', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.placeContent = 'start start';
|
||||||
|
testing.expectEqual('start', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.gap = '10px 10px';
|
||||||
|
testing.expectEqual('10px', div.style.gap);
|
||||||
|
|
||||||
|
// Different values should not collapse
|
||||||
|
div.style.placeContent = 'center start';
|
||||||
|
testing.expectEqual('center start', div.style.placeContent);
|
||||||
|
|
||||||
|
div.style.gap = '10px 20px';
|
||||||
|
testing.expectEqual('10px 20px', div.style.gap);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="CSSStyleDeclaration_normalize_anchor_size">
|
||||||
|
{
|
||||||
|
// anchor-size() should serialize with dashed ident (anchor name) before size keyword
|
||||||
|
const div = document.createElement('div');
|
||||||
|
|
||||||
|
// Already canonical order - should stay the same
|
||||||
|
div.style.width = 'anchor-size(--foo width)';
|
||||||
|
testing.expectEqual('anchor-size(--foo width)', div.style.width);
|
||||||
|
|
||||||
|
// Non-canonical order - should be reordered
|
||||||
|
div.style.width = 'anchor-size(width --foo)';
|
||||||
|
testing.expectEqual('anchor-size(--foo width)', div.style.width);
|
||||||
|
|
||||||
|
// With fallback value
|
||||||
|
div.style.width = 'anchor-size(height --bar, 100px)';
|
||||||
|
testing.expectEqual('anchor-size(--bar height, 100px)', div.style.width);
|
||||||
|
|
||||||
|
// Different size keywords
|
||||||
|
div.style.width = 'anchor-size(block --baz)';
|
||||||
|
testing.expectEqual('anchor-size(--baz block)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(inline --qux)';
|
||||||
|
testing.expectEqual('anchor-size(--qux inline)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(self-block --test)';
|
||||||
|
testing.expectEqual('anchor-size(--test self-block)', div.style.width);
|
||||||
|
|
||||||
|
div.style.width = 'anchor-size(self-inline --test)';
|
||||||
|
testing.expectEqual('anchor-size(--test self-inline)', div.style.width);
|
||||||
|
|
||||||
|
// Without anchor name (implicit default anchor)
|
||||||
|
div.style.width = 'anchor-size(width)';
|
||||||
|
testing.expectEqual('anchor-size(width)', div.style.width);
|
||||||
|
|
||||||
|
// Nested anchor-size in fallback
|
||||||
|
div.style.width = 'anchor-size(width --foo, anchor-size(height --bar))';
|
||||||
|
testing.expectEqual('anchor-size(--foo width, anchor-size(--bar height))', div.style.width);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -397,3 +397,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=getElementsByTagName-xml>
|
||||||
|
{
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString('<layout><row><col>A</col><col>B</col></row></layout>', 'text/xml');
|
||||||
|
|
||||||
|
// Test getElementsByTagName on document
|
||||||
|
const rows = doc.getElementsByTagName('row');
|
||||||
|
testing.expectEqual(1, rows.length);
|
||||||
|
|
||||||
|
// Test getElementsByTagName on element
|
||||||
|
const row = rows[0];
|
||||||
|
const cols = row.getElementsByTagName('col');
|
||||||
|
testing.expectEqual(2, cols.length);
|
||||||
|
testing.expectEqual('A', cols[0].textContent);
|
||||||
|
testing.expectEqual('B', cols[1].textContent);
|
||||||
|
|
||||||
|
// Test getElementsByTagName('*') on element
|
||||||
|
const allElements = row.getElementsByTagName('*');
|
||||||
|
testing.expectEqual(2, allElements.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -137,3 +137,79 @@
|
|||||||
testing.expectEqual('PROPFIND', req.method);
|
testing.expectEqual('PROPFIND', req.method);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=body_methods>
|
||||||
|
testing.async(async () => {
|
||||||
|
const req = new Request('https://example.com/api', {
|
||||||
|
method: 'POST',
|
||||||
|
body: 'Hello, World!',
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await req.text();
|
||||||
|
testing.expectEqual('Hello, World!', text);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const req = new Request('https://example.com/api', {
|
||||||
|
method: 'POST',
|
||||||
|
body: '{"name": "test"}',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await req.json();
|
||||||
|
testing.expectEqual('test', json.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const req = new Request('https://example.com/api', {
|
||||||
|
method: 'POST',
|
||||||
|
body: 'binary data',
|
||||||
|
headers: { 'Content-Type': 'application/octet-stream' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const buffer = await req.arrayBuffer();
|
||||||
|
testing.expectEqual(true, buffer instanceof ArrayBuffer);
|
||||||
|
testing.expectEqual(11, buffer.byteLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const req = new Request('https://example.com/api', {
|
||||||
|
method: 'POST',
|
||||||
|
body: 'blob content',
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = await req.blob();
|
||||||
|
testing.expectEqual(true, blob instanceof Blob);
|
||||||
|
testing.expectEqual(12, blob.size);
|
||||||
|
testing.expectEqual('text/plain', blob.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const req = new Request('https://example.com/api', {
|
||||||
|
method: 'POST',
|
||||||
|
body: 'bytes'
|
||||||
|
});
|
||||||
|
|
||||||
|
const bytes = await req.bytes();
|
||||||
|
testing.expectEqual(true, bytes instanceof Uint8Array);
|
||||||
|
testing.expectEqual(5, bytes.length);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=clone>
|
||||||
|
{
|
||||||
|
const req1 = new Request('https://example.com/api', {
|
||||||
|
method: 'POST',
|
||||||
|
body: 'test body',
|
||||||
|
headers: { 'X-Custom': 'value' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const req2 = req1.clone();
|
||||||
|
|
||||||
|
testing.expectEqual(req1.url, req2.url);
|
||||||
|
testing.expectEqual(req1.method, req2.method);
|
||||||
|
testing.expectEqual('value', req2.headers.get('X-Custom'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -2,51 +2,113 @@
|
|||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
<script id=response>
|
<script id=response>
|
||||||
// let response = new Response("Hello, World!");
|
{
|
||||||
// testing.expectEqual(200, response.status);
|
let response = new Response("Hello, World!");
|
||||||
// testing.expectEqual("", response.statusText);
|
testing.expectEqual(200, response.status);
|
||||||
// testing.expectEqual(true, response.ok);
|
testing.expectEqual("", response.statusText);
|
||||||
// testing.expectEqual("", response.url);
|
testing.expectEqual(true, response.ok);
|
||||||
// testing.expectEqual(false, response.redirected);
|
testing.expectEqual("", response.url);
|
||||||
|
testing.expectEqual(false, response.redirected);
|
||||||
|
}
|
||||||
|
|
||||||
let response2 = new Response("Error occurred", {
|
{
|
||||||
status: 404,
|
let response2 = new Response("Error occurred", {
|
||||||
statusText: "Not Found",
|
status: 404,
|
||||||
headers: {
|
statusText: "Not Found",
|
||||||
"Content-Type": "text/plain",
|
headers: {
|
||||||
"X-Custom": "test-value",
|
"Content-Type": "text/plain",
|
||||||
"Cache-Control": "no-cache"
|
"X-Custom": "test-value",
|
||||||
}
|
"Cache-Control": "no-cache"
|
||||||
});
|
}
|
||||||
testing.expectEqual(true, true);
|
|
||||||
// testing.expectEqual(404, response2.status);
|
|
||||||
// testing.expectEqual("Not Found", response2.statusText);
|
|
||||||
// testing.expectEqual(false, response2.ok);
|
|
||||||
// testing.expectEqual("text/plain", response2.headers);
|
|
||||||
// testing.expectEqual("test-value", response2.headers.get("X-Custom"));
|
|
||||||
testing.expectEqual("no-cache", response2.headers.get("cache-control"));
|
|
||||||
|
|
||||||
// let response3 = new Response("Created", { status: 201, statusText: "Created" });
|
|
||||||
// testing.expectEqual("basic", response3.type);
|
|
||||||
// testing.expectEqual(201, response3.status);
|
|
||||||
// testing.expectEqual("Created", response3.statusText);
|
|
||||||
// testing.expectEqual(true, response3.ok);
|
|
||||||
|
|
||||||
// let nullResponse = new Response(null);
|
|
||||||
// testing.expectEqual(200, nullResponse.status);
|
|
||||||
// testing.expectEqual("", nullResponse.statusText);
|
|
||||||
|
|
||||||
// let emptyResponse = new Response("");
|
|
||||||
// testing.expectEqual(200, emptyResponse.status);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- <script id=json>
|
|
||||||
testing.async(async () => {
|
|
||||||
const json = await new Promise((resolve) => {
|
|
||||||
let response = new Response('[]');
|
|
||||||
response.json().then(resolve)
|
|
||||||
});
|
});
|
||||||
testing.expectEqual([], json);
|
testing.expectEqual(404, response2.status);
|
||||||
|
testing.expectEqual("Not Found", response2.statusText);
|
||||||
|
testing.expectEqual(false, response2.ok);
|
||||||
|
testing.expectEqual("test-value", response2.headers.get("X-Custom"));
|
||||||
|
testing.expectEqual("no-cache", response2.headers.get("cache-control"));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let response3 = new Response("Created", { status: 201, statusText: "Created" });
|
||||||
|
testing.expectEqual("basic", response3.type);
|
||||||
|
testing.expectEqual(201, response3.status);
|
||||||
|
testing.expectEqual("Created", response3.statusText);
|
||||||
|
testing.expectEqual(true, response3.ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let nullResponse = new Response(null);
|
||||||
|
testing.expectEqual(200, nullResponse.status);
|
||||||
|
testing.expectEqual("", nullResponse.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let emptyResponse = new Response("");
|
||||||
|
testing.expectEqual(200, emptyResponse.status);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=body_methods>
|
||||||
|
testing.async(async () => {
|
||||||
|
const response = new Response('Hello, World!');
|
||||||
|
const text = await response.text();
|
||||||
|
testing.expectEqual('Hello, World!', text);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const response = new Response('{"name": "test"}');
|
||||||
|
const json = await response.json();
|
||||||
|
testing.expectEqual('test', json.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const response = new Response('binary data');
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
testing.expectEqual(true, buffer instanceof ArrayBuffer);
|
||||||
|
testing.expectEqual(11, buffer.byteLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const response = new Response('blob content', {
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
const blob = await response.blob();
|
||||||
|
testing.expectEqual(true, blob instanceof Blob);
|
||||||
|
testing.expectEqual(12, blob.size);
|
||||||
|
testing.expectEqual('text/plain', blob.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const response = new Response('bytes');
|
||||||
|
const bytes = await response.bytes();
|
||||||
|
testing.expectEqual(true, bytes instanceof Uint8Array);
|
||||||
|
testing.expectEqual(5, bytes.length);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=clone>
|
||||||
|
{
|
||||||
|
const response1 = new Response('test body', {
|
||||||
|
status: 201,
|
||||||
|
statusText: 'Created',
|
||||||
|
headers: { 'X-Custom': 'value' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const response2 = response1.clone();
|
||||||
|
|
||||||
|
testing.expectEqual(response1.status, response2.status);
|
||||||
|
testing.expectEqual(response1.statusText, response2.statusText);
|
||||||
|
testing.expectEqual('value', response2.headers.get('X-Custom'));
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const response1 = new Response('cloned body');
|
||||||
|
const response2 = response1.clone();
|
||||||
|
|
||||||
|
const text1 = await response1.text();
|
||||||
|
const text2 = await response2.text();
|
||||||
|
|
||||||
|
testing.expectEqual('cloned body', text1);
|
||||||
|
testing.expectEqual('cloned body', text2);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
-->
|
|
||||||
|
|||||||
@@ -218,6 +218,106 @@
|
|||||||
testing.expectEqual('', url.password);
|
testing.expectEqual('', url.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.username = 'newuser';
|
||||||
|
testing.expectEqual('newuser', url.username);
|
||||||
|
testing.expectEqual('https://newuser@example.com/path', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://olduser@example.com/path');
|
||||||
|
url.username = 'newuser';
|
||||||
|
testing.expectEqual('newuser', url.username);
|
||||||
|
testing.expectEqual('https://newuser@example.com/path', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://olduser:pass@example.com/path');
|
||||||
|
url.username = 'newuser';
|
||||||
|
testing.expectEqual('newuser', url.username);
|
||||||
|
testing.expectEqual('pass', url.password);
|
||||||
|
testing.expectEqual('https://newuser:pass@example.com/path', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://user@example.com/path');
|
||||||
|
url.password = 'secret';
|
||||||
|
testing.expectEqual('user', url.username);
|
||||||
|
testing.expectEqual('secret', url.password);
|
||||||
|
testing.expectEqual('https://user:secret@example.com/path', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://user:oldpass@example.com/path');
|
||||||
|
url.password = 'newpass';
|
||||||
|
testing.expectEqual('user', url.username);
|
||||||
|
testing.expectEqual('newpass', url.password);
|
||||||
|
testing.expectEqual('https://user:newpass@example.com/path', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://user:pass@example.com/path');
|
||||||
|
url.username = '';
|
||||||
|
url.password = '';
|
||||||
|
testing.expectEqual('', url.username);
|
||||||
|
testing.expectEqual('', url.password);
|
||||||
|
testing.expectEqual('https://example.com/path', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.username = 'user@domain';
|
||||||
|
testing.expectEqual('user%40domain', url.username);
|
||||||
|
testing.expectEqual('https://user%40domain@example.com/path', url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.username = 'user:name';
|
||||||
|
testing.expectEqual('user%3Aname', url.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.password = 'pass@word';
|
||||||
|
testing.expectEqual('pass%40word', url.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.password = 'pass:word';
|
||||||
|
testing.expectEqual('pass%3Aword', url.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.username = 'user/name';
|
||||||
|
testing.expectEqual('user%2Fname', url.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com/path');
|
||||||
|
url.password = 'pass?word';
|
||||||
|
testing.expectEqual('pass%3Fword', url.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://user%40domain:pass%3Aword@example.com/path');
|
||||||
|
testing.expectEqual('user%40domain', url.username);
|
||||||
|
testing.expectEqual('pass%3Aword', url.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = new URL('https://example.com:8080/path?a=b#hash');
|
||||||
|
url.username = 'user';
|
||||||
|
url.password = 'pass';
|
||||||
|
testing.expectEqual('https://user:pass@example.com:8080/path?a=b#hash', url.href);
|
||||||
|
testing.expectEqual('8080', url.port);
|
||||||
|
testing.expectEqual('?a=b', url.search);
|
||||||
|
testing.expectEqual('#hash', url.hash);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const url = new URL('http://user:pass@example.com:8080/path?query=1#hash');
|
const url = new URL('http://user:pass@example.com:8080/path?query=1#hash');
|
||||||
testing.expectEqual('http:', url.protocol);
|
testing.expectEqual('http:', url.protocol);
|
||||||
@@ -437,9 +537,9 @@
|
|||||||
{
|
{
|
||||||
const url = new URL('https://example.com:8080/path');
|
const url = new URL('https://example.com:8080/path');
|
||||||
url.host = 'newhost.com';
|
url.host = 'newhost.com';
|
||||||
testing.expectEqual('https://newhost.com/path', url.href);
|
testing.expectEqual('https://newhost.com:8080/path', url.href);
|
||||||
testing.expectEqual('newhost.com', url.hostname);
|
testing.expectEqual('newhost.com', url.hostname);
|
||||||
testing.expectEqual('', url.port);
|
testing.expectEqual('8080', url.port);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,13 +76,11 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch abort event
|
// Dispatch abort event
|
||||||
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
|
const target = self.asEventTarget();
|
||||||
try page._event_manager.dispatchDirect(
|
if (page._event_manager.hasDirectListeners(target, "abort", self._on_abort)) {
|
||||||
self.asEventTarget(),
|
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
|
||||||
event,
|
try page._event_manager.dispatchDirect(target, event, self._on_abort, .{ .context = "abort signal" });
|
||||||
self._on_abort,
|
}
|
||||||
.{ .context = "abort signal" },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static method to create an already-aborted signal
|
// Static method to create an already-aborted signal
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const Writer = std.Io.Writer;
|
|||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
const Mime = @import("../Mime.zig");
|
||||||
|
|
||||||
/// https://w3c.github.io/FileAPI/#blob-section
|
/// https://w3c.github.io/FileAPI/#blob-section
|
||||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
/// https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
||||||
@@ -50,21 +51,50 @@ const InitOptions = struct {
|
|||||||
endings: []const u8 = "transparent",
|
endings: []const u8 = "transparent",
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Creates a new Blob.
|
/// Creates a new Blob (JS constructor).
|
||||||
pub fn init(
|
pub fn init(
|
||||||
maybe_blob_parts: ?[]const []const u8,
|
maybe_blob_parts: ?[]const []const u8,
|
||||||
maybe_options: ?InitOptions,
|
maybe_options: ?InitOptions,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
|
) !*Blob {
|
||||||
|
return initWithMimeValidation(maybe_blob_parts, maybe_options, false, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Blob with optional MIME validation.
|
||||||
|
/// When validate_mime is true, uses full MIME parsing (for Response/Request).
|
||||||
|
/// When false, uses simple ASCII validation per FileAPI spec (for Blob constructor).
|
||||||
|
pub fn initWithMimeValidation(
|
||||||
|
maybe_blob_parts: ?[]const []const u8,
|
||||||
|
maybe_options: ?InitOptions,
|
||||||
|
validate_mime: bool,
|
||||||
|
page: *Page,
|
||||||
) !*Blob {
|
) !*Blob {
|
||||||
const options: InitOptions = maybe_options orelse .{};
|
const options: InitOptions = maybe_options orelse .{};
|
||||||
// Setup MIME; This can be any string according to my observations.
|
|
||||||
const mime: []const u8 = blk: {
|
const mime: []const u8 = blk: {
|
||||||
const t = options.type;
|
const t = options.type;
|
||||||
if (t.len == 0) {
|
if (t.len == 0) {
|
||||||
break :blk "";
|
break :blk "";
|
||||||
}
|
}
|
||||||
|
|
||||||
break :blk try page.arena.dupe(u8, t);
|
const buf = try page.arena.dupe(u8, t);
|
||||||
|
|
||||||
|
if (validate_mime) {
|
||||||
|
// Full MIME parsing per MIME sniff spec (for Content-Type headers)
|
||||||
|
_ = Mime.parse(buf) catch break :blk "";
|
||||||
|
} else {
|
||||||
|
// Simple validation per FileAPI spec (for Blob constructor):
|
||||||
|
// - If any char is outside U+0020-U+007E, return empty string
|
||||||
|
// - Otherwise lowercase
|
||||||
|
for (t) |c| {
|
||||||
|
if (c < 0x20 or c > 0x7E) {
|
||||||
|
break :blk "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = std.ascii.lowerString(buf, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk buf;
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = blk: {
|
const data = blk: {
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
|
|||||||
.screen => writer.writeAll("<Screen>"),
|
.screen => writer.writeAll("<Screen>"),
|
||||||
.screen_orientation => writer.writeAll("<ScreenOrientation>"),
|
.screen_orientation => writer.writeAll("<ScreenOrientation>"),
|
||||||
.visual_viewport => writer.writeAll("<VisualViewport>"),
|
.visual_viewport => writer.writeAll("<VisualViewport>"),
|
||||||
|
.file_reader => writer.writeAll("<FileReader>"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,13 +79,11 @@ fn goInner(delta: i32, page: *Page) !void {
|
|||||||
|
|
||||||
if (entry._url) |url| {
|
if (entry._url) |url| {
|
||||||
if (try page.isSameOrigin(url)) {
|
if (try page.isSameOrigin(url)) {
|
||||||
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent();
|
const target = page.window.asEventTarget();
|
||||||
try page._event_manager.dispatchDirect(
|
if (page._event_manager.hasDirectListeners(target, "popstate", page.window._on_popstate)) {
|
||||||
page.window.asEventTarget(),
|
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent();
|
||||||
event,
|
try page._event_manager.dispatchDirect(target, event, page.window._on_popstate, .{ .context = "Pop State" });
|
||||||
page.window._on_popstate,
|
}
|
||||||
.{ .context = "Pop State" },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,23 +122,21 @@ const PostMessageCallback = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
|
const target = self.port.asEventTarget();
|
||||||
.data = self.message,
|
if (page._event_manager.hasDirectListeners(target, "message", self.port._on_message)) {
|
||||||
.origin = "",
|
const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||||
.source = null,
|
.data = self.message,
|
||||||
}, page) catch |err| {
|
.origin = "",
|
||||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
.source = null,
|
||||||
return null;
|
}, page) catch |err| {
|
||||||
}).asEvent();
|
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||||
|
return null;
|
||||||
|
}).asEvent();
|
||||||
|
|
||||||
page._event_manager.dispatchDirect(
|
page._event_manager.dispatchDirect(target, event, self.port._on_message, .{ .context = "MessagePort message" }) catch |err| {
|
||||||
self.port.asEventTarget(),
|
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||||
event,
|
};
|
||||||
self.port._on_message,
|
}
|
||||||
.{ .context = "MessagePort message" },
|
|
||||||
) catch |err| {
|
|
||||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
|
||||||
};
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,10 +66,20 @@ pub fn getUsername(self: *const URL) []const u8 {
|
|||||||
return U.getUsername(self._raw);
|
return U.getUsername(self._raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setUsername(self: *URL, value: []const u8) !void {
|
||||||
|
const allocator = self._arena orelse return error.NoAllocator;
|
||||||
|
self._raw = try U.setUsername(self._raw, value, allocator);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getPassword(self: *const URL) []const u8 {
|
pub fn getPassword(self: *const URL) []const u8 {
|
||||||
return U.getPassword(self._raw);
|
return U.getPassword(self._raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setPassword(self: *URL, value: []const u8) !void {
|
||||||
|
const allocator = self._arena orelse return error.NoAllocator;
|
||||||
|
self._raw = try U.setPassword(self._raw, value, allocator);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getPathname(self: *const URL) []const u8 {
|
pub fn getPathname(self: *const URL) []const u8 {
|
||||||
return U.getPathname(self._raw);
|
return U.getPathname(self._raw);
|
||||||
}
|
}
|
||||||
@@ -272,8 +282,8 @@ pub const JsApi = struct {
|
|||||||
pub const search = bridge.accessor(URL.getSearch, URL.setSearch, .{});
|
pub const search = bridge.accessor(URL.getSearch, URL.setSearch, .{});
|
||||||
pub const hash = bridge.accessor(URL.getHash, URL.setHash, .{});
|
pub const hash = bridge.accessor(URL.getHash, URL.setHash, .{});
|
||||||
pub const pathname = bridge.accessor(URL.getPathname, URL.setPathname, .{});
|
pub const pathname = bridge.accessor(URL.getPathname, URL.setPathname, .{});
|
||||||
pub const username = bridge.accessor(URL.getUsername, null, .{});
|
pub const username = bridge.accessor(URL.getUsername, URL.setUsername, .{});
|
||||||
pub const password = bridge.accessor(URL.getPassword, null, .{});
|
pub const password = bridge.accessor(URL.getPassword, URL.setPassword, .{});
|
||||||
pub const hostname = bridge.accessor(URL.getHostname, URL.setHostname, .{});
|
pub const hostname = bridge.accessor(URL.getHostname, URL.setHostname, .{});
|
||||||
pub const host = bridge.accessor(URL.getHost, URL.setHost, .{});
|
pub const host = bridge.accessor(URL.getHost, URL.setHost, .{});
|
||||||
pub const port = bridge.accessor(URL.getPort, URL.setPort, .{});
|
pub const port = bridge.accessor(URL.getPort, URL.setPort, .{});
|
||||||
|
|||||||
@@ -551,17 +551,14 @@ pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
|
const target = self.asEventTarget();
|
||||||
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
if (page._event_manager.hasDirectListeners(target, "unhandledrejection", self._on_unhandled_rejection)) {
|
||||||
.promise = try rejection.promise().temp(),
|
const event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
|
||||||
}, page)).asEvent();
|
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
||||||
|
.promise = try rejection.promise().temp(),
|
||||||
try page._event_manager.dispatchDirect(
|
}, page)).asEvent();
|
||||||
self.asEventTarget(),
|
try page._event_manager.dispatchDirect(target, event, self._on_unhandled_rejection, .{ .context = "window.unhandledrejection" });
|
||||||
event,
|
}
|
||||||
self._on_unhandled_rejection,
|
|
||||||
.{ .inject_target = true, .context = "window.unhandledrejection" },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScheduleOpts = struct {
|
const ScheduleOpts = struct {
|
||||||
|
|||||||
@@ -219,7 +219,14 @@ pub fn NodeLive(comptime mode: Mode) type {
|
|||||||
switch (mode) {
|
switch (mode) {
|
||||||
.tag => {
|
.tag => {
|
||||||
const el = node.is(Element) orelse return false;
|
const el = node.is(Element) orelse return false;
|
||||||
return el.getTag() == self._filter;
|
// For HTML namespace elements, we can use the optimized tag comparison.
|
||||||
|
// For other namespaces (XML, SVG custom elements, etc.), fall back to string comparison.
|
||||||
|
if (el._namespace == .html) {
|
||||||
|
return el.getTag() == self._filter;
|
||||||
|
}
|
||||||
|
// For non-HTML elements, compare by tag name string
|
||||||
|
const element_tag = el.getTagNameLower();
|
||||||
|
return std.mem.eql(u8, element_tag, @tagName(self._filter));
|
||||||
},
|
},
|
||||||
.tag_name => {
|
.tag_name => {
|
||||||
// If we're in `tag_name` mode, then the tag_name isn't
|
// If we're in `tag_name` mode, then the tag_name isn't
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const js = @import("../../js/js.zig");
|
|||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const Element = @import("../Element.zig");
|
const Element = @import("../Element.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const CSSStyleDeclaration = @This();
|
const CSSStyleDeclaration = @This();
|
||||||
|
|
||||||
_element: ?*Element = null,
|
_element: ?*Element = null,
|
||||||
@@ -114,9 +116,12 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
|
|||||||
|
|
||||||
const normalized = normalizePropertyName(property_name, &page.buf);
|
const normalized = normalizePropertyName(property_name, &page.buf);
|
||||||
|
|
||||||
|
// Normalize the value for canonical serialization
|
||||||
|
const normalized_value = try normalizePropertyValue(page.call_arena, normalized, value);
|
||||||
|
|
||||||
// Find existing property
|
// Find existing property
|
||||||
if (self.findProperty(normalized)) |existing| {
|
if (self.findProperty(normalized)) |existing| {
|
||||||
existing._value = try String.init(page.arena, value, .{});
|
existing._value = try String.init(page.arena, normalized_value, .{});
|
||||||
existing._important = important;
|
existing._important = important;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -125,7 +130,7 @@ fn setPropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, value:
|
|||||||
const prop = try page._factory.create(Property{
|
const prop = try page._factory.create(Property{
|
||||||
._node = .{},
|
._node = .{},
|
||||||
._name = try String.init(page.arena, normalized, .{}),
|
._name = try String.init(page.arena, normalized, .{}),
|
||||||
._value = try String.init(page.arena, value, .{}),
|
._value = try String.init(page.arena, normalized_value, .{}),
|
||||||
._important = important,
|
._important = important,
|
||||||
});
|
});
|
||||||
self._properties.append(&prop._node);
|
self._properties.append(&prop._node);
|
||||||
@@ -227,6 +232,351 @@ fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 {
|
|||||||
return std.ascii.lowerString(buf, name);
|
return std.ascii.lowerString(buf, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize CSS property values for canonical serialization
|
||||||
|
fn normalizePropertyValue(arena: Allocator, property_name: []const u8, value: []const u8) ![]const u8 {
|
||||||
|
// Per CSSOM spec, unitless zero in length properties should serialize as "0px"
|
||||||
|
if (std.mem.eql(u8, value, "0") and isLengthProperty(property_name)) {
|
||||||
|
return "0px";
|
||||||
|
}
|
||||||
|
|
||||||
|
// "first baseline" serializes canonically as "baseline" (first is the default)
|
||||||
|
if (std.ascii.startsWithIgnoreCase(value, "first baseline")) {
|
||||||
|
if (value.len == 14) {
|
||||||
|
// Exact match "first baseline"
|
||||||
|
return "baseline";
|
||||||
|
}
|
||||||
|
if (value.len > 14 and value[14] == ' ') {
|
||||||
|
// "first baseline X" -> "baseline X"
|
||||||
|
return try std.mem.concat(arena, u8, &.{ "baseline", value[14..] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For 2-value shorthand properties, collapse "X X" to "X"
|
||||||
|
if (isTwoValueShorthand(property_name)) {
|
||||||
|
if (collapseDuplicateValue(value)) |single| {
|
||||||
|
return single;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonicalize anchor-size() function: anchor name (dashed ident) comes before size keyword
|
||||||
|
if (std.mem.indexOf(u8, value, "anchor-size(") != null) {
|
||||||
|
return try canonicalizeAnchorSize(arena, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonicalize anchor-size() so that the dashed ident (anchor name) comes before the size keyword.
|
||||||
|
// e.g. "anchor-size(width --foo)" -> "anchor-size(--foo width)"
|
||||||
|
fn canonicalizeAnchorSize(arena: Allocator, value: []const u8) ![]const u8 {
|
||||||
|
var buf = std.Io.Writer.Allocating.init(arena);
|
||||||
|
var i: usize = 0;
|
||||||
|
|
||||||
|
while (i < value.len) {
|
||||||
|
// Look for "anchor-size("
|
||||||
|
if (std.mem.startsWith(u8, value[i..], "anchor-size(")) {
|
||||||
|
try buf.writer.writeAll("anchor-size(");
|
||||||
|
i += "anchor-size(".len;
|
||||||
|
|
||||||
|
// Parse and canonicalize the arguments
|
||||||
|
i = try canonicalizeAnchorSizeArgs(value, i, &buf.writer);
|
||||||
|
} else {
|
||||||
|
try buf.writer.writeByte(value[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.written();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse anchor-size arguments and write them in canonical order
|
||||||
|
fn canonicalizeAnchorSizeArgs(value: []const u8, start: usize, writer: *std.Io.Writer) !usize {
|
||||||
|
var i = start;
|
||||||
|
var depth: usize = 1;
|
||||||
|
|
||||||
|
// Skip leading whitespace
|
||||||
|
while (i < value.len and value[i] == ' ') : (i += 1) {}
|
||||||
|
|
||||||
|
// Collect tokens before the comma or close paren
|
||||||
|
var first_token_start: ?usize = null;
|
||||||
|
var first_token_end: usize = 0;
|
||||||
|
var second_token_start: ?usize = null;
|
||||||
|
var second_token_end: usize = 0;
|
||||||
|
var comma_pos: ?usize = null;
|
||||||
|
var token_count: usize = 0;
|
||||||
|
|
||||||
|
const args_start = i;
|
||||||
|
var in_token = false;
|
||||||
|
|
||||||
|
// First pass: find the structure of arguments before comma/closing paren at depth 1
|
||||||
|
while (i < value.len and depth > 0) {
|
||||||
|
const c = value[i];
|
||||||
|
|
||||||
|
if (c == '(') {
|
||||||
|
depth += 1;
|
||||||
|
in_token = true;
|
||||||
|
i += 1;
|
||||||
|
} else if (c == ')') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth == 0) {
|
||||||
|
if (in_token) {
|
||||||
|
if (token_count == 0) {
|
||||||
|
first_token_end = i;
|
||||||
|
} else if (token_count == 1) {
|
||||||
|
second_token_end = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
} else if (c == ',' and depth == 1) {
|
||||||
|
if (in_token) {
|
||||||
|
if (token_count == 0) {
|
||||||
|
first_token_end = i;
|
||||||
|
} else if (token_count == 1) {
|
||||||
|
second_token_end = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
comma_pos = i;
|
||||||
|
break;
|
||||||
|
} else if (c == ' ') {
|
||||||
|
if (in_token and depth == 1) {
|
||||||
|
if (token_count == 0) {
|
||||||
|
first_token_end = i;
|
||||||
|
token_count = 1;
|
||||||
|
} else if (token_count == 1 and second_token_start != null) {
|
||||||
|
second_token_end = i;
|
||||||
|
token_count = 2;
|
||||||
|
}
|
||||||
|
in_token = false;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
if (!in_token and depth == 1) {
|
||||||
|
if (token_count == 0) {
|
||||||
|
first_token_start = i;
|
||||||
|
} else if (token_count == 1) {
|
||||||
|
second_token_start = i;
|
||||||
|
}
|
||||||
|
in_token = true;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle end of tokens
|
||||||
|
if (in_token and token_count == 1 and second_token_start != null) {
|
||||||
|
second_token_end = i;
|
||||||
|
token_count = 2;
|
||||||
|
} else if (in_token and token_count == 0) {
|
||||||
|
first_token_end = i;
|
||||||
|
token_count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have exactly two tokens that need reordering
|
||||||
|
if (token_count == 2) {
|
||||||
|
const first_start = first_token_start orelse args_start;
|
||||||
|
const second_start = second_token_start orelse first_token_end;
|
||||||
|
|
||||||
|
const first_token = value[first_start..first_token_end];
|
||||||
|
const second_token = value[second_start..second_token_end];
|
||||||
|
|
||||||
|
// If second token is a dashed ident and first is a size keyword, swap them
|
||||||
|
if (std.mem.startsWith(u8, second_token, "--") and isAnchorSizeKeyword(first_token)) {
|
||||||
|
try writer.writeAll(second_token);
|
||||||
|
try writer.writeByte(' ');
|
||||||
|
try writer.writeAll(first_token);
|
||||||
|
} else {
|
||||||
|
// Keep original order
|
||||||
|
try writer.writeAll(first_token);
|
||||||
|
try writer.writeByte(' ');
|
||||||
|
try writer.writeAll(second_token);
|
||||||
|
}
|
||||||
|
} else if (first_token_start) |fts| {
|
||||||
|
// Single token, just copy it
|
||||||
|
try writer.writeAll(value[fts..first_token_end]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle comma and fallback value (may contain nested anchor-size)
|
||||||
|
if (comma_pos) |cp| {
|
||||||
|
try writer.writeAll(", ");
|
||||||
|
i = cp + 1;
|
||||||
|
// Skip whitespace after comma
|
||||||
|
while (i < value.len and value[i] == ' ') : (i += 1) {}
|
||||||
|
|
||||||
|
// Copy the fallback, recursively handling nested anchor-size
|
||||||
|
while (i < value.len and depth > 0) {
|
||||||
|
if (std.mem.startsWith(u8, value[i..], "anchor-size(")) {
|
||||||
|
try writer.writeAll("anchor-size(");
|
||||||
|
i += "anchor-size(".len;
|
||||||
|
depth += 1;
|
||||||
|
i = try canonicalizeAnchorSizeArgs(value, i, writer);
|
||||||
|
depth -= 1;
|
||||||
|
} else if (value[i] == '(') {
|
||||||
|
depth += 1;
|
||||||
|
try writer.writeByte(value[i]);
|
||||||
|
i += 1;
|
||||||
|
} else if (value[i] == ')') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth == 0) break;
|
||||||
|
try writer.writeByte(value[i]);
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
try writer.writeByte(value[i]);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write closing paren
|
||||||
|
try writer.writeByte(')');
|
||||||
|
|
||||||
|
return i + 1; // Skip past the closing paren
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isAnchorSizeKeyword(token: []const u8) bool {
|
||||||
|
const keywords = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "width", {} },
|
||||||
|
.{ "height", {} },
|
||||||
|
.{ "block", {} },
|
||||||
|
.{ "inline", {} },
|
||||||
|
.{ "self-block", {} },
|
||||||
|
.{ "self-inline", {} },
|
||||||
|
});
|
||||||
|
return keywords.has(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a value is "X X" (duplicate) and return just "X"
|
||||||
|
fn collapseDuplicateValue(value: []const u8) ?[]const u8 {
|
||||||
|
const space_idx = std.mem.indexOfScalar(u8, value, ' ') orelse return null;
|
||||||
|
if (space_idx == 0 or space_idx >= value.len - 1) return null;
|
||||||
|
|
||||||
|
const first = value[0..space_idx];
|
||||||
|
const rest = std.mem.trimLeft(u8, value[space_idx + 1 ..], " ");
|
||||||
|
|
||||||
|
// Check if there's only one more value (no additional spaces)
|
||||||
|
if (std.mem.indexOfScalar(u8, rest, ' ') != null) return null;
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, first, rest)) {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isTwoValueShorthand(name: []const u8) bool {
|
||||||
|
const shorthands = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "place-content", {} },
|
||||||
|
.{ "place-items", {} },
|
||||||
|
.{ "place-self", {} },
|
||||||
|
.{ "margin-block", {} },
|
||||||
|
.{ "margin-inline", {} },
|
||||||
|
.{ "padding-block", {} },
|
||||||
|
.{ "padding-inline", {} },
|
||||||
|
.{ "inset-block", {} },
|
||||||
|
.{ "inset-inline", {} },
|
||||||
|
.{ "border-block-style", {} },
|
||||||
|
.{ "border-inline-style", {} },
|
||||||
|
.{ "border-block-width", {} },
|
||||||
|
.{ "border-inline-width", {} },
|
||||||
|
.{ "border-block-color", {} },
|
||||||
|
.{ "border-inline-color", {} },
|
||||||
|
.{ "overflow", {} },
|
||||||
|
.{ "overscroll-behavior", {} },
|
||||||
|
.{ "gap", {} },
|
||||||
|
});
|
||||||
|
return shorthands.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isLengthProperty(name: []const u8) bool {
|
||||||
|
// Properties that accept <length> or <length-percentage> values
|
||||||
|
const length_properties = std.StaticStringMap(void).initComptime(.{
|
||||||
|
// Sizing
|
||||||
|
.{ "width", {} },
|
||||||
|
.{ "height", {} },
|
||||||
|
.{ "min-width", {} },
|
||||||
|
.{ "min-height", {} },
|
||||||
|
.{ "max-width", {} },
|
||||||
|
.{ "max-height", {} },
|
||||||
|
// Margins
|
||||||
|
.{ "margin", {} },
|
||||||
|
.{ "margin-top", {} },
|
||||||
|
.{ "margin-right", {} },
|
||||||
|
.{ "margin-bottom", {} },
|
||||||
|
.{ "margin-left", {} },
|
||||||
|
.{ "margin-block", {} },
|
||||||
|
.{ "margin-block-start", {} },
|
||||||
|
.{ "margin-block-end", {} },
|
||||||
|
.{ "margin-inline", {} },
|
||||||
|
.{ "margin-inline-start", {} },
|
||||||
|
.{ "margin-inline-end", {} },
|
||||||
|
// Padding
|
||||||
|
.{ "padding", {} },
|
||||||
|
.{ "padding-top", {} },
|
||||||
|
.{ "padding-right", {} },
|
||||||
|
.{ "padding-bottom", {} },
|
||||||
|
.{ "padding-left", {} },
|
||||||
|
.{ "padding-block", {} },
|
||||||
|
.{ "padding-block-start", {} },
|
||||||
|
.{ "padding-block-end", {} },
|
||||||
|
.{ "padding-inline", {} },
|
||||||
|
.{ "padding-inline-start", {} },
|
||||||
|
.{ "padding-inline-end", {} },
|
||||||
|
// Positioning
|
||||||
|
.{ "top", {} },
|
||||||
|
.{ "right", {} },
|
||||||
|
.{ "bottom", {} },
|
||||||
|
.{ "left", {} },
|
||||||
|
.{ "inset", {} },
|
||||||
|
.{ "inset-block", {} },
|
||||||
|
.{ "inset-block-start", {} },
|
||||||
|
.{ "inset-block-end", {} },
|
||||||
|
.{ "inset-inline", {} },
|
||||||
|
.{ "inset-inline-start", {} },
|
||||||
|
.{ "inset-inline-end", {} },
|
||||||
|
// Border
|
||||||
|
.{ "border-width", {} },
|
||||||
|
.{ "border-top-width", {} },
|
||||||
|
.{ "border-right-width", {} },
|
||||||
|
.{ "border-bottom-width", {} },
|
||||||
|
.{ "border-left-width", {} },
|
||||||
|
.{ "border-block-width", {} },
|
||||||
|
.{ "border-block-start-width", {} },
|
||||||
|
.{ "border-block-end-width", {} },
|
||||||
|
.{ "border-inline-width", {} },
|
||||||
|
.{ "border-inline-start-width", {} },
|
||||||
|
.{ "border-inline-end-width", {} },
|
||||||
|
.{ "border-radius", {} },
|
||||||
|
.{ "border-top-left-radius", {} },
|
||||||
|
.{ "border-top-right-radius", {} },
|
||||||
|
.{ "border-bottom-left-radius", {} },
|
||||||
|
.{ "border-bottom-right-radius", {} },
|
||||||
|
// Text
|
||||||
|
.{ "font-size", {} },
|
||||||
|
.{ "line-height", {} },
|
||||||
|
.{ "letter-spacing", {} },
|
||||||
|
.{ "word-spacing", {} },
|
||||||
|
.{ "text-indent", {} },
|
||||||
|
// Flexbox/Grid
|
||||||
|
.{ "gap", {} },
|
||||||
|
.{ "row-gap", {} },
|
||||||
|
.{ "column-gap", {} },
|
||||||
|
.{ "flex-basis", {} },
|
||||||
|
// Outline
|
||||||
|
.{ "outline-width", {} },
|
||||||
|
.{ "outline-offset", {} },
|
||||||
|
// Other
|
||||||
|
.{ "border-spacing", {} },
|
||||||
|
.{ "text-shadow", {} },
|
||||||
|
.{ "box-shadow", {} },
|
||||||
|
.{ "baseline-shift", {} },
|
||||||
|
.{ "vertical-align", {} },
|
||||||
|
// Grid lanes
|
||||||
|
.{ "flow-tolerance", {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
return length_properties.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, normalized_name: []const u8) []const u8 {
|
fn getDefaultPropertyValue(self: *const CSSStyleDeclaration, normalized_name: []const u8) []const u8 {
|
||||||
if (std.mem.eql(u8, normalized_name, "visibility")) {
|
if (std.mem.eql(u8, normalized_name, "visibility")) {
|
||||||
return "visible";
|
return "visible";
|
||||||
|
|||||||
@@ -91,39 +91,224 @@ pub fn getNamed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]con
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn isKnownCSSProperty(dash_case: []const u8) bool {
|
fn isKnownCSSProperty(dash_case: []const u8) bool {
|
||||||
// List of common/known CSS properties
|
|
||||||
// In a full implementation, this would include all standard CSS properties
|
|
||||||
const known_properties = std.StaticStringMap(void).initComptime(.{
|
const known_properties = std.StaticStringMap(void).initComptime(.{
|
||||||
|
// Colors & backgrounds
|
||||||
.{ "color", {} },
|
.{ "color", {} },
|
||||||
|
.{ "background", {} },
|
||||||
.{ "background-color", {} },
|
.{ "background-color", {} },
|
||||||
|
.{ "background-image", {} },
|
||||||
|
.{ "background-position", {} },
|
||||||
|
.{ "background-repeat", {} },
|
||||||
|
.{ "background-size", {} },
|
||||||
|
.{ "background-attachment", {} },
|
||||||
|
.{ "background-clip", {} },
|
||||||
|
.{ "background-origin", {} },
|
||||||
|
// Typography
|
||||||
|
.{ "font", {} },
|
||||||
|
.{ "font-family", {} },
|
||||||
.{ "font-size", {} },
|
.{ "font-size", {} },
|
||||||
|
.{ "font-style", {} },
|
||||||
|
.{ "font-weight", {} },
|
||||||
|
.{ "font-variant", {} },
|
||||||
|
.{ "line-height", {} },
|
||||||
|
.{ "letter-spacing", {} },
|
||||||
|
.{ "word-spacing", {} },
|
||||||
|
.{ "text-align", {} },
|
||||||
|
.{ "text-decoration", {} },
|
||||||
|
.{ "text-indent", {} },
|
||||||
|
.{ "text-transform", {} },
|
||||||
|
.{ "white-space", {} },
|
||||||
|
.{ "word-break", {} },
|
||||||
|
.{ "word-wrap", {} },
|
||||||
|
.{ "overflow-wrap", {} },
|
||||||
|
// Box model
|
||||||
|
.{ "margin", {} },
|
||||||
.{ "margin-top", {} },
|
.{ "margin-top", {} },
|
||||||
|
.{ "margin-right", {} },
|
||||||
.{ "margin-bottom", {} },
|
.{ "margin-bottom", {} },
|
||||||
.{ "margin-left", {} },
|
.{ "margin-left", {} },
|
||||||
.{ "margin-right", {} },
|
.{ "margin-block", {} },
|
||||||
|
.{ "margin-block-start", {} },
|
||||||
|
.{ "margin-block-end", {} },
|
||||||
|
.{ "margin-inline", {} },
|
||||||
|
.{ "margin-inline-start", {} },
|
||||||
|
.{ "margin-inline-end", {} },
|
||||||
|
.{ "padding", {} },
|
||||||
.{ "padding-top", {} },
|
.{ "padding-top", {} },
|
||||||
|
.{ "padding-right", {} },
|
||||||
.{ "padding-bottom", {} },
|
.{ "padding-bottom", {} },
|
||||||
.{ "padding-left", {} },
|
.{ "padding-left", {} },
|
||||||
.{ "padding-right", {} },
|
.{ "padding-block", {} },
|
||||||
|
.{ "padding-block-start", {} },
|
||||||
|
.{ "padding-block-end", {} },
|
||||||
|
.{ "padding-inline", {} },
|
||||||
|
.{ "padding-inline-start", {} },
|
||||||
|
.{ "padding-inline-end", {} },
|
||||||
|
// Border
|
||||||
|
.{ "border", {} },
|
||||||
|
.{ "border-width", {} },
|
||||||
|
.{ "border-style", {} },
|
||||||
|
.{ "border-color", {} },
|
||||||
|
.{ "border-top", {} },
|
||||||
|
.{ "border-top-width", {} },
|
||||||
|
.{ "border-top-style", {} },
|
||||||
|
.{ "border-top-color", {} },
|
||||||
|
.{ "border-right", {} },
|
||||||
|
.{ "border-right-width", {} },
|
||||||
|
.{ "border-right-style", {} },
|
||||||
|
.{ "border-right-color", {} },
|
||||||
|
.{ "border-bottom", {} },
|
||||||
|
.{ "border-bottom-width", {} },
|
||||||
|
.{ "border-bottom-style", {} },
|
||||||
|
.{ "border-bottom-color", {} },
|
||||||
|
.{ "border-left", {} },
|
||||||
|
.{ "border-left-width", {} },
|
||||||
|
.{ "border-left-style", {} },
|
||||||
|
.{ "border-left-color", {} },
|
||||||
|
.{ "border-radius", {} },
|
||||||
.{ "border-top-left-radius", {} },
|
.{ "border-top-left-radius", {} },
|
||||||
.{ "border-top-right-radius", {} },
|
.{ "border-top-right-radius", {} },
|
||||||
.{ "border-bottom-left-radius", {} },
|
.{ "border-bottom-left-radius", {} },
|
||||||
.{ "border-bottom-right-radius", {} },
|
.{ "border-bottom-right-radius", {} },
|
||||||
.{ "float", {} },
|
.{ "border-collapse", {} },
|
||||||
.{ "z-index", {} },
|
.{ "border-spacing", {} },
|
||||||
|
// Sizing
|
||||||
.{ "width", {} },
|
.{ "width", {} },
|
||||||
.{ "height", {} },
|
.{ "height", {} },
|
||||||
|
.{ "min-width", {} },
|
||||||
|
.{ "min-height", {} },
|
||||||
|
.{ "max-width", {} },
|
||||||
|
.{ "max-height", {} },
|
||||||
|
.{ "box-sizing", {} },
|
||||||
|
// Positioning
|
||||||
|
.{ "position", {} },
|
||||||
|
.{ "top", {} },
|
||||||
|
.{ "right", {} },
|
||||||
|
.{ "bottom", {} },
|
||||||
|
.{ "left", {} },
|
||||||
|
.{ "inset", {} },
|
||||||
|
.{ "inset-block", {} },
|
||||||
|
.{ "inset-block-start", {} },
|
||||||
|
.{ "inset-block-end", {} },
|
||||||
|
.{ "inset-inline", {} },
|
||||||
|
.{ "inset-inline-start", {} },
|
||||||
|
.{ "inset-inline-end", {} },
|
||||||
|
.{ "z-index", {} },
|
||||||
|
.{ "float", {} },
|
||||||
|
.{ "clear", {} },
|
||||||
|
// Display & visibility
|
||||||
.{ "display", {} },
|
.{ "display", {} },
|
||||||
.{ "visibility", {} },
|
.{ "visibility", {} },
|
||||||
.{ "opacity", {} },
|
.{ "opacity", {} },
|
||||||
.{ "filter", {} },
|
.{ "overflow", {} },
|
||||||
|
.{ "overflow-x", {} },
|
||||||
|
.{ "overflow-y", {} },
|
||||||
|
.{ "clip", {} },
|
||||||
|
.{ "clip-path", {} },
|
||||||
|
// Flexbox
|
||||||
|
.{ "flex", {} },
|
||||||
|
.{ "flex-direction", {} },
|
||||||
|
.{ "flex-wrap", {} },
|
||||||
|
.{ "flex-flow", {} },
|
||||||
|
.{ "flex-grow", {} },
|
||||||
|
.{ "flex-shrink", {} },
|
||||||
|
.{ "flex-basis", {} },
|
||||||
|
.{ "order", {} },
|
||||||
|
// Grid
|
||||||
|
.{ "grid", {} },
|
||||||
|
.{ "grid-template", {} },
|
||||||
|
.{ "grid-template-columns", {} },
|
||||||
|
.{ "grid-template-rows", {} },
|
||||||
|
.{ "grid-template-areas", {} },
|
||||||
|
.{ "grid-auto-columns", {} },
|
||||||
|
.{ "grid-auto-rows", {} },
|
||||||
|
.{ "grid-auto-flow", {} },
|
||||||
|
.{ "grid-column", {} },
|
||||||
|
.{ "grid-column-start", {} },
|
||||||
|
.{ "grid-column-end", {} },
|
||||||
|
.{ "grid-row", {} },
|
||||||
|
.{ "grid-row-start", {} },
|
||||||
|
.{ "grid-row-end", {} },
|
||||||
|
.{ "grid-area", {} },
|
||||||
|
.{ "gap", {} },
|
||||||
|
.{ "row-gap", {} },
|
||||||
|
.{ "column-gap", {} },
|
||||||
|
// Alignment (flexbox & grid)
|
||||||
|
.{ "align-content", {} },
|
||||||
|
.{ "align-items", {} },
|
||||||
|
.{ "align-self", {} },
|
||||||
|
.{ "justify-content", {} },
|
||||||
|
.{ "justify-items", {} },
|
||||||
|
.{ "justify-self", {} },
|
||||||
|
.{ "place-content", {} },
|
||||||
|
.{ "place-items", {} },
|
||||||
|
.{ "place-self", {} },
|
||||||
|
// Transforms & animations
|
||||||
.{ "transform", {} },
|
.{ "transform", {} },
|
||||||
|
.{ "transform-origin", {} },
|
||||||
|
.{ "transform-style", {} },
|
||||||
|
.{ "perspective", {} },
|
||||||
|
.{ "perspective-origin", {} },
|
||||||
.{ "transition", {} },
|
.{ "transition", {} },
|
||||||
.{ "position", {} },
|
.{ "transition-property", {} },
|
||||||
.{ "top", {} },
|
.{ "transition-duration", {} },
|
||||||
.{ "bottom", {} },
|
.{ "transition-timing-function", {} },
|
||||||
.{ "left", {} },
|
.{ "transition-delay", {} },
|
||||||
.{ "right", {} },
|
.{ "animation", {} },
|
||||||
|
.{ "animation-name", {} },
|
||||||
|
.{ "animation-duration", {} },
|
||||||
|
.{ "animation-timing-function", {} },
|
||||||
|
.{ "animation-delay", {} },
|
||||||
|
.{ "animation-iteration-count", {} },
|
||||||
|
.{ "animation-direction", {} },
|
||||||
|
.{ "animation-fill-mode", {} },
|
||||||
|
.{ "animation-play-state", {} },
|
||||||
|
// Filters & effects
|
||||||
|
.{ "filter", {} },
|
||||||
|
.{ "backdrop-filter", {} },
|
||||||
|
.{ "box-shadow", {} },
|
||||||
|
.{ "text-shadow", {} },
|
||||||
|
// Outline
|
||||||
|
.{ "outline", {} },
|
||||||
|
.{ "outline-width", {} },
|
||||||
|
.{ "outline-style", {} },
|
||||||
|
.{ "outline-color", {} },
|
||||||
|
.{ "outline-offset", {} },
|
||||||
|
// Lists
|
||||||
|
.{ "list-style", {} },
|
||||||
|
.{ "list-style-type", {} },
|
||||||
|
.{ "list-style-position", {} },
|
||||||
|
.{ "list-style-image", {} },
|
||||||
|
// Tables
|
||||||
|
.{ "table-layout", {} },
|
||||||
|
.{ "caption-side", {} },
|
||||||
|
.{ "empty-cells", {} },
|
||||||
|
// Misc
|
||||||
|
.{ "cursor", {} },
|
||||||
|
.{ "pointer-events", {} },
|
||||||
|
.{ "user-select", {} },
|
||||||
|
.{ "resize", {} },
|
||||||
|
.{ "object-fit", {} },
|
||||||
|
.{ "object-position", {} },
|
||||||
|
.{ "vertical-align", {} },
|
||||||
|
.{ "content", {} },
|
||||||
|
.{ "quotes", {} },
|
||||||
|
.{ "counter-reset", {} },
|
||||||
|
.{ "counter-increment", {} },
|
||||||
|
// Scrolling
|
||||||
|
.{ "scroll-behavior", {} },
|
||||||
|
.{ "scroll-margin", {} },
|
||||||
|
.{ "scroll-padding", {} },
|
||||||
|
.{ "overscroll-behavior", {} },
|
||||||
|
.{ "overscroll-behavior-x", {} },
|
||||||
|
.{ "overscroll-behavior-y", {} },
|
||||||
|
// Containment
|
||||||
|
.{ "contain", {} },
|
||||||
|
.{ "container", {} },
|
||||||
|
.{ "container-type", {} },
|
||||||
|
.{ "container-name", {} },
|
||||||
|
// Aspect ratio
|
||||||
|
.{ "aspect-ratio", {} },
|
||||||
});
|
});
|
||||||
|
|
||||||
return known_properties.has(dash_case);
|
return known_properties.has(dash_case);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const Http = @import("../../../http/Http.zig");
|
|||||||
const URL = @import("../URL.zig");
|
const URL = @import("../URL.zig");
|
||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const Headers = @import("Headers.zig");
|
const Headers = @import("Headers.zig");
|
||||||
|
const Blob = @import("../Blob.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Request = @This();
|
const Request = @This();
|
||||||
@@ -153,6 +154,55 @@ pub fn getHeaders(self: *Request, page: *Page) !*Headers {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn blob(self: *Request, page: *Page) !js.Promise {
|
||||||
|
const body = self._body orelse "";
|
||||||
|
const headers = try self.getHeaders(page);
|
||||||
|
const content_type = try headers.get("content-type", page) orelse "";
|
||||||
|
|
||||||
|
const b = try Blob.initWithMimeValidation(
|
||||||
|
&.{body},
|
||||||
|
.{ .type = content_type },
|
||||||
|
true,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
|
||||||
|
return page.js.local.?.resolvePromise(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(self: *const Request, page: *Page) !js.Promise {
|
||||||
|
const body = self._body orelse "";
|
||||||
|
return page.js.local.?.resolvePromise(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json(self: *const Request, page: *Page) !js.Promise {
|
||||||
|
const body = self._body orelse "";
|
||||||
|
const local = page.js.local.?;
|
||||||
|
const value = local.parseJSON(body) catch |err| {
|
||||||
|
return local.rejectPromise(.{@errorName(err)});
|
||||||
|
};
|
||||||
|
return local.resolvePromise(try value.persist());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arrayBuffer(self: *const Request, page: *Page) !js.Promise {
|
||||||
|
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bytes(self: *const Request, page: *Page) !js.Promise {
|
||||||
|
return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone(self: *const Request, page: *Page) !*Request {
|
||||||
|
return page._factory.create(Request{
|
||||||
|
._url = self._url,
|
||||||
|
._arena = self._arena,
|
||||||
|
._method = self._method,
|
||||||
|
._headers = self._headers,
|
||||||
|
._cache = self._cache,
|
||||||
|
._credentials = self._credentials,
|
||||||
|
._body = self._body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Request);
|
pub const bridge = js.Bridge(Request);
|
||||||
|
|
||||||
@@ -168,6 +218,12 @@ pub const JsApi = struct {
|
|||||||
pub const headers = bridge.accessor(Request.getHeaders, null, .{});
|
pub const headers = bridge.accessor(Request.getHeaders, null, .{});
|
||||||
pub const cache = bridge.accessor(Request.getCache, null, .{});
|
pub const cache = bridge.accessor(Request.getCache, null, .{});
|
||||||
pub const credentials = bridge.accessor(Request.getCredentials, null, .{});
|
pub const credentials = bridge.accessor(Request.getCredentials, null, .{});
|
||||||
|
pub const blob = bridge.function(Request.blob, .{});
|
||||||
|
pub const text = bridge.function(Request.text, .{});
|
||||||
|
pub const json = bridge.function(Request.json, .{});
|
||||||
|
pub const arrayBuffer = bridge.function(Request.arrayBuffer, .{});
|
||||||
|
pub const bytes = bridge.function(Request.bytes, .{});
|
||||||
|
pub const clone = bridge.function(Request.clone, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../../testing.zig");
|
const testing = @import("../../../testing.zig");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const Http = @import("../../../http/Http.zig");
|
|||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const Headers = @import("Headers.zig");
|
const Headers = @import("Headers.zig");
|
||||||
const ReadableStream = @import("../streams/ReadableStream.zig");
|
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||||
|
const Blob = @import("../Blob.zig");
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -147,6 +148,47 @@ pub fn arrayBuffer(self: *const Response, page: *Page) !js.Promise {
|
|||||||
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" });
|
return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn blob(self: *const Response, page: *Page) !js.Promise {
|
||||||
|
const body = self._body orelse "";
|
||||||
|
const content_type = try self._headers.get("content-type", page) orelse "";
|
||||||
|
|
||||||
|
const b = try Blob.initWithMimeValidation(
|
||||||
|
&.{body},
|
||||||
|
.{ .type = content_type },
|
||||||
|
true,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
|
||||||
|
return page.js.local.?.resolvePromise(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bytes(self: *const Response, page: *Page) !js.Promise {
|
||||||
|
return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone(self: *const Response, page: *Page) !*Response {
|
||||||
|
const arena = try page.getArena(.{ .debug = "Response.clone" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const body = if (self._body) |b| try arena.dupe(u8, b) else null;
|
||||||
|
const status_text = try arena.dupe(u8, self._status_text);
|
||||||
|
const url = try arena.dupeZ(u8, self._url);
|
||||||
|
|
||||||
|
const cloned = try arena.create(Response);
|
||||||
|
cloned.* = .{
|
||||||
|
._arena = arena,
|
||||||
|
._status = self._status,
|
||||||
|
._status_text = status_text,
|
||||||
|
._url = url,
|
||||||
|
._body = body,
|
||||||
|
._type = self._type,
|
||||||
|
._is_redirected = self._is_redirected,
|
||||||
|
._headers = try Headers.init(.{ .obj = self._headers }, page),
|
||||||
|
._transfer = null,
|
||||||
|
};
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Response);
|
pub const bridge = js.Bridge(Response);
|
||||||
|
|
||||||
@@ -170,6 +212,9 @@ pub const JsApi = struct {
|
|||||||
pub const url = bridge.accessor(Response.getURL, null, .{});
|
pub const url = bridge.accessor(Response.getURL, null, .{});
|
||||||
pub const redirected = bridge.accessor(Response.isRedirected, null, .{});
|
pub const redirected = bridge.accessor(Response.isRedirected, null, .{});
|
||||||
pub const arrayBuffer = bridge.function(Response.arrayBuffer, .{});
|
pub const arrayBuffer = bridge.function(Response.arrayBuffer, .{});
|
||||||
|
pub const blob = bridge.function(Response.blob, .{});
|
||||||
|
pub const bytes = bridge.function(Response.bytes, .{});
|
||||||
|
pub const clone = bridge.function(Response.clone, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../../testing.zig");
|
const testing = @import("../../../testing.zig");
|
||||||
|
|||||||
@@ -508,13 +508,11 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void {
|
|||||||
|
|
||||||
self._ready_state = state;
|
self._ready_state = state;
|
||||||
|
|
||||||
const event = try Event.initTrusted(.wrap("readystatechange"), .{}, page);
|
const target = self.asEventTarget();
|
||||||
try page._event_manager.dispatchDirect(
|
if (page._event_manager.hasDirectListeners(target, "readystatechange", self._on_ready_state_change)) {
|
||||||
self.asEventTarget(),
|
const event = try Event.initTrusted(.wrap("readystatechange"), .{}, page);
|
||||||
event,
|
try page._event_manager.dispatchDirect(target, event, self._on_ready_state_change, .{ .context = "XHR state change" });
|
||||||
self._on_ready_state_change,
|
}
|
||||||
.{ .context = "XHR state change" },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseMethod(method: []const u8) !Http.Method {
|
fn parseMethod(method: []const u8) !Http.Method {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const log = @import("../../log.zig");
|
|||||||
const markdown = lp.markdown;
|
const markdown = lp.markdown;
|
||||||
const SemanticTree = lp.SemanticTree;
|
const SemanticTree = lp.SemanticTree;
|
||||||
const interactive = lp.interactive;
|
const interactive = lp.interactive;
|
||||||
|
const structured_data = lp.structured_data;
|
||||||
const Node = @import("../Node.zig");
|
const Node = @import("../Node.zig");
|
||||||
const DOMNode = @import("../../browser/webapi/Node.zig");
|
const DOMNode = @import("../../browser/webapi/Node.zig");
|
||||||
|
|
||||||
@@ -30,12 +31,14 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
getMarkdown,
|
getMarkdown,
|
||||||
getSemanticTree,
|
getSemanticTree,
|
||||||
getInteractiveElements,
|
getInteractiveElements,
|
||||||
|
getStructuredData,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.getMarkdown => return getMarkdown(cmd),
|
.getMarkdown => return getMarkdown(cmd),
|
||||||
.getSemanticTree => return getSemanticTree(cmd),
|
.getSemanticTree => return getSemanticTree(cmd),
|
||||||
.getInteractiveElements => return getInteractiveElements(cmd),
|
.getInteractiveElements => return getInteractiveElements(cmd),
|
||||||
|
.getStructuredData => return getStructuredData(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +131,21 @@ fn getInteractiveElements(cmd: anytype) !void {
|
|||||||
}, .{});
|
}, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn getStructuredData(cmd: anytype) !void {
|
||||||
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
const data = try structured_data.collectStructuredData(
|
||||||
|
page.document.asNode(),
|
||||||
|
cmd.arena,
|
||||||
|
page,
|
||||||
|
);
|
||||||
|
|
||||||
|
return cmd.sendResult(.{
|
||||||
|
.structuredData = data,
|
||||||
|
}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "cdp.lp: getMarkdown" {
|
test "cdp.lp: getMarkdown" {
|
||||||
var ctx = testing.context();
|
var ctx = testing.context();
|
||||||
@@ -161,3 +179,19 @@ test "cdp.lp: getInteractiveElements" {
|
|||||||
try testing.expect(result.get("elements") != null);
|
try testing.expect(result.get("elements") != null);
|
||||||
try testing.expect(result.get("nodeIds") != null);
|
try testing.expect(result.get("nodeIds") != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "cdp.lp: getStructuredData" {
|
||||||
|
var ctx = testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
const bc = try ctx.loadBrowserContext(.{});
|
||||||
|
_ = try bc.session.createPage();
|
||||||
|
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 1,
|
||||||
|
.method = "LP.getStructuredData",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = ctx.client.?.sent.items[0].object.get("result").?.object;
|
||||||
|
try testing.expect(result.get("structuredData") != null);
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
@@ -34,6 +34,7 @@ pub const markdown = @import("browser/markdown.zig");
|
|||||||
pub const SemanticTree = @import("SemanticTree.zig");
|
pub const SemanticTree = @import("SemanticTree.zig");
|
||||||
pub const CDPNode = @import("cdp/Node.zig");
|
pub const CDPNode = @import("cdp/Node.zig");
|
||||||
pub const interactive = @import("browser/interactive.zig");
|
pub const interactive = @import("browser/interactive.zig");
|
||||||
|
pub const structured_data = @import("browser/structured_data.zig");
|
||||||
pub const mcp = @import("mcp.zig");
|
pub const mcp = @import("mcp.zig");
|
||||||
pub const build_config = @import("build_config");
|
pub const build_config = @import("build_config");
|
||||||
pub const crash_handler = @import("crash_handler.zig");
|
pub const crash_handler = @import("crash_handler.zig");
|
||||||
|
|||||||
Reference in New Issue
Block a user