diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index 6b119843..37d947c4 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -1221,7 +1221,6 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
return node;
};
-
// After constructor runs, invoke attributeChangedCallback for initial attributes
const element = node.as(Element);
if (element._attributes) |attributes| {
diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig
index 57cd6524..f037713f 100644
--- a/src/browser/ScriptManager.zig
+++ b/src/browser/ScriptManager.zig
@@ -300,7 +300,7 @@ pub fn resolveSpecifier(self: *ScriptManager, arena: Allocator, base: [:0]const
return s;
}
- return URL.resolve(arena, base, specifier, .{.always_dupe = true});
+ return URL.resolve(arena, base, specifier, .{ .always_dupe = true });
}
pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const u8) !void {
diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig
index 3dc59ab5..2c87510f 100644
--- a/src/browser/js/Env.zig
+++ b/src/browser/js/Env.zig
@@ -198,13 +198,10 @@ fn promiseRejectCallback(v8_msg: v8.C_PromiseRejectMessage) callconv(.c) void {
const value =
if (msg.getValue()) |v8_value|
context.valueToString(v8_value, .{}) catch |err| @errorName(err)
- else "no value"
- ;
+ else
+ "no value";
- log.debug(.js, "unhandled rejection", .{
- .value = value,
- .stack = context.stackTrace() catch |err| @errorName(err) orelse "???"
- });
+ log.debug(.js, "unhandled rejection", .{ .value = value, .stack = context.stackTrace() catch |err| @errorName(err) orelse "???" });
}
// Give it a Zig struct, get back a v8.FunctionTemplate.
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 63aef20a..850c8070 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -59,7 +59,7 @@ pub fn Builder(comptime T: type) type {
pub fn property(value: anytype) Property {
switch (@typeInfo(@TypeOf(value))) {
- .comptime_int, .int => return .{.int = value},
+ .comptime_int, .int => return .{ .int = value },
else => {},
}
@compileError("Property for " ++ @typeName(@TypeOf(value)) ++ " hasn't been defined yet");
@@ -485,6 +485,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/collections.zig"),
@import("../webapi/Console.zig"),
@import("../webapi/Crypto.zig"),
+ @import("../webapi/CSS.zig"),
@import("../webapi/css/CSSRule.zig"),
@import("../webapi/css/CSSRuleList.zig"),
@import("../webapi/css/CSSStyleDeclaration.zig"),
diff --git a/src/browser/tests/css.html b/src/browser/tests/css.html
new file mode 100644
index 00000000..ac0b6aba
--- /dev/null
+++ b/src/browser/tests/css.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index f73e5374..c688d6a8 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -32,7 +32,7 @@ pub const Attribute = @import("element/Attribute.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
pub const DOMStringMap = @import("element/DOMStringMap.zig");
const DOMRect = @import("DOMRect.zig");
-const css = @import("css.zig");
+const CSS = @import("CSS.zig");
const ShadowRoot = @import("ShadowRoot.zig");
pub const Svg = @import("element/Svg.zig");
@@ -623,8 +623,8 @@ pub fn getBoundingClientRect(self: *Element, page: *Page) !*DOMRect {
const style = try self.getStyle(page);
const decl = style.asCSSStyleDeclaration();
- width = css.parseDimension(decl.getPropertyValue("width", page)) orelse 1.0;
- height = css.parseDimension(decl.getPropertyValue("height", page)) orelse 1.0;
+ width = CSS.parseDimension(decl.getPropertyValue("width", page)) orelse 1.0;
+ height = CSS.parseDimension(decl.getPropertyValue("height", page)) orelse 1.0;
if (width == 1.0 or height == 1.0) {
const tag = self.getTag();
diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig
index 60b972a3..c659a7f8 100644
--- a/src/browser/webapi/Performance.zig
+++ b/src/browser/webapi/Performance.zig
@@ -69,7 +69,7 @@ pub const Entry = struct {
}
pub fn getEntryType(self: *const Entry) []const u8 {
- return switch (self._entry_type) {
+ return switch (self._entry_type) {
.first_input => "first-input",
.largest_contentful_paint => "largest-contentful-paint",
.layout_shift => "layout-shift",
diff --git a/src/browser/webapi/PerformanceObserver.zig b/src/browser/webapi/PerformanceObserver.zig
index 68eafe01..cd77ad18 100644
--- a/src/browser/webapi/PerformanceObserver.zig
+++ b/src/browser/webapi/PerformanceObserver.zig
@@ -68,5 +68,5 @@ pub const JsApi = struct {
pub const observe = bridge.function(PerformanceObserver.observe, .{});
pub const disconnect = bridge.function(PerformanceObserver.disconnect, .{});
pub const takeRecords = bridge.function(PerformanceObserver.takeRecords, .{});
- pub const supportedEntryTypes = bridge.accessor(PerformanceObserver.getSupportedEntryTypes, null, .{.static = true});
+ pub const supportedEntryTypes = bridge.accessor(PerformanceObserver.getSupportedEntryTypes, null, .{ .static = true });
};
diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig
index 503dc008..16562980 100644
--- a/src/browser/webapi/Window.zig
+++ b/src/browser/webapi/Window.zig
@@ -25,6 +25,7 @@ const Page = @import("../Page.zig");
const Console = @import("Console.zig");
const History = @import("History.zig");
const Crypto = @import("Crypto.zig");
+const CSS = @import("CSS.zig");
const Navigator = @import("Navigator.zig");
const Screen = @import("Screen.zig");
const Performance = @import("Performance.zig");
@@ -43,6 +44,7 @@ const Window = @This();
_proto: *EventTarget,
_document: *Document,
+_css: CSS = .init,
_crypto: Crypto = .init,
_console: Console = .init,
_navigator: Navigator = .init,
@@ -89,6 +91,10 @@ pub fn getCrypto(self: *Window) *Crypto {
return &self._crypto;
}
+pub fn getCSS(self: *Window) *CSS {
+ return &self._css;
+}
+
pub fn getPerformance(self: *Window) *Performance {
return &self._performance;
}
@@ -380,6 +386,7 @@ pub const JsApi = struct {
pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" });
pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" });
pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" });
+ pub const CSS = bridge.accessor(Window.getCSS, null, .{ .cache = "CSS" });
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{});
diff --git a/src/browser/webapi/css.zig b/src/browser/webapi/css.zig
index f285e8d2..a2f320f7 100644
--- a/src/browser/webapi/css.zig
+++ b/src/browser/webapi/css.zig
@@ -17,6 +17,13 @@
// along with this program. If not, see .
const std = @import("std");
+const js = @import("../js/js.zig");
+const Page = @import("../Page.zig");
+
+const CSS = @This();
+_pad: bool = false,
+
+pub const init: CSS = .{};
pub fn parseDimension(value: []const u8) ?f64 {
if (value.len == 0) {
@@ -30,3 +37,134 @@ pub fn parseDimension(value: []const u8) ?f64 {
return std.fmt.parseFloat(f64, num_str) catch null;
}
+
+/// Escapes a CSS identifier string
+/// https://drafts.csswg.org/cssom/#the-css.escape()-method
+pub fn escape(_: *const CSS, value: []const u8, page: *Page) ![]const u8 {
+ if (value.len == 0) {
+ return error.InvalidCharacterError;
+ }
+
+ const first = value[0];
+
+ // Count how many characters we need for the output
+ var out_len: usize = escapeLen(true, first);
+ for (value[1..]) |c| {
+ out_len += escapeLen(false, c);
+ }
+
+ if (out_len == value.len) {
+ return value;
+ }
+
+ const result = try page.call_arena.alloc(u8, out_len);
+ var pos: usize = 0;
+
+ if (needsEscape(true, first)) {
+ pos = writeEscape(true, result, first);
+ } else {
+ result[0] = first;
+ pos = 1;
+ }
+
+ for (value[1..]) |c| {
+ if (!needsEscape(false, c)) {
+ result[pos] = c;
+ pos += 1;
+ } else {
+ pos += writeEscape(false, result[pos..], c);
+ }
+ }
+
+ return result;
+}
+
+pub fn supports(_: *const CSS, property_or_condition: []const u8, value: ?[]const u8) bool {
+ _ = property_or_condition;
+ _ = value;
+ return true;
+}
+
+fn escapeLen(comptime is_first: bool, c: u8) usize {
+ if (needsEscape(is_first, c) == false) {
+ return 1;
+ }
+ if (c == 0) {
+ return "\u{FFFD}".len;
+ }
+ if (isHexEscape(c) or ((comptime is_first) and c >= '0' and c <= '9')) {
+ // Will be escaped as \XX (backslash + 1-6 hex digits + space)
+ return 2 + hexDigitsNeeded(c);
+ }
+ // Escaped as \C (backslash + character)
+ return 2;
+}
+
+fn needsEscape(comptime is_first: bool, c: u8) bool {
+ if (comptime is_first) {
+ if (c >= '0' and c <= '9') {
+ return true;
+ }
+ if (c == '-') {
+ return true;
+ }
+ }
+
+ // Characters that need escaping
+ return switch (c) {
+ 0...0x1F, 0x7F => true,
+ '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '`', '{', '|', '}', '~' => true,
+ ' ' => true,
+ else => false,
+ };
+}
+
+fn isHexEscape(c: u8) bool {
+ return (c >= 0x00 and c <= 0x1F) or c == 0x7F;
+}
+
+fn hexDigitsNeeded(c: u8) usize {
+ if (c < 0x10) {
+ return 1;
+ }
+ return 2;
+}
+
+fn writeEscape(comptime is_first: bool, buf: []u8, c: u8) usize {
+ buf[0] = '\\';
+ var data = buf[1..];
+
+ if (c == 0) {
+ // NULL character becomes replacement character
+ const replacement = "\u{FFFD}";
+ @memcpy(data[0..replacement.len], replacement);
+ return 1 + replacement.len;
+ }
+
+ if (isHexEscape(c) or ((comptime is_first) and c >= '0' and c <= '9')) {
+ const hex_str = std.fmt.bufPrint(data, "{x} ", .{c}) catch unreachable;
+ return 1 + hex_str.len;
+ }
+
+ data[0] = c;
+ return 2;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(CSS);
+
+ pub const Meta = struct {
+ pub const name = "Css";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ pub const empty_with_no_proto = true;
+ };
+
+ pub const escape = bridge.function(CSS.escape, .{});
+ pub const supports = bridge.function(CSS.supports, .{});
+};
+
+const testing = @import("../../testing.zig");
+test "WebApi: CSS" {
+ try testing.htmlRunner("css.html", .{});
+}
diff --git a/src/browser/webapi/element/html/Slot.zig b/src/browser/webapi/element/html/Slot.zig
index 1089ad56..9e0c41b9 100644
--- a/src/browser/webapi/element/html/Slot.zig
+++ b/src/browser/webapi/element/html/Slot.zig
@@ -97,7 +97,7 @@ pub fn assign(self: *Slot, nodes: []const *Node) void {
_ = nodes;
// let's see if this is ever actually used
- log.warn(.not_implemented, "Slot.assign", .{ });
+ log.warn(.not_implemented, "Slot.assign", .{});
}
fn findShadowRoot(self: *Slot) ?*ShadowRoot {
diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig
index d072f7b6..de1c151b 100644
--- a/src/browser/webapi/net/Response.zig
+++ b/src/browser/webapi/net/Response.zig
@@ -20,6 +20,7 @@ const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
+const Headers = @import("Headers.zig");
const Allocator = std.mem.Allocator;
const Response = @This();
@@ -28,6 +29,22 @@ _status: u16,
_data: []const u8,
_arena: Allocator,
+const InitOpts = struct {
+ status: u16 = 200,
+ headers: ?*Headers = null,
+ statusText: ?[]const u8 = null,
+};
+
+pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
+ const opts = opts_ orelse InitOpts{};
+
+ return page._factory.create(Response{
+ ._status = opts.status,
+ ._data = if (body_) |b| try page.arena.dupe(u8, b) else "",
+ ._arena = page.arena,
+ });
+}
+
pub fn initFromFetch(arena: Allocator, data: []const u8, page: *Page) !*Response {
return page._factory.create(Response{
._status = 200,
@@ -65,6 +82,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
+ pub const constructor = bridge.constructor(Response.init, .{});
pub const ok = bridge.accessor(Response.isOK, null, .{});
pub const status = bridge.accessor(Response.getStatus, null, .{});
pub const json = bridge.function(Response.getJson, .{});