Skip to content

Commit 913568a

Browse files
committed
Added support for CSSStyleDeclaration API
1 parent 6506fa7 commit 913568a

File tree

5 files changed

+882
-5
lines changed

5 files changed

+882
-5
lines changed

src/browser/cssom/css_parser.zig

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
const std = @import("std");
2+
3+
const CSSConstants = struct {
4+
const IMPORTANT_KEYWORD = "!important";
5+
const IMPORTANT_LENGTH = IMPORTANT_KEYWORD.len;
6+
const URL_PREFIX = "url(";
7+
const URL_PREFIX_LENGTH = URL_PREFIX.len;
8+
};
9+
10+
pub const CSSParserState = enum {
11+
seekName,
12+
inName,
13+
seekColon,
14+
seekValue,
15+
inValue,
16+
inQuotedValue,
17+
inSingleQuotedValue,
18+
inUrl,
19+
inImportant,
20+
};
21+
22+
pub const CSSDeclaration = struct {
23+
name: []const u8,
24+
value: []const u8,
25+
is_important: bool,
26+
27+
pub fn init(name: []const u8, value: []const u8, is_important: bool) CSSDeclaration {
28+
return .{
29+
.name = name,
30+
.value = value,
31+
.is_important = is_important,
32+
};
33+
}
34+
};
35+
36+
pub const CSSParser = struct {
37+
state: CSSParserState,
38+
name_start: usize,
39+
name_end: usize,
40+
value_start: usize,
41+
position: usize,
42+
paren_depth: usize,
43+
escape_next: bool,
44+
45+
pub fn init() CSSParser {
46+
return .{
47+
.state = .seekName,
48+
.name_start = 0,
49+
.name_end = 0,
50+
.value_start = 0,
51+
.position = 0,
52+
.paren_depth = 0,
53+
.escape_next = false,
54+
};
55+
}
56+
57+
pub fn parseDeclarations(text: []const u8, allocator: std.mem.Allocator) ![]CSSDeclaration {
58+
var parser = init();
59+
var declarations = std.ArrayList(CSSDeclaration).init(allocator);
60+
errdefer declarations.deinit();
61+
62+
while (parser.position < text.len) {
63+
const c = text[parser.position];
64+
65+
switch (parser.state) {
66+
.seekName => {
67+
if (!std.ascii.isWhitespace(c)) {
68+
parser.name_start = parser.position;
69+
parser.state = .inName;
70+
continue;
71+
}
72+
},
73+
.inName => {
74+
if (c == ':') {
75+
parser.name_end = parser.position;
76+
parser.state = .seekValue;
77+
} else if (std.ascii.isWhitespace(c)) {
78+
parser.name_end = parser.position;
79+
parser.state = .seekColon;
80+
}
81+
},
82+
.seekColon => {
83+
if (c == ':') {
84+
parser.state = .seekValue;
85+
} else if (!std.ascii.isWhitespace(c)) {
86+
parser.state = .seekName;
87+
continue;
88+
}
89+
},
90+
.seekValue => {
91+
if (!std.ascii.isWhitespace(c)) {
92+
parser.value_start = parser.position;
93+
if (c == '"') {
94+
parser.state = .inQuotedValue;
95+
} else if (c == '\'') {
96+
parser.state = .inSingleQuotedValue;
97+
} else if (c == 'u' and parser.position + 3 < text.len and
98+
std.mem.eql(u8, text[parser.position .. parser.position + 4], CSSConstants.URL_PREFIX))
99+
{
100+
parser.state = .inUrl;
101+
parser.paren_depth = 1;
102+
parser.position += 3;
103+
} else {
104+
parser.state = .inValue;
105+
continue;
106+
}
107+
}
108+
},
109+
.inValue => {
110+
if (parser.escape_next) {
111+
parser.escape_next = false;
112+
} else if (c == '\\') {
113+
parser.escape_next = true;
114+
} else if (c == '(') {
115+
parser.paren_depth += 1;
116+
} else if (c == ')' and parser.paren_depth > 0) {
117+
parser.paren_depth -= 1;
118+
} else if (c == ';' and parser.paren_depth == 0) {
119+
try parser.finishDeclaration(text, &declarations);
120+
parser.state = .seekName;
121+
}
122+
},
123+
.inQuotedValue => {
124+
if (parser.escape_next) {
125+
parser.escape_next = false;
126+
} else if (c == '\\') {
127+
parser.escape_next = true;
128+
} else if (c == '"') {
129+
parser.state = .inValue;
130+
}
131+
},
132+
.inSingleQuotedValue => {
133+
if (parser.escape_next) {
134+
parser.escape_next = false;
135+
} else if (c == '\\') {
136+
parser.escape_next = true;
137+
} else if (c == '\'') {
138+
parser.state = .inValue;
139+
}
140+
},
141+
.inUrl => {
142+
if (parser.escape_next) {
143+
parser.escape_next = false;
144+
} else if (c == '\\') {
145+
parser.escape_next = true;
146+
} else if (c == '(') {
147+
parser.paren_depth += 1;
148+
} else if (c == ')') {
149+
parser.paren_depth -= 1;
150+
if (parser.paren_depth == 0) {
151+
parser.state = .inValue;
152+
}
153+
}
154+
},
155+
.inImportant => {},
156+
}
157+
158+
parser.position += 1;
159+
}
160+
161+
try parser.finalize(text, &declarations);
162+
163+
return declarations.toOwnedSlice();
164+
}
165+
166+
fn finishDeclaration(self: *CSSParser, text: []const u8, declarations: *std.ArrayList(CSSDeclaration)) !void {
167+
const name = std.mem.trim(u8, text[self.name_start..self.name_end], &std.ascii.whitespace);
168+
if (name.len == 0) return;
169+
170+
const raw_value = text[self.value_start..self.position];
171+
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
172+
173+
var final_value = value;
174+
var is_important = false;
175+
176+
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT_KEYWORD)) {
177+
is_important = true;
178+
final_value = std.mem.trim(u8, value[0 .. value.len - CSSConstants.IMPORTANT_LENGTH], &std.ascii.whitespace);
179+
}
180+
181+
const declaration = CSSDeclaration.init(name, final_value, is_important);
182+
try declarations.append(declaration);
183+
}
184+
185+
fn finalize(self: *CSSParser, text: []const u8, declarations: *std.ArrayList(CSSDeclaration)) !void {
186+
if (self.state == .inValue) {
187+
const name = text[self.name_start..self.name_end];
188+
const trimmed_name = std.mem.trim(u8, name, &std.ascii.whitespace);
189+
190+
if (trimmed_name.len > 0) {
191+
const raw_value = text[self.value_start..self.position];
192+
const value = std.mem.trim(u8, raw_value, &std.ascii.whitespace);
193+
194+
var final_value = value;
195+
var is_important = false;
196+
if (std.mem.endsWith(u8, value, CSSConstants.IMPORTANT_KEYWORD)) {
197+
is_important = true;
198+
final_value = std.mem.trim(u8, value[0 .. value.len - CSSConstants.IMPORTANT_LENGTH], &std.ascii.whitespace);
199+
}
200+
201+
const declaration = CSSDeclaration.init(trimmed_name, final_value, is_important);
202+
try declarations.append(declaration);
203+
}
204+
}
205+
}
206+
207+
pub fn getState(self: *const CSSParser) CSSParserState {
208+
return self.state;
209+
}
210+
211+
pub fn getPosition(self: *const CSSParser) usize {
212+
return self.position;
213+
}
214+
215+
pub fn reset(self: *CSSParser) void {
216+
self.* = init();
217+
}
218+
};
219+
220+
const testing = std.testing;
221+
222+
test "CSSParser - Simple property" {
223+
const text = "color: red;";
224+
const allocator = testing.allocator;
225+
226+
const declarations = try CSSParser.parseDeclarations(text, allocator);
227+
defer allocator.free(declarations);
228+
229+
try testing.expect(declarations.len == 1);
230+
try testing.expectEqualStrings("color", declarations[0].name);
231+
try testing.expectEqualStrings("red", declarations[0].value);
232+
try testing.expect(!declarations[0].is_important);
233+
}
234+
235+
test "CSSParser - Property with !important" {
236+
const text = "margin: 10px !important;";
237+
const allocator = testing.allocator;
238+
239+
const declarations = try CSSParser.parseDeclarations(text, allocator);
240+
defer allocator.free(declarations);
241+
242+
try testing.expect(declarations.len == 1);
243+
try testing.expectEqualStrings("margin", declarations[0].name);
244+
try testing.expectEqualStrings("10px", declarations[0].value);
245+
try testing.expect(declarations[0].is_important);
246+
}
247+
248+
test "CSSParser - Multiple properties" {
249+
const text = "color: red; font-size: 12px; margin: 5px !important;";
250+
const allocator = testing.allocator;
251+
252+
const declarations = try CSSParser.parseDeclarations(text, allocator);
253+
defer allocator.free(declarations);
254+
255+
try testing.expect(declarations.len == 3);
256+
257+
try testing.expectEqualStrings("color", declarations[0].name);
258+
try testing.expectEqualStrings("red", declarations[0].value);
259+
try testing.expect(!declarations[0].is_important);
260+
261+
try testing.expectEqualStrings("font-size", declarations[1].name);
262+
try testing.expectEqualStrings("12px", declarations[1].value);
263+
try testing.expect(!declarations[1].is_important);
264+
265+
try testing.expectEqualStrings("margin", declarations[2].name);
266+
try testing.expectEqualStrings("5px", declarations[2].value);
267+
try testing.expect(declarations[2].is_important);
268+
}
269+
270+
test "CSSParser - Quoted value with semicolon" {
271+
const text = "content: \"Hello; world!\";";
272+
const allocator = testing.allocator;
273+
274+
const declarations = try CSSParser.parseDeclarations(text, allocator);
275+
defer allocator.free(declarations);
276+
277+
try testing.expect(declarations.len == 1);
278+
try testing.expectEqualStrings("content", declarations[0].name);
279+
try testing.expectEqualStrings("\"Hello; world!\"", declarations[0].value);
280+
try testing.expect(!declarations[0].is_important);
281+
}
282+
283+
test "CSSParser - URL value" {
284+
const text = "background-image: url(\"test.png\");";
285+
const allocator = testing.allocator;
286+
287+
const declarations = try CSSParser.parseDeclarations(text, allocator);
288+
defer allocator.free(declarations);
289+
290+
try testing.expect(declarations.len == 1);
291+
try testing.expectEqualStrings("background-image", declarations[0].name);
292+
try testing.expectEqualStrings("url(\"test.png\")", declarations[0].value);
293+
try testing.expect(!declarations[0].is_important);
294+
}
295+
296+
test "CSSParser - Whitespace handling" {
297+
const text = " color : purple ; margin : 10px ; ";
298+
const allocator = testing.allocator;
299+
300+
const declarations = try CSSParser.parseDeclarations(text, allocator);
301+
defer allocator.free(declarations);
302+
303+
try testing.expect(declarations.len == 2);
304+
try testing.expectEqualStrings("color", declarations[0].name);
305+
try testing.expectEqualStrings("purple", declarations[0].value);
306+
try testing.expectEqualStrings("margin", declarations[1].name);
307+
try testing.expectEqualStrings("10px", declarations[1].value);
308+
}

0 commit comments

Comments
 (0)