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+
119const std = @import ("std" );
220
321const CSSConstants = struct {
4- const IMPORTANT_KEYWORD = "!important" ;
5- const IMPORTANT_LENGTH = IMPORTANT_KEYWORD .len ;
22+ const IMPORTANT = "!important" ;
623 const URL_PREFIX = "url(" ;
7- const URL_PREFIX_LENGTH = URL_PREFIX .len ;
824};
925
1026pub const CSSParserState = enum {
11- seekName ,
12- inName ,
13- seekColon ,
14- seekValue ,
15- inValue ,
16- inQuotedValue ,
17- inSingleQuotedValue ,
18- inUrl ,
19- inImportant ,
27+ seek_name ,
28+ in_name ,
29+ seek_colon ,
30+ seek_value ,
31+ in_value ,
32+ in_quoted_value ,
33+ in_single_quoted_value ,
34+ in_url ,
35+ in_important ,
2036};
2137
2238pub const CSSDeclaration = struct {
2339 name : []const u8 ,
2440 value : []const u8 ,
2541 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- }
3442};
3543
3644pub const CSSParser = struct {
@@ -44,7 +52,7 @@ pub const CSSParser = struct {
4452
4553 pub fn init () CSSParser {
4654 return .{
47- .state = .seekName ,
55+ .state = .seek_name ,
4856 .name_start = 0 ,
4957 .name_end = 0 ,
5058 .value_start = 0 ,
@@ -54,59 +62,56 @@ pub const CSSParser = struct {
5462 };
5563 }
5664
57- pub fn parseDeclarations (text : [] const u8 , allocator : std.mem.Allocator ) ! []CSSDeclaration {
65+ pub fn parseDeclarations (allocator : std.mem.Allocator , text : [] const u8 ) ! []CSSDeclaration {
5866 var parser = init ();
59- var declarations = std .ArrayList (CSSDeclaration ).init (allocator );
60- errdefer declarations .deinit ();
67+ var declarations = std .ArrayListUnmanaged (CSSDeclaration ){};
6168
6269 while (parser .position < text .len ) {
6370 const c = text [parser .position ];
6471
6572 switch (parser .state ) {
66- .seekName = > {
73+ .seek_name = > {
6774 if (! std .ascii .isWhitespace (c )) {
6875 parser .name_start = parser .position ;
69- parser .state = .inName ;
76+ parser .state = .in_name ;
7077 continue ;
7178 }
7279 },
73- .inName = > {
80+ .in_name = > {
7481 if (c == ':' ) {
7582 parser .name_end = parser .position ;
76- parser .state = .seekValue ;
83+ parser .state = .seek_value ;
7784 } else if (std .ascii .isWhitespace (c )) {
7885 parser .name_end = parser .position ;
79- parser .state = .seekColon ;
86+ parser .state = .seek_colon ;
8087 }
8188 },
82- .seekColon = > {
89+ .seek_colon = > {
8390 if (c == ':' ) {
84- parser .state = .seekValue ;
91+ parser .state = .seek_value ;
8592 } else if (! std .ascii .isWhitespace (c )) {
86- parser .state = .seekName ;
93+ parser .state = .seek_name ;
8794 continue ;
8895 }
8996 },
90- .seekValue = > {
97+ .seek_value = > {
9198 if (! std .ascii .isWhitespace (c )) {
9299 parser .value_start = parser .position ;
93100 if (c == '"' ) {
94- parser .state = .inQuotedValue ;
101+ parser .state = .in_quoted_value ;
95102 } 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 ;
103+ parser .state = .in_single_quoted_value ;
104+ } else if (c == 'u' and parser .position + CSSConstants .URL_PREFIX .len <= text .len and std .mem .startsWith (u8 , text [parser .position .. ], CSSConstants .URL_PREFIX )) {
105+ parser .state = .in_url ;
101106 parser .paren_depth = 1 ;
102107 parser .position += 3 ;
103108 } else {
104- parser .state = .inValue ;
109+ parser .state = .in_value ;
105110 continue ;
106111 }
107112 }
108113 },
109- .inValue = > {
114+ .in_value = > {
110115 if (parser .escape_next ) {
111116 parser .escape_next = false ;
112117 } else if (c == '\\ ' ) {
@@ -116,29 +121,29 @@ pub const CSSParser = struct {
116121 } else if (c == ')' and parser .paren_depth > 0 ) {
117122 parser .paren_depth -= 1 ;
118123 } else if (c == ';' and parser .paren_depth == 0 ) {
119- try parser .finishDeclaration (text , & declarations );
120- parser .state = .seekName ;
124+ try parser .finishDeclaration (allocator , & declarations , text );
125+ parser .state = .seek_name ;
121126 }
122127 },
123- .inQuotedValue = > {
128+ .in_quoted_value = > {
124129 if (parser .escape_next ) {
125130 parser .escape_next = false ;
126131 } else if (c == '\\ ' ) {
127132 parser .escape_next = true ;
128133 } else if (c == '"' ) {
129- parser .state = .inValue ;
134+ parser .state = .in_value ;
130135 }
131136 },
132- .inSingleQuotedValue = > {
137+ .in_single_quoted_value = > {
133138 if (parser .escape_next ) {
134139 parser .escape_next = false ;
135140 } else if (c == '\\ ' ) {
136141 parser .escape_next = true ;
137142 } else if (c == '\' ' ) {
138- parser .state = .inValue ;
143+ parser .state = .in_value ;
139144 }
140145 },
141- .inUrl = > {
146+ .in_url = > {
142147 if (parser .escape_next ) {
143148 parser .escape_next = false ;
144149 } else if (c == '\\ ' ) {
@@ -148,22 +153,22 @@ pub const CSSParser = struct {
148153 } else if (c == ')' ) {
149154 parser .paren_depth -= 1 ;
150155 if (parser .paren_depth == 0 ) {
151- parser .state = .inValue ;
156+ parser .state = .in_value ;
152157 }
153158 }
154159 },
155- .inImportant = > {},
160+ .in_important = > {},
156161 }
157162
158163 parser .position += 1 ;
159164 }
160165
161- try parser .finalize (text , & declarations );
166+ try parser .finalize (allocator , & declarations , text );
162167
163- return declarations .toOwnedSlice () ;
168+ return declarations .items ;
164169 }
165170
166- fn finishDeclaration (self : * CSSParser , text : [] const u8 , declarations : * std .ArrayList (CSSDeclaration )) ! void {
171+ fn finishDeclaration (self : * CSSParser , allocator : std.mem.Allocator , declarations : * std .ArrayListUnmanaged (CSSDeclaration ), text : [] const u8 ) ! void {
167172 const name = std .mem .trim (u8 , text [self .name_start .. self .name_end ], & std .ascii .whitespace );
168173 if (name .len == 0 ) return ;
169174
@@ -173,17 +178,20 @@ pub const CSSParser = struct {
173178 var final_value = value ;
174179 var is_important = false ;
175180
176- if (std .mem .endsWith (u8 , value , CSSConstants .IMPORTANT_KEYWORD )) {
181+ if (std .mem .endsWith (u8 , value , CSSConstants .IMPORTANT )) {
177182 is_important = true ;
178- final_value = std .mem .trim (u8 , value [0 .. value .len - CSSConstants .IMPORTANT_LENGTH ], & std .ascii .whitespace );
183+ final_value = std .mem .trim (u8 , value [0 .. value .len - CSSConstants .IMPORTANT . len ], & std .ascii .whitespace );
179184 }
180185
181- const declaration = CSSDeclaration .init (name , final_value , is_important );
182- try declarations .append (declaration );
186+ try declarations .append (allocator , .{
187+ .name = name ,
188+ .value = final_value ,
189+ .is_important = is_important ,
190+ });
183191 }
184192
185- fn finalize (self : * CSSParser , text : [] const u8 , declarations : * std .ArrayList (CSSDeclaration )) ! void {
186- if (self .state == .inValue ) {
193+ fn finalize (self : * CSSParser , allocator : std.mem.Allocator , declarations : * std .ArrayListUnmanaged (CSSDeclaration ), text : [] const u8 ) ! void {
194+ if (self .state == .in_value ) {
187195 const name = text [self .name_start .. self .name_end ];
188196 const trimmed_name = std .mem .trim (u8 , name , & std .ascii .whitespace );
189197
@@ -193,13 +201,16 @@ pub const CSSParser = struct {
193201
194202 var final_value = value ;
195203 var is_important = false ;
196- if (std .mem .endsWith (u8 , value , CSSConstants .IMPORTANT_KEYWORD )) {
204+ if (std .mem .endsWith (u8 , value , CSSConstants .IMPORTANT )) {
197205 is_important = true ;
198- final_value = std .mem .trim (u8 , value [0 .. value .len - CSSConstants .IMPORTANT_LENGTH ], & std .ascii .whitespace );
206+ final_value = std .mem .trim (u8 , value [0 .. value .len - CSSConstants .IMPORTANT . len ], & std .ascii .whitespace );
199207 }
200208
201- const declaration = CSSDeclaration .init (trimmed_name , final_value , is_important );
202- try declarations .append (declaration );
209+ try declarations .append (allocator , .{
210+ .name = trimmed_name ,
211+ .value = final_value ,
212+ .is_important = is_important ,
213+ });
203214 }
204215 }
205216 }
@@ -217,92 +228,93 @@ pub const CSSParser = struct {
217228 }
218229};
219230
220- const testing = std . testing ;
231+ const testing = @import ( "../../ testing.zig" ) ;
221232
222233test "CSSParser - Simple property" {
234+ defer testing .reset ();
235+
223236 const text = "color: red;" ;
224- const allocator = testing .allocator ;
237+ const allocator = testing .arena_allocator ;
225238
226- const declarations = try CSSParser .parseDeclarations (text , allocator );
227- defer allocator .free (declarations );
239+ const declarations = try CSSParser .parseDeclarations (allocator , text );
228240
229241 try testing .expect (declarations .len == 1 );
230- try testing .expectEqualStrings ("color" , declarations [0 ].name );
231- try testing .expectEqualStrings ("red" , declarations [0 ].value );
242+ try testing .expectEqual ("color" , declarations [0 ].name );
243+ try testing .expectEqual ("red" , declarations [0 ].value );
232244 try testing .expect (! declarations [0 ].is_important );
233245}
234246
235247test "CSSParser - Property with !important" {
248+ defer testing .reset ();
236249 const text = "margin: 10px !important;" ;
237- const allocator = testing .allocator ;
250+ const allocator = testing .arena_allocator ;
238251
239- const declarations = try CSSParser .parseDeclarations (text , allocator );
240- defer allocator .free (declarations );
252+ const declarations = try CSSParser .parseDeclarations (allocator , text );
241253
242254 try testing .expect (declarations .len == 1 );
243- try testing .expectEqualStrings ("margin" , declarations [0 ].name );
244- try testing .expectEqualStrings ("10px" , declarations [0 ].value );
255+ try testing .expectEqual ("margin" , declarations [0 ].name );
256+ try testing .expectEqual ("10px" , declarations [0 ].value );
245257 try testing .expect (declarations [0 ].is_important );
246258}
247259
248260test "CSSParser - Multiple properties" {
261+ defer testing .reset ();
249262 const text = "color: red; font-size: 12px; margin: 5px !important;" ;
250- const allocator = testing .allocator ;
263+ const allocator = testing .arena_allocator ;
251264
252- const declarations = try CSSParser .parseDeclarations (text , allocator );
253- defer allocator .free (declarations );
265+ const declarations = try CSSParser .parseDeclarations (allocator , text );
254266
255267 try testing .expect (declarations .len == 3 );
256268
257- try testing .expectEqualStrings ("color" , declarations [0 ].name );
258- try testing .expectEqualStrings ("red" , declarations [0 ].value );
269+ try testing .expectEqual ("color" , declarations [0 ].name );
270+ try testing .expectEqual ("red" , declarations [0 ].value );
259271 try testing .expect (! declarations [0 ].is_important );
260272
261- try testing .expectEqualStrings ("font-size" , declarations [1 ].name );
262- try testing .expectEqualStrings ("12px" , declarations [1 ].value );
273+ try testing .expectEqual ("font-size" , declarations [1 ].name );
274+ try testing .expectEqual ("12px" , declarations [1 ].value );
263275 try testing .expect (! declarations [1 ].is_important );
264276
265- try testing .expectEqualStrings ("margin" , declarations [2 ].name );
266- try testing .expectEqualStrings ("5px" , declarations [2 ].value );
277+ try testing .expectEqual ("margin" , declarations [2 ].name );
278+ try testing .expectEqual ("5px" , declarations [2 ].value );
267279 try testing .expect (declarations [2 ].is_important );
268280}
269281
270282test "CSSParser - Quoted value with semicolon" {
283+ defer testing .reset ();
271284 const text = "content: \" Hello; world!\" ;" ;
272- const allocator = testing .allocator ;
285+ const allocator = testing .arena_allocator ;
273286
274- const declarations = try CSSParser .parseDeclarations (text , allocator );
275- defer allocator .free (declarations );
287+ const declarations = try CSSParser .parseDeclarations (allocator , text );
276288
277289 try testing .expect (declarations .len == 1 );
278- try testing .expectEqualStrings ("content" , declarations [0 ].name );
279- try testing .expectEqualStrings ("\" Hello; world!\" " , declarations [0 ].value );
290+ try testing .expectEqual ("content" , declarations [0 ].name );
291+ try testing .expectEqual ("\" Hello; world!\" " , declarations [0 ].value );
280292 try testing .expect (! declarations [0 ].is_important );
281293}
282294
283295test "CSSParser - URL value" {
296+ defer testing .reset ();
284297 const text = "background-image: url(\" test.png\" );" ;
285- const allocator = testing .allocator ;
298+ const allocator = testing .arena_allocator ;
286299
287- const declarations = try CSSParser .parseDeclarations (text , allocator );
288- defer allocator .free (declarations );
300+ const declarations = try CSSParser .parseDeclarations (allocator , text );
289301
290302 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 );
303+ try testing .expectEqual ("background-image" , declarations [0 ].name );
304+ try testing .expectEqual ("url(\" test.png\" )" , declarations [0 ].value );
293305 try testing .expect (! declarations [0 ].is_important );
294306}
295307
296308test "CSSParser - Whitespace handling" {
309+ defer testing .reset ();
297310 const text = " color : purple ; margin : 10px ; " ;
298- const allocator = testing .allocator ;
311+ const allocator = testing .arena_allocator ;
299312
300- const declarations = try CSSParser .parseDeclarations (text , allocator );
301- defer allocator .free (declarations );
313+ const declarations = try CSSParser .parseDeclarations (allocator , text );
302314
303315 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 );
316+ try testing .expectEqual ("color" , declarations [0 ].name );
317+ try testing .expectEqual ("purple" , declarations [0 ].value );
318+ try testing .expectEqual ("margin" , declarations [1 ].name );
319+ try testing .expectEqual ("10px" , declarations [1 ].value );
308320}
0 commit comments