From 218d08b1f68ab03111950800602cd9bd7b867290 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 25 Nov 2025 13:00:32 +0800 Subject: [PATCH] add some skeleton implementations for various CSS WebAPIs --- src/browser/dump.zig | 2 +- src/browser/js/bridge.zig | 5 + src/browser/tests/css/stylesheet.html | 207 ++++++++++++++++++ src/browser/webapi/Document.zig | 13 +- src/browser/webapi/Navigator.zig | 1 - src/browser/webapi/Window.zig | 1 - src/browser/webapi/css/CSSRule.zig | 90 ++++++++ src/browser/webapi/css/CSSRuleList.zig | 36 +++ .../webapi/css/CSSStyleDeclaration.zig | 44 ++-- src/browser/webapi/css/CSSStyleProperties.zig | 4 +- src/browser/webapi/css/CSSStyleRule.zig | 48 ++++ src/browser/webapi/css/CSSStyleSheet.zig | 87 ++++++++ src/browser/webapi/css/StyleSheetList.zig | 34 +++ src/browser/webapi/selector/Parser.zig | 1 - src/cdp/domains/log.zig | 2 +- src/html5ever/lib.rs | 1 + 16 files changed, 547 insertions(+), 29 deletions(-) create mode 100644 src/browser/tests/css/stylesheet.html create mode 100644 src/browser/webapi/css/CSSRule.zig create mode 100644 src/browser/webapi/css/CSSRuleList.zig create mode 100644 src/browser/webapi/css/CSSStyleRule.zig create mode 100644 src/browser/webapi/css/CSSStyleSheet.zig create mode 100644 src/browser/webapi/css/StyleSheetList.zig diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 2e00ba39..0617b488 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -45,7 +45,7 @@ pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void { } } - return deep(doc.asNode(), .{.strip = opts.strip}, writer); + return deep(doc.asNode(), .{ .strip = opts.strip }, writer); } pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer) error{WriteFailed}!void { diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index bc380d1a..d4b6b6fe 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -488,9 +488,14 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/collections.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), + @import("../webapi/css/CSSRule.zig"), + @import("../webapi/css/CSSRuleList.zig"), @import("../webapi/css/CSSStyleDeclaration.zig"), + @import("../webapi/css/CSSStyleRule.zig"), + @import("../webapi/css/CSSStyleSheet.zig"), @import("../webapi/css/CSSStyleProperties.zig"), @import("../webapi/css/MediaQueryList.zig"), + @import("../webapi/css/StyleSheetList.zig"), @import("../webapi/Document.zig"), @import("../webapi/HTMLDocument.zig"), @import("../webapi/History.zig"), diff --git a/src/browser/tests/css/stylesheet.html b/src/browser/tests/css/stylesheet.html new file mode 100644 index 00000000..abc1ed92 --- /dev/null +++ b/src/browser/tests/css/stylesheet.html @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index e895bfd4..05223fde 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -31,6 +31,7 @@ const NodeFilter = @import("NodeFilter.zig"); const DOMTreeWalker = @import("DOMTreeWalker.zig"); const DOMNodeIterator = @import("DOMNodeIterator.zig"); const DOMImplementation = @import("DOMImplementation.zig"); +const StyleSheetList = @import("css/StyleSheetList.zig"); pub const HTMLDocument = @import("HTMLDocument.zig"); @@ -43,6 +44,7 @@ _ready_state: ReadyState = .loading, _current_script: ?*Element.Html.Script = null, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, _active_element: ?*Element = null, +_style_sheets: ?*StyleSheetList = null, pub const Type = union(enum) { generic, @@ -225,6 +227,15 @@ pub fn getActiveElement(self: *Document) ?*Element { return self.getDocumentElement(); } +pub fn getStyleSheets(self: *Document, page: *Page) !*StyleSheetList { + if (self._style_sheets) |sheets| { + return sheets; + } + const sheets = try StyleSheetList.init(page); + self._style_sheets = sheets; + return sheets; +} + const ReadyState = enum { loading, interactive, @@ -253,7 +264,7 @@ pub const JsApi = struct { pub const readyState = bridge.accessor(Document.getReadyState, null, .{}); pub const implementation = bridge.accessor(Document.getImplementation, null, .{}); pub const activeElement = bridge.accessor(Document.getActiveElement, null, .{}); - + pub const styleSheets = bridge.accessor(Document.getStyleSheets, null, .{}); pub const createElement = bridge.function(Document.createElement, .{}); pub const createElementNS = bridge.function(Document.createElementNS, .{}); pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{}); diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 63b4cfc9..3fa8154f 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -120,4 +120,3 @@ pub const JsApi = struct { // Methods pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{}); }; - diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index b65359e4..1607bb79 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -243,7 +243,6 @@ pub fn atob(_: *const Window, input: []const u8, page: *Page) ![]const u8 { return decoded; } - const ScheduleOpts = struct { repeat: bool, params: []js.Object, diff --git a/src/browser/webapi/css/CSSRule.zig b/src/browser/webapi/css/CSSRule.zig new file mode 100644 index 00000000..dcf41db9 --- /dev/null +++ b/src/browser/webapi/css/CSSRule.zig @@ -0,0 +1,90 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); + +const CSSRule = @This(); + +pub const Type = enum(u16) { + style = 1, + charset = 2, + import = 3, + media = 4, + font_face = 5, + page = 6, + keyframes = 7, + keyframe = 8, + margin = 9, + namespace = 10, + counter_style = 11, + supports = 12, + document = 13, + font_feature_values = 14, + viewport = 15, + region_style = 16, +}; + +_type: Type, + +pub fn init(rule_type: Type, page: *Page) !*CSSRule { + return page._factory.create(CSSRule{ + ._type = rule_type, + }); +} + +pub fn getType(self: *const CSSRule) u16 { + return @intFromEnum(self._type); +} + +pub fn getCssText(self: *const CSSRule, page: *Page) []const u8 { + _ = self; + _ = page; + return ""; +} + +pub fn setCssText(self: *CSSRule, text: []const u8, page: *Page) !void { + _ = self; + _ = text; + _ = page; +} + +pub fn getParentRule(self: *const CSSRule) ?*CSSRule { + _ = self; + return null; +} + +pub fn getParentStyleSheet(self: *const CSSRule) ?*CSSRule { + _ = self; + return null; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSSRule); + + pub const Meta = struct { + pub const name = "CSSRule"; + pub var class_id: bridge.ClassId = undefined; + pub const prototype_chain = bridge.prototypeChain(); + }; + + pub const STYLE_RULE = 1; + pub const CHARSET_RULE = 2; + pub const IMPORT_RULE = 3; + pub const MEDIA_RULE = 4; + pub const FONT_FACE_RULE = 5; + pub const PAGE_RULE = 6; + pub const KEYFRAMES_RULE = 7; + pub const KEYFRAME_RULE = 8; + pub const MARGIN_RULE = 9; + pub const NAMESPACE_RULE = 10; + pub const COUNTER_STYLE_RULE = 11; + pub const SUPPORTS_RULE = 12; + pub const DOCUMENT_RULE = 13; + pub const FONT_FEATURE_VALUES_RULE = 14; + pub const VIEWPORT_RULE = 15; + pub const REGION_STYLE_RULE = 16; + + pub const @"type" = bridge.accessor(CSSRule.getType, null, .{}); + pub const cssText = bridge.accessor(CSSRule.getCssText, CSSRule.setCssText, .{}); + pub const parentRule = bridge.accessor(CSSRule.getParentRule, null, .{}); + pub const parentStyleSheet = bridge.accessor(CSSRule.getParentStyleSheet, null, .{}); +}; diff --git a/src/browser/webapi/css/CSSRuleList.zig b/src/browser/webapi/css/CSSRuleList.zig new file mode 100644 index 00000000..4a700237 --- /dev/null +++ b/src/browser/webapi/css/CSSRuleList.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const CSSRule = @import("CSSRule.zig"); + +const CSSRuleList = @This(); + +_rules: []*CSSRule = &.{}, + +pub fn init(page: *Page) !*CSSRuleList { + return page._factory.create(CSSRuleList{}); +} + +pub fn length(self: *const CSSRuleList) u32 { + return @intCast(self._rules.len); +} + +pub fn item(self: *const CSSRuleList, index: usize) ?*CSSRule { + if (index >= self._rules.len) { + return null; + } + return self._rules[index]; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSSRuleList); + + pub const Meta = struct { + pub const name = "CSSRuleList"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const length = bridge.accessor(CSSRuleList.length, null, .{}); + pub const @"[]" = bridge.indexed(CSSRuleList.item, .{ .null_as_undefined = true }); +}; diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig index 887a8098..536fa737 100644 --- a/src/browser/webapi/css/CSSStyleDeclaration.zig +++ b/src/browser/webapi/css/CSSStyleDeclaration.zig @@ -29,28 +29,6 @@ const CSSStyleDeclaration = @This(); _element: ?*Element = null, _properties: std.DoublyLinkedList = .{}, -pub const Property = struct { - _name: String, - _value: String, - _important: bool = false, - _node: std.DoublyLinkedList.Node, - - fn fromNodeLink(n: *std.DoublyLinkedList.Node) *Property { - return @alignCast(@fieldParentPtr("_node", n)); - } - - pub fn format(self: *const Property, writer: *std.Io.Writer) !void { - try self._name.format(writer); - try writer.writeAll(": "); - try self._value.format(writer); - - if (self._important) { - try writer.writeAll(" !important"); - } - try writer.writeByte(';'); - } -}; - pub fn init(element: ?*Element, page: *Page) !*CSSStyleDeclaration { return page._factory.create(CSSStyleDeclaration{ ._element = element, @@ -214,6 +192,28 @@ fn normalizePropertyName(name: []const u8, buf: []u8) []const u8 { return std.ascii.lowerString(buf, name); } +pub const Property = struct { + _name: String, + _value: String, + _important: bool = false, + _node: std.DoublyLinkedList.Node, + + fn fromNodeLink(n: *std.DoublyLinkedList.Node) *Property { + return @alignCast(@fieldParentPtr("_node", n)); + } + + pub fn format(self: *const Property, writer: *std.Io.Writer) !void { + try self._name.format(writer); + try writer.writeAll(": "); + try self._value.format(writer); + + if (self._important) { + try writer.writeAll(" !important"); + } + try writer.writeByte(';'); + } +}; + pub const JsApi = struct { pub const bridge = js.Bridge(CSSStyleDeclaration); diff --git a/src/browser/webapi/css/CSSStyleProperties.zig b/src/browser/webapi/css/CSSStyleProperties.zig index f595838e..199d1214 100644 --- a/src/browser/webapi/css/CSSStyleProperties.zig +++ b/src/browser/webapi/css/CSSStyleProperties.zig @@ -72,7 +72,9 @@ fn isKnownCSSProperty(dash_case: []const u8) bool { } fn camelCaseToDashCase(name: []const u8, buf: []u8) []const u8 { - if (name.len == 0) return name; + if (name.len == 0) { + return name; + } // Special case: cssFloat -> float const lower_name = std.ascii.lowerString(buf, name); diff --git a/src/browser/webapi/css/CSSStyleRule.zig b/src/browser/webapi/css/CSSStyleRule.zig new file mode 100644 index 00000000..c477621c --- /dev/null +++ b/src/browser/webapi/css/CSSStyleRule.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const CSSRule = @import("CSSRule.zig"); +const CSSStyleDeclaration = @import("CSSStyleDeclaration.zig"); + +const CSSStyleRule = @This(); + +_proto: *CSSRule, +_selector_text: []const u8 = "", +_style: ?*CSSStyleDeclaration = null, + +pub fn init(page: *Page) !*CSSStyleRule { + const rule = try CSSRule.init(.style, page); + return page._factory.create(CSSStyleRule{ + ._proto = rule, + }); +} + +pub fn getSelectorText(self: *const CSSStyleRule) []const u8 { + return self._selector_text; +} + +pub fn setSelectorText(self: *CSSStyleRule, text: []const u8, page: *Page) !void { + self._selector_text = try page.dupeString(text); +} + +pub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleDeclaration { + if (self._style) |style| { + return style; + } + const style = try CSSStyleDeclaration.init(null, page); + self._style = style; + return style; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSSStyleRule); + + pub const Meta = struct { + pub const name = "CSSStyleRule"; + pub const prototype_chain = bridge.prototypeChain(CSSRule); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{}); + pub const style = bridge.accessor(CSSStyleRule.getStyle, null, .{}); +}; diff --git a/src/browser/webapi/css/CSSStyleSheet.zig b/src/browser/webapi/css/CSSStyleSheet.zig new file mode 100644 index 00000000..a377618d --- /dev/null +++ b/src/browser/webapi/css/CSSStyleSheet.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const CSSRuleList = @import("CSSRuleList.zig"); +const CSSRule = @import("CSSRule.zig"); + +const CSSStyleSheet = @This(); + +_href: ?[]const u8 = null, +_title: []const u8 = "", +_disabled: bool = false, +_css_rules: ?*CSSRuleList = null, +_owner_rule: ?*CSSRule = null, + +pub fn init(page: *Page) !*CSSStyleSheet { + return page._factory.create(CSSStyleSheet{}); +} + +pub fn getOwnerNode(self: *const CSSStyleSheet) ?*CSSStyleSheet { + _ = self; + return null; +} + +pub fn getHref(self: *const CSSStyleSheet) ?[]const u8 { + return self._href; +} + +pub fn getTitle(self: *const CSSStyleSheet) []const u8 { + return self._title; +} + +pub fn getDisabled(self: *const CSSStyleSheet) bool { + return self._disabled; +} + +pub fn setDisabled(self: *CSSStyleSheet, disabled: bool) void { + self._disabled = disabled; +} + +pub fn getCssRules(self: *CSSStyleSheet, page: *Page) !*CSSRuleList { + if (self._css_rules) |rules| return rules; + const rules = try CSSRuleList.init(page); + self._css_rules = rules; + return rules; +} + +pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule { + return self._owner_rule; +} + +pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Page) !u32 { + _ = self; + _ = rule; + _ = index; + _ = page; + return 0; +} + +pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void { + _ = self; + _ = index; + _ = page; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(CSSStyleSheet); + + pub const Meta = struct { + pub const name = "CSSStyleSheet"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const ownerNode = bridge.accessor(CSSStyleSheet.getOwnerNode, null, .{ .null_as_undefined = true }); + pub const href = bridge.accessor(CSSStyleSheet.getHref, null, .{ .null_as_undefined = true }); + pub const title = bridge.accessor(CSSStyleSheet.getTitle, null, .{}); + pub const disabled = bridge.accessor(CSSStyleSheet.getDisabled, CSSStyleSheet.setDisabled, .{}); + pub const cssRules = bridge.accessor(CSSStyleSheet.getCssRules, null, .{}); + pub const ownerRule = bridge.accessor(CSSStyleSheet.getOwnerRule, null, .{ .null_as_undefined = true }); + pub const insertRule = bridge.function(CSSStyleSheet.insertRule, .{}); + pub const deleteRule = bridge.function(CSSStyleSheet.deleteRule, .{}); +}; + +const testing = @import("../../../testing.zig"); +test "WebApi: CSSStyleSheet" { + try testing.htmlRunner("css/stylesheet.html", .{}); +} diff --git a/src/browser/webapi/css/StyleSheetList.zig b/src/browser/webapi/css/StyleSheetList.zig new file mode 100644 index 00000000..8a019a18 --- /dev/null +++ b/src/browser/webapi/css/StyleSheetList.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const CSSStyleSheet = @import("CSSStyleSheet.zig"); + +const StyleSheetList = @This(); + +_sheets: []*CSSStyleSheet = &.{}, + +pub fn init(page: *Page) !*StyleSheetList { + return page._factory.create(StyleSheetList{}); +} + +pub fn length(self: *const StyleSheetList) u32 { + return @intCast(self._sheets.len); +} + +pub fn item(self: *const StyleSheetList, index: usize) ?*CSSStyleSheet { + if (index >= self._sheets.len) return null; + return self._sheets[index]; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(StyleSheetList); + + pub const Meta = struct { + pub const name = "StyleSheetList"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const length = bridge.accessor(StyleSheetList.length, null, .{}); + pub const @"[]" = bridge.indexed(StyleSheetList.item, .{ .null_as_undefined = true }); +}; diff --git a/src/browser/webapi/selector/Parser.zig b/src/browser/webapi/selector/Parser.zig index b97f7c00..a793e7c8 100644 --- a/src/browser/webapi/selector/Parser.zig +++ b/src/browser/webapi/selector/Parser.zig @@ -1454,4 +1454,3 @@ test "Selector: Parser.parseNthPattern" { try testing.expectEqual(" )", parser.input); } } - diff --git a/src/cdp/domains/log.zig b/src/cdp/domains/log.zig index 66b8b79f..2eca6847 100644 --- a/src/cdp/domains/log.zig +++ b/src/cdp/domains/log.zig @@ -88,7 +88,7 @@ pub fn LogInterceptor(comptime BC: type) type { self.bc.cdp.sendEvent("Log.entryAdded", .{ .entry = .{ .source = switch (scope) { - .js, .console => "javascript", + .js, .console => "javascript", .http => "network", .telemetry, .unknown_prop, .interceptor => unreachable, // filtered out in writer above else => "other", diff --git a/src/html5ever/lib.rs b/src/html5ever/lib.rs index 992b00fd..69f6b399 100644 --- a/src/html5ever/lib.rs +++ b/src/html5ever/lib.rs @@ -184,6 +184,7 @@ pub extern "C" fn html5ever_get_memory_usage() -> Memory { // Streaming parser API // The Parser type from html5ever implements TendrilSink and supports streaming pub struct StreamingParser { + #[allow(dead_code)] arena: Box>, parser: Box, }