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, .{});