add some skeleton implementations for various CSS WebAPIs

This commit is contained in:
Karl Seguin
2025-11-25 13:00:32 +08:00
parent e336c67857
commit 218d08b1f6
16 changed files with 547 additions and 29 deletions

View File

@@ -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 {

View File

@@ -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"),

View File

@@ -0,0 +1,207 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id="document_styleSheets">
{
const sheets = document.styleSheets;
testing.expectTrue(sheets !== null);
testing.expectEqual(0, sheets.length);
// Should return same instance
const sheets2 = document.styleSheets;
testing.expectTrue(sheets === sheets2);
}
</script>
<script id="CSSStyleSheet_basic">
{
const sheets = document.styleSheets;
testing.expectEqual('StyleSheetList', sheets.constructor.name);
// Test indexed access on empty list
testing.expectEqual(undefined, sheets[0]);
testing.expectEqual(undefined, sheets[99]);
}
</script>
<script id="CSSStyleDeclaration_setCssText_basic">
{
const div = document.createElement('div');
const style = div.style;
// Set single property
style.cssText = 'color: red';
testing.expectEqual('red', style.getPropertyValue('color'));
testing.expectEqual(1, style.length);
}
</script>
<script id="CSSStyleDeclaration_setCssText_multiple">
{
const div = document.createElement('div');
const style = div.style;
// Set multiple properties
style.cssText = 'color: red; background: blue; margin: 10px';
testing.expectEqual('red', style.getPropertyValue('color'));
testing.expectEqual('blue', style.getPropertyValue('background'));
testing.expectEqual('10px', style.getPropertyValue('margin'));
testing.expectEqual(3, style.length);
}
</script>
<script id="CSSStyleDeclaration_setCssText_important">
{
const div = document.createElement('div');
const style = div.style;
// Set property with !important
style.cssText = 'color: red !important';
testing.expectEqual('red', style.getPropertyValue('color'));
testing.expectEqual('important', style.getPropertyPriority('color'));
}
</script>
<script id="CSSStyleDeclaration_setCssText_important_multiple">
{
const div = document.createElement('div');
const style = div.style;
// Mix of important and non-important
style.cssText = 'color: red !important; background: blue; margin: 10px !important';
testing.expectEqual('important', style.getPropertyPriority('color'));
testing.expectEqual('', style.getPropertyPriority('background'));
testing.expectEqual('important', style.getPropertyPriority('margin'));
}
</script>
<script id="CSSStyleDeclaration_setCssText_whitespace">
{
const div = document.createElement('div');
const style = div.style;
// Test whitespace handling
style.cssText = ' color : red ; background : blue ';
testing.expectEqual('red', style.getPropertyValue('color'));
testing.expectEqual('blue', style.getPropertyValue('background'));
}
</script>
<script id="CSSStyleDeclaration_setCssText_trailing_semicolon">
{
const div = document.createElement('div');
const style = div.style;
// Trailing semicolon should be handled
style.cssText = 'color: red;';
testing.expectEqual('red', style.getPropertyValue('color'));
testing.expectEqual(1, style.length);
}
</script>
<script id="CSSStyleDeclaration_setCssText_no_semicolon">
{
const div = document.createElement('div');
const style = div.style;
// Single property without semicolon
style.cssText = 'color: red';
testing.expectEqual('red', style.getPropertyValue('color'));
testing.expectEqual(1, style.length);
}
</script>
<script id="CSSStyleDeclaration_setCssText_empty_declarations">
{
const div = document.createElement('div');
const style = div.style;
// Multiple semicolons should be ignored
style.cssText = 'color: red;;; background: blue';
testing.expectEqual('red', style.getPropertyValue('color'));
testing.expectEqual('blue', style.getPropertyValue('background'));
testing.expectEqual(2, style.length);
}
</script>
<script id="CSSStyleDeclaration_setCssText_replace">
{
const div = document.createElement('div');
const style = div.style;
// Set initial properties
style.cssText = 'color: red; background: blue';
testing.expectEqual(2, style.length);
// Replace with new properties
style.cssText = 'margin: 10px';
testing.expectEqual('', style.getPropertyValue('color'));
testing.expectEqual('', style.getPropertyValue('background'));
testing.expectEqual('10px', style.getPropertyValue('margin'));
testing.expectEqual(1, style.length);
}
</script>
<script id="CSSStyleDeclaration_setCssText_clear">
{
const div = document.createElement('div');
const style = div.style;
style.cssText = 'color: red; background: blue';
testing.expectEqual(2, style.length);
// Clear all properties
style.cssText = '';
testing.expectEqual(0, style.length);
testing.expectEqual('', style.getPropertyValue('color'));
}
</script>
<script id="CSSStyleDeclaration_setCssText_colon_in_value">
{
const div = document.createElement('div');
const style = div.style;
// URL value with colon
style.cssText = 'background: url(http://example.com/image.png)';
testing.expectEqual('url(http://example.com/image.png)', style.getPropertyValue('background'));
}
</script>
<script id="CSSStyleDeclaration_setCssText_important_whitespace">
{
const div = document.createElement('div');
const style = div.style;
// Various whitespace around !important
style.cssText = 'color: red!important';
testing.expectEqual('important', style.getPropertyPriority('color'));
style.cssText = 'color: red ! important';
testing.expectEqual('important', style.getPropertyPriority('color'));
}
</script>
<script id="CSSStyleDeclaration_setCssText_case_insensitive_property">
{
const div = document.createElement('div');
const style = div.style;
// Property names should be normalized to lowercase
style.cssText = 'COLOR: red; BACKGROUND: blue';
testing.expectEqual('red', style.getPropertyValue('color'));
testing.expectEqual('blue', style.getPropertyValue('background'));
}
</script>
<script id="CSSStyleDeclaration_setCssText_exclamation_not_important">
{
const div = document.createElement('div');
const style = div.style;
// Exclamation mark without "important" should be kept in value
style.cssText = 'content: "hello!"';
testing.expectEqual('"hello!"', style.getPropertyValue('content'));
testing.expectEqual('', style.getPropertyPriority('content'));
}
</script>

View File

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

View File

@@ -120,4 +120,3 @@ pub const JsApi = struct {
// Methods
pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{});
};

View File

@@ -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,

View File

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

View File

@@ -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 });
};

View File

@@ -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);

View File

@@ -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);

View File

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

View File

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

View File

@@ -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 });
};

View File

@@ -1454,4 +1454,3 @@ test "Selector: Parser.parseNthPattern" {
try testing.expectEqual(" )", parser.input);
}
}

View File

@@ -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",

View File

@@ -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<typed_arena::Arena<sink::ElementData>>,
parser: Box<dyn std::any::Any>,
}