Skip to content

Commit 37bd91b

Browse files
committed
Fix #8
Treat `.` and `#` as `:`
1 parent 2ddbfe2 commit 37bd91b

File tree

4 files changed

+174
-50
lines changed

4 files changed

+174
-50
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,12 @@ let css =
4848
*)
4949
```
5050

51+
### Remarks
52+
53+
Whitespaces and comments are discarded by the lexer, so they are not available
54+
to the parser. An exception is made for significant whitespaces in rule
55+
preludes, to disambiguate between selectors like `p :first-child` and
56+
`p:first-child`. These whitespaces are replaced with `*` to keep CSS semantics
57+
intact. So, e.g., `p :first-child` is parsed as `p *:first-child`, `p .class`
58+
as `p *.class`, and `p #id` as `p *#id`.
59+

lib/lexer.ml

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ let token_to_string = function
4040
| LEFT_BRACKET -> "["
4141
| RIGHT_BRACKET -> "]"
4242
| COLON -> ":"
43-
| WHITESPACE_BEFORE_COLON -> "*"
44-
| WHITESPACE_COLON -> "*:"
43+
| DOT -> "."
44+
(* Whitespaces are detected only in selectors, before ":", ".", and "#", to
45+
* disambiguate between "p :first-child" and "p:first-child", these
46+
* whitespaces are replaced with "*" *)
47+
| WHITESPACE -> "*"
4548
| SEMI_COLON -> ";"
4649
| PERCENTAGE -> "%"
4750
| IMPORTANT -> "!important"
@@ -262,34 +265,39 @@ let discard_comments_and_white_spaces buf =
262265
in
263266
discard_white_spaces buf false
264267

265-
let rec get_next_token buf spaces_detected =
268+
let rec get_next_tokens buf spaces_detected =
266269
let open Menhir_parser in
267270
match%sedlex buf with
268-
| eof -> EOF
269-
| ';' -> SEMI_COLON
270-
| '}' -> RIGHT_BRACE
271-
| '{' -> LEFT_BRACE
272-
| ':' -> if spaces_detected then WHITESPACE_COLON else COLON
273-
| '(' -> LEFT_PAREN
274-
| ')' -> RIGHT_PAREN
275-
| '[' -> LEFT_BRACKET
276-
| ']' -> RIGHT_BRACKET
277-
| '%' -> PERCENTAGE
278-
| operator -> OPERATOR (Lex_buffer.latin1 buf)
279-
| string -> STRING (Lex_buffer.latin1 ~skip:1 ~drop:1 buf)
280-
| "url(" -> get_url "" buf
281-
| important -> IMPORTANT
282-
| nested_at_rule -> NESTED_AT_RULE (Lex_buffer.latin1 ~skip:1 buf)
283-
| at_rule_without_body -> AT_RULE_WITHOUT_BODY (Lex_buffer.latin1 ~skip:1 buf)
284-
| at_rule -> AT_RULE (Lex_buffer.latin1 ~skip:1 buf)
271+
| eof -> [ EOF ]
272+
| ';' -> [ SEMI_COLON ]
273+
| '}' -> [ RIGHT_BRACE ]
274+
| '{' -> [ LEFT_BRACE ]
275+
| ':' -> if spaces_detected then [ WHITESPACE; COLON ] else [ COLON ]
276+
| '.' -> if spaces_detected then [ WHITESPACE; DOT ] else [ DOT ]
277+
| '(' -> [ LEFT_PAREN ]
278+
| ')' -> [ RIGHT_PAREN ]
279+
| '[' -> [ LEFT_BRACKET ]
280+
| ']' -> [ RIGHT_BRACKET ]
281+
| '%' -> [ PERCENTAGE ]
282+
| operator -> [ OPERATOR (Lex_buffer.latin1 buf) ]
283+
| string -> [ STRING (Lex_buffer.latin1 ~skip:1 ~drop:1 buf) ]
284+
| "url(" -> [ get_url "" buf ]
285+
| important -> [ IMPORTANT ]
286+
| nested_at_rule -> [ NESTED_AT_RULE (Lex_buffer.latin1 ~skip:1 buf) ]
287+
| at_rule_without_body ->
288+
[ AT_RULE_WITHOUT_BODY (Lex_buffer.latin1 ~skip:1 buf) ]
289+
| at_rule -> [ AT_RULE (Lex_buffer.latin1 ~skip:1 buf) ]
285290
(* NOTE: should be placed above ident, otherwise pattern with
286291
* '-[0-9a-z]{1,6}' cannot be matched *)
287-
| _u, '+', unicode_range -> UNICODE_RANGE (Lex_buffer.latin1 buf)
288-
| ident, '(' -> FUNCTION (Lex_buffer.latin1 ~drop:1 buf)
289-
| ident -> IDENT (Lex_buffer.latin1 buf)
290-
| '#', name -> HASH (Lex_buffer.latin1 ~skip:1 buf)
291-
| number -> get_dimension (Lex_buffer.latin1 buf) buf
292-
| any -> DELIM (Lex_buffer.latin1 buf)
292+
| _u, '+', unicode_range -> [ UNICODE_RANGE (Lex_buffer.latin1 buf) ]
293+
| ident, '(' -> [ FUNCTION (Lex_buffer.latin1 ~drop:1 buf) ]
294+
| ident -> [ IDENT (Lex_buffer.latin1 buf) ]
295+
| '#', name ->
296+
if spaces_detected then
297+
[ WHITESPACE; HASH (Lex_buffer.latin1 ~skip:1 buf) ]
298+
else [ HASH (Lex_buffer.latin1 ~skip:1 buf) ]
299+
| number -> [ get_dimension (Lex_buffer.latin1 buf) buf ]
300+
| any -> [ DELIM (Lex_buffer.latin1 buf) ]
293301
| _ -> assert false
294302

295303
and get_dimension n buf =
@@ -316,25 +324,19 @@ and get_url url buf =
316324

317325
let token_queue = Queue.create ()
318326

319-
let queue_next_token_with_location buf =
327+
let queue_next_tokens_with_location buf =
320328
let spaces_detected = discard_comments_and_white_spaces buf in
321329
let loc_start = Lex_buffer.next_loc buf in
322-
let token = get_next_token buf spaces_detected in
330+
let tokens = get_next_tokens buf spaces_detected in
323331
let loc_end = Lex_buffer.next_loc buf in
324-
match token with
325-
| Menhir_parser.WHITESPACE_COLON ->
326-
Queue.add
327-
(Menhir_parser.WHITESPACE_BEFORE_COLON, loc_start, loc_end)
328-
token_queue;
329-
Queue.add (Menhir_parser.COLON, loc_start, loc_end) token_queue
330-
| _ -> Queue.add (token, loc_start, loc_end) token_queue
332+
List.iter (fun t -> Queue.add (t, loc_start, loc_end) token_queue) tokens
331333

332334
let parse buf p =
333335
let last_token =
334336
ref (Menhir_parser.EOF, Lexing.dummy_pos, Lexing.dummy_pos)
335337
in
336338
let next_token () =
337-
if Queue.is_empty token_queue then queue_next_token_with_location buf;
339+
if Queue.is_empty token_queue then queue_next_tokens_with_location buf;
338340
last_token := Queue.take token_queue;
339341
!last_token
340342
in

lib/menhir_parser.mly

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ open Types
1515
%token LEFT_BRACKET
1616
%token RIGHT_BRACKET
1717
%token COLON
18-
%token WHITESPACE_BEFORE_COLON
19-
%token WHITESPACE_COLON
18+
%token DOT
19+
(* Whitespaces are detected only in selectors, before ":", ".", and "#", to
20+
* disambiguate between "p :first-child" and "p:first-child", these
21+
* whitespaces are replaced with "*" *)
22+
%token WHITESPACE
2023
%token SEMI_COLON
2124
%token PERCENTAGE
2225
%token IMPORTANT
@@ -101,7 +104,7 @@ prelude_with_loc:
101104
;
102105

103106
prelude:
104-
xs = list(component_value_with_loc) { xs }
107+
xs = list(component_value_with_loc_in_prelude) { xs }
105108
;
106109

107110
declarations_with_loc:
@@ -124,7 +127,7 @@ declaration_or_at_rule:
124127
;
125128

126129
declaration:
127-
n = IDENT; option(WHITESPACE_BEFORE_COLON); COLON; v = list(component_value_with_loc); i = boption(IMPORTANT) {
130+
n = IDENT; option(WHITESPACE); COLON; v = list(component_value_with_loc); i = boption(IMPORTANT) {
128131
{ Declaration.name = (n, Lex_buffer.make_loc_and_fix $startpos(n) $endpos(n));
129132
value = (v, Lex_buffer.make_loc_and_fix $startpos(v) $endpos(v));
130133
important = (i, Lex_buffer.make_loc_and_fix $startpos(i) $endpos(i));
@@ -153,9 +156,34 @@ component_value:
153156
| u = URI { Component_value.Uri u }
154157
| o = OPERATOR { Component_value.Operator o }
155158
| d = DELIM { Component_value.Delim d }
159+
| option(WHITESPACE); COLON { Component_value.Delim ":" }
160+
| option(WHITESPACE); DOT { Component_value.Delim "." }
161+
| f = FUNCTION; xs = list(component_value_with_loc); RIGHT_PAREN {
162+
Component_value.Function ((f, Lex_buffer.make_loc_and_fix $startpos(f) $endpos(f)),
163+
(xs, Lex_buffer.make_loc_and_fix $startpos(xs) $endpos(xs)))
164+
}
165+
| option(WHITESPACE); h = HASH { Component_value.Hash h }
166+
| n = NUMBER { Component_value.Number n }
167+
| r = UNICODE_RANGE { Component_value.Unicode_range r }
168+
| d = FLOAT_DIMENSION { Component_value.Float_dimension d }
169+
| d = DIMENSION { Component_value.Dimension d }
170+
;
171+
172+
component_value_with_loc_in_prelude:
173+
| c = component_value_in_prelude { (c, Lex_buffer.make_loc_and_fix $startpos $endpos) }
174+
175+
component_value_in_prelude:
176+
| b = paren_block { Component_value.Paren_block b }
177+
| b = bracket_block { Component_value.Bracket_block b }
178+
| n = NUMBER; PERCENTAGE { Component_value.Percentage n }
179+
| i = IDENT { Component_value.Ident i }
180+
| s = STRING { Component_value.String s }
181+
| u = URI { Component_value.Uri u }
182+
| o = OPERATOR { Component_value.Operator o }
183+
| d = DELIM { Component_value.Delim d }
184+
| WHITESPACE { Component_value.Delim "*" }
156185
| COLON { Component_value.Delim ":" }
157-
| WHITESPACE_BEFORE_COLON { Component_value.Delim "*" }
158-
| COLON { Component_value.Delim ":" }
186+
| DOT { Component_value.Delim "." }
159187
| f = FUNCTION; xs = list(component_value_with_loc); RIGHT_PAREN {
160188
Component_value.Function ((f, Lex_buffer.make_loc_and_fix $startpos(f) $endpos(f)),
161189
(xs, Lex_buffer.make_loc_and_fix $startpos(xs) $endpos(xs)))

test/test_parser.ml

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,7 @@ let test_hover_selector () =
899899

900900
let test_id_selector () =
901901
let css = {|
902-
#element {
902+
#id {
903903
color: blue
904904
}
905905
|} in
@@ -909,7 +909,10 @@ let test_id_selector () =
909909
Rule.Style_rule
910910
{
911911
Style_rule.prelude =
912-
( [ (Component_value.Hash "element", Location.none) ],
912+
( [
913+
(Component_value.Delim "*", Location.none);
914+
(Component_value.Hash "id", Location.none);
915+
],
913916
Location.none );
914917
block =
915918
( [
@@ -934,7 +937,7 @@ let test_id_selector () =
934937

935938
let test_class_selector () =
936939
let css = {|
937-
.element {
940+
.classname {
938941
color: blue
939942
}
940943
|} in
@@ -945,8 +948,9 @@ let test_class_selector () =
945948
{
946949
Style_rule.prelude =
947950
( [
951+
(Component_value.Delim "*", Location.none);
948952
(Component_value.Delim ".", Location.none);
949-
(Component_value.Ident "element", Location.none);
953+
(Component_value.Ident "classname", Location.none);
950954
],
951955
Location.none );
952956
block =
@@ -1261,6 +1265,45 @@ p :first-child {
12611265
Alcotest.(check (testable Css_fmt_printer.dump_stylesheet eq_ast))
12621266
"different CSS AST" expected_ast ast
12631267

1268+
let test_p_first_child_selector () =
1269+
let css = {|
1270+
p:first-child {
1271+
color: blue
1272+
}
1273+
|} in
1274+
let ast = Css.Parser.parse_stylesheet css in
1275+
let expected_ast =
1276+
( [
1277+
Rule.Style_rule
1278+
{
1279+
Style_rule.prelude =
1280+
( [
1281+
(Component_value.Ident "p", Location.none);
1282+
(Component_value.Delim ":", Location.none);
1283+
(Component_value.Ident "first-child", Location.none);
1284+
],
1285+
Location.none );
1286+
block =
1287+
( [
1288+
Declaration_list.Declaration
1289+
{
1290+
Declaration.name = ("color", Location.none);
1291+
value =
1292+
( [ (Component_value.Ident "blue", Location.none) ],
1293+
Location.none );
1294+
important = (false, Location.none);
1295+
loc = Location.none;
1296+
};
1297+
],
1298+
Location.none );
1299+
loc = Location.none;
1300+
};
1301+
],
1302+
Location.none )
1303+
in
1304+
Alcotest.(check (testable Css_fmt_printer.dump_stylesheet eq_ast))
1305+
"different CSS AST" expected_ast ast
1306+
12641307
let test_p_star_space_first_child_selector () =
12651308
let css = {|
12661309
p * :first-child {
@@ -1302,9 +1345,9 @@ p * :first-child {
13021345
Alcotest.(check (testable Css_fmt_printer.dump_stylesheet eq_ast))
13031346
"different CSS AST" expected_ast ast
13041347

1305-
let test_p_first_child_selector () =
1348+
let test_p_space_dot_classname () =
13061349
let css = {|
1307-
p:first-child {
1350+
p .classname {
13081351
color: blue
13091352
}
13101353
|} in
@@ -1316,8 +1359,48 @@ p:first-child {
13161359
Style_rule.prelude =
13171360
( [
13181361
(Component_value.Ident "p", Location.none);
1319-
(Component_value.Delim ":", Location.none);
1320-
(Component_value.Ident "first-child", Location.none);
1362+
(Component_value.Delim "*", Location.none);
1363+
(Component_value.Delim ".", Location.none);
1364+
(Component_value.Ident "classname", Location.none);
1365+
],
1366+
Location.none );
1367+
block =
1368+
( [
1369+
Declaration_list.Declaration
1370+
{
1371+
Declaration.name = ("color", Location.none);
1372+
value =
1373+
( [ (Component_value.Ident "blue", Location.none) ],
1374+
Location.none );
1375+
important = (false, Location.none);
1376+
loc = Location.none;
1377+
};
1378+
],
1379+
Location.none );
1380+
loc = Location.none;
1381+
};
1382+
],
1383+
Location.none )
1384+
in
1385+
Alcotest.(check (testable Css_fmt_printer.dump_stylesheet eq_ast))
1386+
"different CSS AST" expected_ast ast
1387+
1388+
let test_p_space_hash_id () =
1389+
let css = {|
1390+
p #id {
1391+
color: blue
1392+
}
1393+
|} in
1394+
let ast = Css.Parser.parse_stylesheet css in
1395+
let expected_ast =
1396+
( [
1397+
Rule.Style_rule
1398+
{
1399+
Style_rule.prelude =
1400+
( [
1401+
(Component_value.Ident "p", Location.none);
1402+
(Component_value.Delim "*", Location.none);
1403+
(Component_value.Hash "id", Location.none);
13211404
],
13221405
Location.none );
13231406
block =
@@ -1370,4 +1453,6 @@ let test_set =
13701453
("p :first-child selector", `Quick, test_p_space_first_child_selector);
13711454
("p:first-child selector", `Quick, test_p_first_child_selector);
13721455
("p * :first-child selector", `Quick, test_p_star_space_first_child_selector);
1456+
("p .classname selector", `Quick, test_p_space_dot_classname);
1457+
("p #id selector", `Quick, test_p_space_hash_id);
13731458
]

0 commit comments

Comments
 (0)