mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 12:44:43 +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,
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
_rules: []*CSSRule = &.{},
|
||||
_rules: std.ArrayListUnmanaged(*CSSRule) = .{},
|
||||
|
||||
pub fn init(page: *Page) !*CSSRuleList {
|
||||
return page._factory.create(CSSRuleList{});
|
||||
}
|
||||
|
||||
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 {
|
||||
if (index >= self._rules.len) {
|
||||
if (index >= self._rules.items.len) {
|
||||
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 {
|
||||
|
||||
@@ -34,6 +34,21 @@ pub fn getStyle(self: *CSSStyleRule, page: *Page) !*CSSStyleDeclaration {
|
||||
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 bridge = js.Bridge(CSSStyleRule);
|
||||
|
||||
@@ -45,4 +60,5 @@ pub const JsApi = struct {
|
||||
|
||||
pub const selectorText = bridge.accessor(CSSStyleRule.getSelectorText, CSSStyleRule.setSelectorText, .{});
|
||||
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 CSSRuleList = @import("CSSRuleList.zig");
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
const CSSStyleRule = @import("CSSStyleRule.zig");
|
||||
const Parser = @import("../../css/Parser.zig");
|
||||
|
||||
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 {
|
||||
_ = self;
|
||||
_ = rule;
|
||||
_ = index;
|
||||
_ = page;
|
||||
return 0;
|
||||
var it = Parser.parseStylesheet(rule);
|
||||
const parsed_rule = it.next() orelse return error.SyntaxError;
|
||||
|
||||
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);
|
||||
|
||||
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 {
|
||||
_ = self;
|
||||
_ = index;
|
||||
_ = page;
|
||||
const rules = try self.getCssRules(page);
|
||||
rules.remove(index);
|
||||
}
|
||||
|
||||
pub fn replace(self: *CSSStyleSheet, text: []const u8, page: *Page) !js.Promise {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
try self.replaceSync(text, page);
|
||||
return page.js.local.?.resolvePromise({});
|
||||
}
|
||||
|
||||
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8) !void {
|
||||
_ = self;
|
||||
_ = text;
|
||||
// TODO: clear self.css_rules
|
||||
pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, page: *Page) !void {
|
||||
const rules = try self.getCssRules(page);
|
||||
rules.clear();
|
||||
|
||||
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 {
|
||||
|
||||
@@ -94,6 +94,10 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {
|
||||
if (self._sheet) |sheet| return sheet;
|
||||
const sheet = try CSSStyleSheet.initWithOwner(self.asElement(), page);
|
||||
self._sheet = sheet;
|
||||
|
||||
const text = try self.asNode().getTextContentAlloc(page.call_arena);
|
||||
try sheet.replaceSync(text, page);
|
||||
|
||||
return sheet;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user