Skip to content

Commit 22d8a92

Browse files
committed
Rewrite attribute selector parsing.
1 parent ad81a18 commit 22d8a92

File tree

11 files changed

+360
-55
lines changed

11 files changed

+360
-55
lines changed

package-lock.json

Lines changed: 4 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"test": "nyc ava src/__tests__/*.js"
3737
},
3838
"dependencies": {
39+
"dot-prop": "^4.1.1",
3940
"indexes-of": "^1.0.1",
4041
"uniq": "^1.0.1"
4142
},

src/__tests__/__snapshots__/postcss.js.md

Lines changed: 140 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,144 @@ The actual snapshot is saved in `postcss.js.snap`.
44

55
Generated by [AVA](https://ava.li).
66

7+
## bad doubled operator
8+
9+
> Snapshot 1
10+
11+
`CssSyntaxError: <css input>:1:10: Unexpected "=" found; an operator was already defined.␊
12+
13+
> 1 | [href=foo=bar] {}␊
14+
| ^␊
15+
`
16+
17+
## bad lonely asterisk
18+
19+
> Snapshot 1
20+
21+
`CssSyntaxError: <css input>:1:2: Expected an attribute.␊
22+
23+
> 1 | [*] {}␊
24+
| ^␊
25+
`
26+
27+
## bad lonely caret
28+
29+
> Snapshot 1
30+
31+
`CssSyntaxError: <css input>:1:2: Expected an attribute.␊
32+
33+
> 1 | [^] {}␊
34+
| ^␊
35+
`
36+
37+
## bad lonely dollar
38+
39+
> Snapshot 1
40+
41+
`CssSyntaxError: <css input>:1:2: Expected an attribute.␊
42+
43+
> 1 | [$] {}␊
44+
| ^␊
45+
`
46+
47+
## bad lonely equals
48+
49+
> Snapshot 1
50+
51+
`CssSyntaxError: <css input>:1:2: Expected an attribute.␊
52+
53+
> 1 | [=] {}␊
54+
| ^␊
55+
`
56+
57+
## bad lonely operator
58+
59+
> Snapshot 1
60+
61+
`CssSyntaxError: <css input>:1:3: Expected an attribute, found "=" instead.␊
62+
63+
> 1 | [*=] {}␊
64+
| ^␊
65+
`
66+
67+
## bad lonely operator (2)
68+
69+
> Snapshot 1
70+
71+
`CssSyntaxError: <css input>:1:3: Expected an attribute, found "=" instead.␊
72+
73+
> 1 | [|=] {}␊
74+
| ^␊
75+
`
76+
77+
## bad lonely pipe
78+
79+
> Snapshot 1
80+
81+
`CssSyntaxError: <css input>:1:2: Expected an attribute.␊
82+
83+
> 1 | [|] {}␊
84+
| ^␊
85+
`
86+
87+
## bad lonely tilde
88+
89+
> Snapshot 1
90+
91+
`CssSyntaxError: <css input>:1:2: Expected an attribute.␊
92+
93+
> 1 | [~] {}␊
94+
| ^␊
95+
`
96+
97+
## bad parentheses
98+
99+
> Snapshot 1
100+
101+
`CssSyntaxError: <css input>:1:6: Unexpected "(" found.␊
102+
103+
> 1 | [foo=(bar)] {}␊
104+
| ^␊
105+
`
106+
107+
## bad string attribute
108+
109+
> Snapshot 1
110+
111+
`CssSyntaxError: <css input>:1:2: Expected an attribute.␊
112+
113+
> 1 | ["hello"] {}␊
114+
| ^␊
115+
`
116+
117+
## bad string attribute with value
118+
119+
> Snapshot 1
120+
121+
`CssSyntaxError: <css input>:1:2: Expected an attribute followed by an operator preceding the string.␊
122+
123+
> 1 | ["foo"=bar] {}␊
124+
| ^␊
125+
`
126+
127+
## bad whitespace instead of namespace token
128+
129+
> Snapshot 1
130+
131+
`CssSyntaxError: <css input>:1:2: Expected an attribute, found " " instead.␊
132+
133+
> 1 | [ |href=test] {}␊
134+
| ^␊
135+
`
136+
7137
## missing open parenthesis
8138

9139
> Snapshot 1
10140
11141
`CssSyntaxError: <css input>:1:6: Expected an opening parenthesis.␊
12142
13-
[31m[1m>[22m[39m[90m 1 | [39ma b c[36m)[39m [33m{[39m[33m}[39m
14-
[90m | [39m [31m[1m^[22m[39m
143+
> 1 | a b c) {}
144+
| ^
15145
`
16146

17147
## missing open square bracket
@@ -20,8 +150,8 @@ Generated by [AVA](https://ava.li).
20150
21151
`CssSyntaxError: <css input>:1:6: Expected an opening square bracket.␊
22152
23-
[31m[1m>[22m[39m[90m 1 | [39ma b c[33m][39m [33m{[39m[33m}[39m
24-
[90m | [39m [31m[1m^[22m[39m
153+
> 1 | a b c] {}
154+
| ^
25155
`
26156

27157
## missing pseudo class or pseudo element
@@ -30,8 +160,8 @@ Generated by [AVA](https://ava.li).
30160
31161
`CssSyntaxError: <css input>:1:6: Expected a pseudo-class or pseudo-element.␊
32162
33-
[31m[1m>[22m[39m[90m 1 | [39ma b c[33m:[39m [33m{[39m[33m}[39m
34-
[90m | [39m [31m[1m^[22m[39m
163+
> 1 | a b c: {}
164+
| ^
35165
`
36166

37167
## space in between colon and word (incorrect pseudo)
@@ -40,8 +170,8 @@ Generated by [AVA](https://ava.li).
40170
41171
`CssSyntaxError: <css input>:1:5: Expected a pseudo-class or pseudo-element.␊
42172
43-
[31m[1m>[22m[39m[90m 1 | [39ma b[33m:[39m c [33m{[39m[33m}[39m
44-
[90m | [39m [31m[1m^[22m[39m
173+
> 1 | a b: c {}
174+
| ^
45175
`
46176

47177
## string after colon (incorrect pseudo)
@@ -50,6 +180,6 @@ Generated by [AVA](https://ava.li).
50180
51181
`CssSyntaxError: <css input>:1:5: Expected a pseudo-class or pseudo-element.␊
52182
53-
[31m[1m>[22m[39m[90m 1 | [39ma b[33m:[39m[32m"wow"[39m [33m{[39m[33m}[39m
54-
[90m | [39m [31m[1m^[22m[39m
183+
> 1 | a b:"wow" {}
184+
| ^
55185
`
480 Bytes
Binary file not shown.

src/__tests__/attributes.js

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,35 @@ test('attribute selector', '[href]', (t, tree) => {
66
t.falsy(tree.nodes[0].nodes[0].quoted);
77
});
88

9+
test('attribute selector spaces (before)', '[ href]', (t, tree) => {
10+
t.deepEqual(tree.nodes[0].nodes[0].attribute, ' href');
11+
t.deepEqual(tree.nodes[0].nodes[0].type, 'attribute');
12+
t.falsy(tree.nodes[0].nodes[0].quoted);
13+
});
14+
15+
test('attribute selector spaces (after)', '[href ]', (t, tree) => {
16+
t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href ');
17+
t.deepEqual(tree.nodes[0].nodes[0].type, 'attribute');
18+
t.falsy(tree.nodes[0].nodes[0].quoted);
19+
});
20+
21+
test('attribute selector spaces (both)', '[ href ]', (t, tree) => {
22+
t.deepEqual(tree.nodes[0].nodes[0].attribute, ' href ');
23+
t.deepEqual(tree.nodes[0].nodes[0].type, 'attribute');
24+
t.falsy(tree.nodes[0].nodes[0].quoted);
25+
});
26+
927
test('multiple attribute selectors', '[href][class][name]', (t, tree) => {
1028
t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href');
1129
t.deepEqual(tree.nodes[0].nodes[1].attribute, 'class');
1230
t.deepEqual(tree.nodes[0].nodes[2].attribute, 'name');
1331
});
1432

33+
test('select elements with or without a namespace', '[*|href]', (t, tree) => {
34+
t.deepEqual(tree.nodes[0].nodes[0].namespace, '*');
35+
t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href');
36+
});
37+
1538
test('attribute selector with a value', '[name=james]', (t, tree) => {
1639
t.deepEqual(tree.nodes[0].nodes[0].attribute, 'name');
1740
t.deepEqual(tree.nodes[0].nodes[0].operator, '=');
@@ -216,8 +239,8 @@ test('more multiple attribute selectors with quoted value containing multiple "=
216239

217240
test('spaces in attribute selectors', 'h1[ href *= "test" ]', (t, tree) => {
218241
t.deepEqual(tree.nodes[0].nodes[1].attribute, ' href ');
219-
t.deepEqual(tree.nodes[0].nodes[1].operator, '*=');
220-
t.deepEqual(tree.nodes[0].nodes[1].value, ' "test" ');
242+
t.deepEqual(tree.nodes[0].nodes[1].operator, '*= ');
243+
t.deepEqual(tree.nodes[0].nodes[1].value, '"test" ');
221244
t.truthy(tree.nodes[0].nodes[1].quoted);
222245
t.deepEqual(tree.nodes[0].nodes[1].raws.unquoted, 'test');
223246
});
@@ -245,3 +268,21 @@ test('extraneous non-combinating whitespace', ' [href] , [class] ', (t, tr
245268
t.deepEqual(tree.nodes[1].nodes[0].spaces.before, ' ');
246269
t.deepEqual(tree.nodes[1].nodes[0].spaces.after, ' ');
247270
});
271+
272+
test('comments within attribute selectors', '[href/* wow */=/* wow */test]', (t, tree) => {
273+
t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href/* wow */');
274+
t.deepEqual(tree.nodes[0].nodes[0].operator, '=/* wow */');
275+
t.deepEqual(tree.nodes[0].nodes[0].value, 'test');
276+
});
277+
278+
test('comments within attribute selectors (2)', '[/* wow */href=test/* wow */]', (t, tree) => {
279+
t.deepEqual(tree.nodes[0].nodes[0].attribute, '/* wow */href');
280+
t.deepEqual(tree.nodes[0].nodes[0].operator, '=');
281+
t.deepEqual(tree.nodes[0].nodes[0].value, 'test/* wow */');
282+
});
283+
284+
test('comments within attribute selectors (3)', '[href=test/* wow */i]', (t, tree) => {
285+
t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href');
286+
t.deepEqual(tree.nodes[0].nodes[0].value, 'test/* wow */');
287+
t.deepEqual(tree.nodes[0].nodes[0].insensitive, true);
288+
});

src/__tests__/lossy.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ ava('namespace - inside attribute (2), space before', testLossy, ' [ postcss|hr
4646
ava('namespace - inside attribute (2), space after', testLossy, '[postcss|href ] ', '[postcss|href]');
4747
ava('namespace - inside attribute (3), space before', testLossy, ' [ *|href=test]', '[*|href=test]');
4848
ava('namespace - inside attribute (3), space after', testLossy, '[*|href= test ] ', '[*|href=test]');
49-
ava('namespace - inside attribute (4), space before', testLossy, ' [ |href=test]', '[|href=test]');
5049
ava('namespace - inside attribute (4), space after', testLossy, '[|href= test ] ', '[|href=test]');
5150

5251
ava('tag - extraneous whitespace', testLossy, ' h1 , h2 ', 'h1,h2');

src/__tests__/postcss.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ import test from 'ava';
22
import postcss from 'postcss';
33
import {parse} from './util/helpers';
44

5+
const cse = 'CssSyntaxError';
6+
57
function showCode (t, selector) {
68
const rule = postcss.parse(selector).first;
79
try {
810
parse(rule);
911
} catch (e) {
10-
t.snapshot(e.toString());
12+
if (e.name !== cse) {
13+
return;
14+
}
15+
// Removes ANSI codes from snapshot tests as it makes them illegible.
16+
// The formatting of this error is otherwise identical to e.toString()
17+
t.snapshot(`${cse}: ${e.message}\n\n${e.showSourceCode(false)}\n`);
1118
}
1219
}
1320

@@ -17,3 +24,19 @@ test('missing pseudo class or pseudo element', showCode, 'a b c: {}');
1724

1825
test('space in between colon and word (incorrect pseudo)', showCode, 'a b: c {}');
1926
test('string after colon (incorrect pseudo)', showCode, 'a b:"wow" {}');
27+
28+
// attribute selectors
29+
30+
test('bad whitespace instead of namespace token', showCode, '[ |href=test] {}');
31+
test('bad string attribute', showCode, '["hello"] {}');
32+
test('bad string attribute with value', showCode, '["foo"=bar] {}');
33+
test('bad parentheses', showCode, '[foo=(bar)] {}');
34+
test('bad lonely asterisk', showCode, '[*] {}');
35+
test('bad lonely pipe', showCode, '[|] {}');
36+
test('bad lonely caret', showCode, '[^] {}');
37+
test('bad lonely dollar', showCode, '[$] {}');
38+
test('bad lonely tilde', showCode, '[~] {}');
39+
test('bad lonely equals', showCode, '[=] {}');
40+
test('bad lonely operator', showCode, '[*=] {}');
41+
test('bad lonely operator (2)', showCode, '[|=] {}');
42+
test('bad doubled operator', showCode, '[href=foo=bar] {}');

0 commit comments

Comments
 (0)