Skip to content

Commit 02fe46d

Browse files
authored
Merge pull request #915 from lightpanda-io/css_tweaks
Tweak cssom
2 parents ab2fd0a + 8a0c490 commit 02fe46d

15 files changed

+1511
-1509
lines changed

src/browser/State.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ const Env = @import("env.zig").Env;
3030
const parser = @import("netsurf.zig");
3131
const DataSet = @import("html/DataSet.zig");
3232
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
33-
const StyleSheet = @import("cssom/stylesheet.zig").StyleSheet;
34-
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
33+
const StyleSheet = @import("cssom/StyleSheet.zig");
34+
const CSSStyleDeclaration = @import("cssom/CSSStyleDeclaration.zig");
3535

3636
// for HTMLScript (but probably needs to be added to more)
3737
onload: ?Env.Function = null,

src/browser/cssom/CSSParser.zig

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
const Allocator = std.mem.Allocator;
21+
22+
const CSSConstants = struct {
23+
const IMPORTANT = "!important";
24+
const URL_PREFIX = "url(";
25+
};
26+
27+
const CSSParserState = enum {
28+
seek_name,
29+
in_name,
30+
seek_colon,
31+
seek_value,
32+
in_value,
33+
in_quoted_value,
34+
in_single_quoted_value,
35+
in_url,
36+
in_important,
37+
};
38+
39+
const CSSDeclaration = struct {
40+
name: []const u8,
41+
value: []const u8,
42+
is_important: bool,
43+
};
44+
45+
const CSSParser = @This();
46+
state: CSSParserState,
47+
name_start: usize,
48+
name_end: usize,
49+
value_start: usize,
50+
position: usize,
51+
paren_depth: usize,
52+
escape_next: bool,
53+
54+
pub fn init() CSSParser {
55+
return .{
56+
.state = .seek_name,
57+
.name_start = 0,
58+
.name_end = 0,
59+
.value_start = 0,
60+
.position = 0,
61+
.paren_depth = 0,
62+
.escape_next = false,
63+
};
64+
}
65+
66+
pub fn parseDeclarations(arena: Allocator, text: []const u8) ![]CSSDeclaration {
67+
var parser = init();
68+
var declarations: std.ArrayListUnmanaged(CSSDeclaration) = .empty;
69+
70+
while (parser.position < text.len) {
71+
const c = text[parser.position];
72+
73+
switch (parser.state) {
74+
.seek_name => {
75+
if (!std.ascii.isWhitespace(c)) {
76+
parser.name_start = parser.position;
77+
parser.state = .in_name;
78+
continue;
79+
}
80+
},
81+
.in_name => {
82+
if (c == ':') {
83+
parser.name_end = parser.position;
84+
parser.state = .seek_value;
85+
} else if (std.ascii.isWhitespace(c)) {
86+
parser.name_end = parser.position;
87+
parser.state = .seek_colon;
88+
}
89+
},
90+
.seek_colon => {
91+
if (c == ':') {
92+
parser.state = .seek_value;
93+
} else if (!std.ascii.isWhitespace(c)) {
94+
parser.state = .seek_name;
95+
continue;
96+
}
97+
},
98+
.seek_value => {
99+
if (!std.ascii.isWhitespace(c)) {
100+
parser.value_start = parser.position;
101+
if (c == '"') {
102+
parser.state = .in_quoted_value;
103+
} else if (c == '\'') {
104+
parser.state = .in_single_quoted_value;
105+
} else if (c == 'u' and parser.position + CSSConstants.URL_PREFIX.len <= text.len and std.mem.startsWith(u8, text[parser.position..], CSSConstants.URL_PREFIX)) {
106+
parser.state = .in_url;
107+
parser.paren_depth = 1;
108+
parser.position += 3;
109+
} else {
110+
parser.state = .in_value;
111+
continue;
112+
}
113+
}
114+
},
115+
.in_value => {
116+
if (parser.escape_next) {
117+
parser.escape_next = false;
118+
} else if (c == '\\') {
119+
parser.escape_next = true;
120+
} else if (c == '(') {
121+
parser.paren_depth += 1;
122+
} else if (c == ')' and parser.paren_depth > 0) {
123+
parser.paren_depth -= 1;
124+
} else if (c == ';' and parser.paren_depth == 0) {
125+
try parser.finishDeclaration(arena, &declarations, text);
126+
parser.state = .seek_name;
127+
}
128+
},
129+
.in_quoted_value => {
130+
if (parser.escape_next) {
131+
parser.escape_next = false;
132+
} else if (c == '\\') {
133+
parser.escape_next = true;
134+
} else if (c == '"') {
135+
parser.state = .in_value;
136+
}
137+
},
138+
.in_single_quoted_value => {
139+
if (parser.escape_next) {
140+
parser.escape_next = false;
141+
} else if (c == '\\') {
142+
parser.escape_next = true;
143+
} else if (c == '\'') {
144+
parser.state = .in_value;
145+
}
146+
},
147+
.in_url => {
148+
if (parser.escape_next) {
149+
parser.escape_next = false;
150+
} else if (c == '\\') {
151+
parser.escape_next = true;
152+
} else if (c == '(') {
153+
parser.paren_depth += 1;
154+
} else if (c == ')') {
155+
parser.paren_depth -= 1;
156+
if (parser.paren_depth == 0) {
157+
parser.state = .in_value;
158+
}
159+
}
160+
},
161+
.in_important => {},
162+
}
163+
164+
parser.position += 1;
165+
}
166+
167+
try parser.finalize(arena, &declarations, text);
168+
169+
return declarations.items;
170+
}
171+
172+
fn finishDeclaration(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
173+
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
174+
if (name.len == 0) return;
175+
176+
const raw_value = text[self.value_start..self.position];
177+
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
178+
179+
var final_value = value;
180+
var is_important = false;
181+
182+
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT)) {
183+
is_important = true;
184+
final_value = std.mem.trimRight(u8, value[0 .. value.len - CSSConstants.IMPORTANT.len], &std.ascii.whitespace);
185+
}
186+
187+
try declarations.append(arena, .{
188+
.name = name,
189+
.value = final_value,
190+
.is_important = is_important,
191+
});
192+
}
193+
194+
fn finalize(self: *CSSParser, arena: Allocator, declarations: *std.ArrayListUnmanaged(CSSDeclaration), text: []const u8) !void {
195+
if (self.state != .in_value) {
196+
return;
197+
}
198+
return self.finishDeclaration(arena, declarations, text);
199+
}
200+
201+
const testing = @import("../../testing.zig");
202+
test "CSSParser - Simple property" {
203+
defer testing.reset();
204+
205+
const text = "color: red;";
206+
const allocator = testing.arena_allocator;
207+
208+
const declarations = try CSSParser.parseDeclarations(allocator, text);
209+
210+
try testing.expectEqual(1, declarations.len);
211+
try testing.expectEqual("color", declarations[0].name);
212+
try testing.expectEqual("red", declarations[0].value);
213+
try testing.expectEqual(false, declarations[0].is_important);
214+
}
215+
216+
test "CSSParser - Property with !important" {
217+
defer testing.reset();
218+
const text = "margin: 10px !important;";
219+
const allocator = testing.arena_allocator;
220+
221+
const declarations = try CSSParser.parseDeclarations(allocator, text);
222+
223+
try testing.expectEqual(1, declarations.len);
224+
try testing.expectEqual("margin", declarations[0].name);
225+
try testing.expectEqual("10px", declarations[0].value);
226+
try testing.expectEqual(true, declarations[0].is_important);
227+
}
228+
229+
test "CSSParser - Multiple properties" {
230+
defer testing.reset();
231+
const text = "color: red; font-size: 12px; margin: 5px !important;";
232+
const allocator = testing.arena_allocator;
233+
234+
const declarations = try CSSParser.parseDeclarations(allocator, text);
235+
236+
try testing.expect(declarations.len == 3);
237+
238+
try testing.expectEqual("color", declarations[0].name);
239+
try testing.expectEqual("red", declarations[0].value);
240+
try testing.expectEqual(false, declarations[0].is_important);
241+
242+
try testing.expectEqual("font-size", declarations[1].name);
243+
try testing.expectEqual("12px", declarations[1].value);
244+
try testing.expectEqual(false, declarations[1].is_important);
245+
246+
try testing.expectEqual("margin", declarations[2].name);
247+
try testing.expectEqual("5px", declarations[2].value);
248+
try testing.expectEqual(true, declarations[2].is_important);
249+
}
250+
251+
test "CSSParser - Quoted value with semicolon" {
252+
defer testing.reset();
253+
const text = "content: \"Hello; world!\";";
254+
const allocator = testing.arena_allocator;
255+
256+
const declarations = try CSSParser.parseDeclarations(allocator, text);
257+
258+
try testing.expectEqual(1, declarations.len);
259+
try testing.expectEqual("content", declarations[0].name);
260+
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
261+
try testing.expectEqual(false, declarations[0].is_important);
262+
}
263+
264+
test "CSSParser - URL value" {
265+
defer testing.reset();
266+
const text = "background-image: url(\"test.png\");";
267+
const allocator = testing.arena_allocator;
268+
269+
const declarations = try CSSParser.parseDeclarations(allocator, text);
270+
271+
try testing.expectEqual(1, declarations.len);
272+
try testing.expectEqual("background-image", declarations[0].name);
273+
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
274+
try testing.expectEqual(false, declarations[0].is_important);
275+
}
276+
277+
test "CSSParser - Whitespace handling" {
278+
defer testing.reset();
279+
const text = " color : purple ; margin : 10px ; ";
280+
const allocator = testing.arena_allocator;
281+
282+
const declarations = try CSSParser.parseDeclarations(allocator, text);
283+
284+
try testing.expectEqual(2, declarations.len);
285+
try testing.expectEqual("color", declarations[0].name);
286+
try testing.expectEqual("purple", declarations[0].value);
287+
try testing.expectEqual("margin", declarations[1].name);
288+
try testing.expectEqual("10px", declarations[1].value);
289+
}

src/browser/cssom/css_rule.zig renamed to src/browser/cssom/CSSRule.zig

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,18 @@
1818

1919
const std = @import("std");
2020

21-
const CSSStyleSheet = @import("css_stylesheet.zig").CSSStyleSheet;
21+
const CSSStyleSheet = @import("CSSStyleSheet.zig");
2222

2323
pub const Interfaces = .{
2424
CSSRule,
2525
CSSImportRule,
2626
};
2727

2828
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
29-
pub const CSSRule = struct {
30-
css_text: []const u8,
31-
parent_rule: ?*CSSRule = null,
32-
parent_stylesheet: ?*CSSStyleSheet = null,
33-
};
29+
const CSSRule = @This();
30+
css_text: []const u8,
31+
parent_rule: ?*CSSRule = null,
32+
parent_stylesheet: ?*CSSStyleSheet = null,
3433

3534
pub const CSSImportRule = struct {
3635
pub const prototype = *CSSRule;

src/browser/cssom/css_rule_list.zig renamed to src/browser/cssom/CSSRuleList.zig

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,33 @@
1818

1919
const std = @import("std");
2020

21-
const StyleSheet = @import("stylesheet.zig").StyleSheet;
22-
const CSSRule = @import("css_rule.zig").CSSRule;
23-
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
21+
const CSSRule = @import("CSSRule.zig");
22+
const StyleSheet = @import("StyleSheet.zig").StyleSheet;
2423

25-
pub const CSSRuleList = struct {
26-
list: std.ArrayListUnmanaged([]const u8),
24+
const CSSImportRule = CSSRule.CSSImportRule;
2725

28-
pub fn constructor() CSSRuleList {
29-
return .{ .list = .empty };
30-
}
26+
const CSSRuleList = @This();
27+
list: std.ArrayListUnmanaged([]const u8),
3128

32-
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
33-
const index: usize = @intCast(_index);
29+
pub fn constructor() CSSRuleList {
30+
return .{ .list = .empty };
31+
}
3432

35-
if (index > self.list.items.len) {
36-
return null;
37-
}
33+
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
34+
const index: usize = @intCast(_index);
3835

39-
// todo: for now, just return null.
40-
// this depends on properly parsing CSSRule
36+
if (index > self.list.items.len) {
4137
return null;
4238
}
4339

44-
pub fn get_length(self: *CSSRuleList) u32 {
45-
return @intCast(self.list.items.len);
46-
}
47-
};
40+
// todo: for now, just return null.
41+
// this depends on properly parsing CSSRule
42+
return null;
43+
}
44+
45+
pub fn get_length(self: *CSSRuleList) u32 {
46+
return @intCast(self.list.items.len);
47+
}
4848

4949
const testing = @import("../../testing.zig");
5050
test "Browser.CSS.CSSRuleList" {

0 commit comments

Comments
 (0)