mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
feat(css): implement stylesheet rule management
Adds a CSS rule parser and implements `insertRule`, `deleteRule`, and `replaceSync` in `CSSStyleSheet`. Also updates `CSSRuleList` to use dynamic storage and populates sheets from `<style>` elements.
This commit is contained in:
@@ -293,3 +293,143 @@ fn isBang(token: Tokenizer.Token) bool {
|
|||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const Rule = struct {
|
||||||
|
selector: []const u8,
|
||||||
|
block: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn parseStylesheet(input: []const u8) RulesIterator {
|
||||||
|
return RulesIterator.init(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RulesIterator = struct {
|
||||||
|
input: []const u8,
|
||||||
|
stream: TokenStream,
|
||||||
|
|
||||||
|
pub fn init(input: []const u8) RulesIterator {
|
||||||
|
return .{
|
||||||
|
.input = input,
|
||||||
|
.stream = TokenStream.init(input),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *RulesIterator) ?Rule {
|
||||||
|
var selector_start: ?usize = null;
|
||||||
|
var selector_end: ?usize = null;
|
||||||
|
|
||||||
|
// Skip leading trivia
|
||||||
|
while (self.stream.peek()) |peeked| {
|
||||||
|
if (!isWhitespaceOrComment(peeked.token)) break;
|
||||||
|
_ = self.stream.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const peeked = self.stream.peek() orelse return null;
|
||||||
|
|
||||||
|
if (isCurlyBlockStart(peeked.token)) {
|
||||||
|
if (selector_start == null) {
|
||||||
|
self.skipBlock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const open_brace = self.stream.next() orelse return null;
|
||||||
|
const block_start = open_brace.end;
|
||||||
|
var block_end = block_start;
|
||||||
|
|
||||||
|
var depth: usize = 1;
|
||||||
|
while (true) {
|
||||||
|
const span = self.stream.next() orelse {
|
||||||
|
block_end = self.input.len;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if (isCurlyBlockStart(span.token)) {
|
||||||
|
depth += 1;
|
||||||
|
} else if (isCurlyBlockEnd(span.token)) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth == 0) {
|
||||||
|
block_end = span.start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selector = self.input[selector_start.?..selector_end.?];
|
||||||
|
selector = std.mem.trim(u8, selector, &std.ascii.whitespace);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.selector = selector,
|
||||||
|
.block = self.input[block_start..block_end],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peeked.token == .at_keyword) {
|
||||||
|
self.skipAtRule();
|
||||||
|
selector_start = null;
|
||||||
|
selector_end = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse return null;
|
||||||
|
if (!isWhitespaceOrComment(span.token)) {
|
||||||
|
if (selector_start == null) selector_start = span.start;
|
||||||
|
selector_end = span.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipBlock(self: *RulesIterator) void {
|
||||||
|
const span = self.stream.next() orelse return;
|
||||||
|
if (!isCurlyBlockStart(span.token)) return;
|
||||||
|
|
||||||
|
var depth: usize = 1;
|
||||||
|
while (true) {
|
||||||
|
const next_span = self.stream.next() orelse return;
|
||||||
|
if (isCurlyBlockStart(next_span.token)) {
|
||||||
|
depth += 1;
|
||||||
|
} else if (isCurlyBlockEnd(next_span.token)) {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth == 0) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipAtRule(self: *RulesIterator) void {
|
||||||
|
_ = self.stream.next(); // consume @keyword
|
||||||
|
var depth: usize = 0;
|
||||||
|
var saw_block = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const peeked = self.stream.peek() orelse return;
|
||||||
|
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
|
||||||
|
_ = self.stream.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = self.stream.next() orelse return;
|
||||||
|
if (isWhitespaceOrComment(span.token)) continue;
|
||||||
|
|
||||||
|
if (isCurlyBlockStart(span.token)) {
|
||||||
|
depth += 1;
|
||||||
|
saw_block = true;
|
||||||
|
} else if (isCurlyBlockEnd(span.token)) {
|
||||||
|
if (depth > 0) depth -= 1;
|
||||||
|
if (saw_block and depth == 0) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn isCurlyBlockStart(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.curly_bracket_block => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isCurlyBlockEnd(token: Tokenizer.Token) bool {
|
||||||
|
return switch (token) {
|
||||||
|
.close_curly_bracket => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,21 +5,39 @@ const CSSRule = @import("CSSRule.zig");
|
|||||||
|
|
||||||
const CSSRuleList = @This();
|
const CSSRuleList = @This();
|
||||||
|
|
||||||
_rules: []*CSSRule = &.{},
|
_rules: std.ArrayListUnmanaged(*CSSRule) = .{},
|
||||||
|
|
||||||
pub fn init(page: *Page) !*CSSRuleList {
|
pub fn init(page: *Page) !*CSSRuleList {
|
||||||
return page._factory.create(CSSRuleList{});
|
return page._factory.create(CSSRuleList{});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn length(self: *const CSSRuleList) u32 {
|
pub fn length(self: *const CSSRuleList) u32 {
|
||||||
return @intCast(self._rules.len);
|
return @intCast(self._rules.items.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item(self: *const CSSRuleList, index: usize) ?*CSSRule {
|
pub fn item(self: *const CSSRuleList, index: usize) ?*CSSRule {
|
||||||
if (index >= self._rules.len) {
|
if (index >= self._rules.items.len) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return self._rules[index];
|
return self._rules.items[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(self: *CSSRuleList, index: u32, rule: *CSSRule, page: *Page) !void {
|
||||||
|
if (index > self._rules.items.len) {
|
||||||
|
return error.IndexSizeError; // Or standard DOMException mapped error
|
||||||
|
}
|
||||||
|
try self._rules.insert(page.arena, index, rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(self: *CSSRuleList, index: u32) void {
|
||||||
|
if (index >= self._rules.items.len) {
|
||||||
|
return; // Ignore or throw? Standard says IndexSizeError DOMException, but we might just no-op or return an error depending on the caller.
|
||||||
|
}
|
||||||
|
_ = self._rules.orderedRemove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(self: *CSSRuleList) void {
|
||||||
|
self._rules.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
|
|||||||
@@ -34,6 +34,21 @@ pub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleDeclaration {
|
|||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getCssText(self: *CSSStyleRule, page: *Page) ![]const u8 {
|
||||||
|
const style = try self.getStyle(page);
|
||||||
|
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||||
|
try buf.writer.print("{s} {{ ", .{self._selector_text});
|
||||||
|
try style.format(&buf.writer);
|
||||||
|
try buf.writer.writeAll(" }");
|
||||||
|
return buf.written();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setCssText(self: *CSSStyleRule, text: []const u8, page: *Page) !void {
|
||||||
|
_ = self;
|
||||||
|
_ = text;
|
||||||
|
_ = page;
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(CSSStyleRule);
|
pub const bridge = js.Bridge(CSSStyleRule);
|
||||||
|
|
||||||
@@ -45,4 +60,5 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{});
|
pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{});
|
||||||
pub const style = bridge.accessor(CSSStyleRule.getStyle, null, .{});
|
pub const style = bridge.accessor(CSSStyleRule.getStyle, null, .{});
|
||||||
|
pub const cssText = bridge.accessor(CSSStyleRule.getCssText, CSSStyleRule.setCssText, .{});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const Page = @import("../../Page.zig");
|
|||||||
const Element = @import("../Element.zig");
|
const Element = @import("../Element.zig");
|
||||||
const CSSRuleList = @import("CSSRuleList.zig");
|
const CSSRuleList = @import("CSSRuleList.zig");
|
||||||
const CSSRule = @import("CSSRule.zig");
|
const CSSRule = @import("CSSRule.zig");
|
||||||
|
const CSSStyleRule = @import("CSSStyleRule.zig");
|
||||||
|
const Parser = @import("../../css/Parser.zig");
|
||||||
|
|
||||||
const CSSStyleSheet = @This();
|
const CSSStyleSheet = @This();
|
||||||
|
|
||||||
@@ -54,30 +56,46 @@ pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Page) !u32 {
|
pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, index: u32, page: *Page) !u32 {
|
||||||
_ = self;
|
var it = Parser.parseStylesheet(rule);
|
||||||
_ = rule;
|
const parsed_rule = it.next() orelse return error.SyntaxError;
|
||||||
_ = index;
|
|
||||||
_ = page;
|
const style_rule = try CSSStyleRule.init(page);
|
||||||
return 0;
|
try style_rule.setSelectorText(parsed_rule.selector, page);
|
||||||
|
|
||||||
|
const style = try style_rule.getStyle(page);
|
||||||
|
try style.setCssText(parsed_rule.block, page);
|
||||||
|
|
||||||
|
const rules = try self.getCssRules(page);
|
||||||
|
try rules.insert(index, style_rule._proto, page);
|
||||||
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {
|
pub fn deleteRule(self: *CSSStyleSheet, index: u32, page: *Page) !void {
|
||||||
_ = self;
|
const rules = try self.getCssRules(page);
|
||||||
_ = index;
|
rules.remove(index);
|
||||||
_ = page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
|
pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
|
||||||
_ = self;
|
try self.replaceSync(text, page);
|
||||||
_ = text;
|
|
||||||
// TODO: clear self.css_rules
|
|
||||||
return page.js.local.?.resolvePromise({});
|
return page.js.local.?.resolvePromise({});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) !void {
|
||||||
_ = self;
|
const rules = try self.getCssRules(page);
|
||||||
_ = text;
|
rules.clear();
|
||||||
// TODO: clear self.css_rules
|
|
||||||
|
var it = Parser.parseStylesheet(text);
|
||||||
|
var index: u32 = 0;
|
||||||
|
while (it.next()) |parsed_rule| {
|
||||||
|
const style_rule = try CSSStyleRule.init(page);
|
||||||
|
try style_rule.setSelectorText(parsed_rule.selector, page);
|
||||||
|
|
||||||
|
const style = try style_rule.getStyle(page);
|
||||||
|
try style.setCssText(parsed_rule.block, page);
|
||||||
|
|
||||||
|
try rules.insert(index, style_rule._proto, page);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {
|
|||||||
if (self._sheet) |sheet| return sheet;
|
if (self._sheet) |sheet| return sheet;
|
||||||
const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page);
|
const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page);
|
||||||
self._sheet = sheet;
|
self._sheet = sheet;
|
||||||
|
|
||||||
|
const text = try self.asNode().getTextContentAlloc(page.call_arena);
|
||||||
|
try sheet.replaceSync(text, page);
|
||||||
|
|
||||||
return sheet;
|
return sheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user