Skip to content

Commit d2c13ed

Browse files
authored
Merge pull request #680 from lightpanda-io/css_style_declaration
CSSStyleDeclaration implementation
2 parents 7cc332a + bed394d commit d2c13ed

File tree

5 files changed

+1345
-5
lines changed

5 files changed

+1345
-5
lines changed

src/browser/cssom/css_parser.zig

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
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+
pub 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+
pub const CSSDeclaration = struct {
40+
name: []const u8,
41+
value: []const u8,
42+
is_important: bool,
43+
};
44+
45+
pub const CSSParser = struct {
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+
202+
const testing = @import("../../testing.zig");
203+
204+
test "CSSParser - Simple property" {
205+
defer testing.reset();
206+
207+
const text = "color: red;";
208+
const allocator = testing.arena_allocator;
209+
210+
const declarations = try CSSParser.parseDeclarations(allocator, text);
211+
212+
try testing.expectEqual(1, declarations.len);
213+
try testing.expectEqual("color", declarations[0].name);
214+
try testing.expectEqual("red", declarations[0].value);
215+
try testing.expectEqual(false, declarations[0].is_important);
216+
}
217+
218+
test "CSSParser - Property with !important" {
219+
defer testing.reset();
220+
const text = "margin: 10px !important;";
221+
const allocator = testing.arena_allocator;
222+
223+
const declarations = try CSSParser.parseDeclarations(allocator, text);
224+
225+
try testing.expectEqual(1, declarations.len);
226+
try testing.expectEqual("margin", declarations[0].name);
227+
try testing.expectEqual("10px", declarations[0].value);
228+
try testing.expectEqual(true, declarations[0].is_important);
229+
}
230+
231+
test "CSSParser - Multiple properties" {
232+
defer testing.reset();
233+
const text = "color: red; font-size: 12px; margin: 5px !important;";
234+
const allocator = testing.arena_allocator;
235+
236+
const declarations = try CSSParser.parseDeclarations(allocator, text);
237+
238+
try testing.expect(declarations.len == 3);
239+
240+
try testing.expectEqual("color", declarations[0].name);
241+
try testing.expectEqual("red", declarations[0].value);
242+
try testing.expectEqual(false, declarations[0].is_important);
243+
244+
try testing.expectEqual("font-size", declarations[1].name);
245+
try testing.expectEqual("12px", declarations[1].value);
246+
try testing.expectEqual(false, declarations[1].is_important);
247+
248+
try testing.expectEqual("margin", declarations[2].name);
249+
try testing.expectEqual("5px", declarations[2].value);
250+
try testing.expectEqual(true, declarations[2].is_important);
251+
}
252+
253+
test "CSSParser - Quoted value with semicolon" {
254+
defer testing.reset();
255+
const text = "content: \"Hello; world!\";";
256+
const allocator = testing.arena_allocator;
257+
258+
const declarations = try CSSParser.parseDeclarations(allocator, text);
259+
260+
try testing.expectEqual(1, declarations.len);
261+
try testing.expectEqual("content", declarations[0].name);
262+
try testing.expectEqual("\"Hello; world!\"", declarations[0].value);
263+
try testing.expectEqual(false, declarations[0].is_important);
264+
}
265+
266+
test "CSSParser - URL value" {
267+
defer testing.reset();
268+
const text = "background-image: url(\"test.png\");";
269+
const allocator = testing.arena_allocator;
270+
271+
const declarations = try CSSParser.parseDeclarations(allocator, text);
272+
273+
try testing.expectEqual(1, declarations.len);
274+
try testing.expectEqual("background-image", declarations[0].name);
275+
try testing.expectEqual("url(\"test.png\")", declarations[0].value);
276+
try testing.expectEqual(false, declarations[0].is_important);
277+
}
278+
279+
test "CSSParser - Whitespace handling" {
280+
defer testing.reset();
281+
const text = " color : purple ; margin : 10px ; ";
282+
const allocator = testing.arena_allocator;
283+
284+
const declarations = try CSSParser.parseDeclarations(allocator, text);
285+
286+
try testing.expectEqual(2, declarations.len);
287+
try testing.expectEqual("color", declarations[0].name);
288+
try testing.expectEqual("purple", declarations[0].value);
289+
try testing.expectEqual("margin", declarations[1].name);
290+
try testing.expectEqual("10px", declarations[1].value);
291+
}

0 commit comments

Comments
 (0)