Skip to content

Commit 479d8a7

Browse files
Merge pull request #140.
* The way a descendent combinator that isn't a single space character (E.g. `.a .b`) is stored in the AST has changed. * Named Combinators (E.g. `.a /for/ .b`) are now properly parsed as a combinator. * It is now possible to look up a node based on the source location of a character in that node and to query nodes if they contain some character. * Several bug fixes that caused the parser to hang and run out of memory when a `/` was encountered have been fixed. * The minimum supported version of Node is now `v6.0.0`. This fixes stylelint in conjunction with stylelint/stylelint#3284
2 parents faf2eb2 + 746e4ba commit 479d8a7

24 files changed

+776
-97
lines changed

.gitmodules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[submodule "integration/stylelint"]
2+
path = integration/stylelint
3+
url = https://github.com/chriseppstein/stylelint.git
4+
branch = selector-parser-4.0

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
integration/*/

.travis.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,20 @@ sudo: false
33
language: node_js
44
matrix:
55
include:
6+
- node_js: '10'
7+
env: INTEGRATION=false
68
- node_js: '8'
7-
- node_js: '7'
9+
env: INTEGRATION=true
810
- node_js: '6'
9-
- node_js: '5'
10-
- node_js: '4'
11-
11+
env: INTEGRATION=false
12+
install:
13+
- npm install -g npm@latest
14+
- npm ci
15+
script:
16+
- npm test
17+
- ./integration_test.sh
18+
cache:
19+
directories:
20+
- ~/.npm
1221
after_success:
1322
- './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'

API.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ Arguments:
7878

7979
* `props (object)`: The new node's properties.
8080

81+
Notes:
82+
* **Descendant Combinators** The value of descendant combinators created by the
83+
parser always just a single space (`" "`). For descendant selectors with no
84+
comments, additional space is now stored in `node.spaces.before`. Depending
85+
on the location of comments, additional spaces may be stored in
86+
`node.raws.spaces.before`, `node.raws.spaces.after`, or `node.raws.value`.
87+
* **Named Combinators** Although, nonstandard and unlikely to ever become a standard,
88+
named combinators like `/deep/` and `/for/` are parsed as combinators. The
89+
`node.value` is name after being unescaped and normalized as lowercase. The
90+
original value for the combinator name is stored in `node.raws.value`.
91+
92+
8193
### `parser.comment([props])`
8294

8395
Creates a new comment.
@@ -275,6 +287,17 @@ String(cloned);
275287
// => #search
276288
```
277289

290+
### `node.isAtPosition(line, column)`
291+
292+
Return a `boolean` indicating whether this node includes the character at the
293+
position of the given line and column. Returns `undefined` if the nodes lack
294+
sufficient source metadata to determine the position.
295+
296+
Arguments:
297+
298+
* `line`: 1-index based line number relative to the start of the selector.
299+
* `column`: 1-index based column number relative to the start of the selector.
300+
278301
### `node.spaces`
279302

280303
Extra whitespaces around the node will be moved into `node.spaces.before` and
@@ -285,15 +308,13 @@ no semantic meaning:
285308
h1 , h2 {}
286309
```
287310

288-
However, *combinating* spaces will form a `combinator` node:
311+
For descendent selectors, the value is always a single space.
289312

290313
```css
291314
h1 h2 {}
292315
```
293316

294-
A `combinator` node may only have the `spaces` property set if the combinator
295-
value is a non-whitespace character, such as `+`, `~` or `>`. Otherwise, the
296-
combinator value will contain all of the spaces between selectors.
317+
Additional whitespace is found in either the `node.spaces.before` and `node.spaces.after` depending on the presence of comments or other whitespace characters. If the actual whitespace does not start or end with a single space, the node's raw value is set to the actual space(s) found in the source.
297318

298319
### `node.source`
299320

@@ -369,6 +390,19 @@ Arguments:
369390

370391
* `index`: The index of the node to return.
371392

393+
### `container.atPosition(line, column)`
394+
395+
Returns the node at the source position `index`.
396+
397+
```js
398+
selector.at(0) === selector.first;
399+
selector.at(0) === selector.nodes[0];
400+
```
401+
402+
Arguments:
403+
404+
* `index`: The index of the node to return.
405+
372406
### `container.index(node)`
373407

374408
Return the index of the node within its container.
@@ -551,7 +585,7 @@ support parsing of legacy CSS hacks.
551585

552586
## Selector nodes
553587

554-
A selector node represents a single compound selector. For example, this
588+
A selector node represents a single complex selector. For example, this
555589
selector string `h1 h2 h3, [href] > p`, is represented as two selector nodes.
556590
It has no special functionality of its own.
557591

CHANGELOG.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,235 @@
1+
# 5.0.0-rc.0
2+
3+
This release has **BREAKING CHANGES** that were required to fix regressions
4+
in 4.0.0 and to make the Combinator Node API consistent for all combinator
5+
types. Please read carefully.
6+
7+
## Summary of Changes
8+
9+
* The way a descendent combinator that isn't a single space character (E.g. `.a .b`) is stored in the AST has changed.
10+
* Named Combinators (E.g. `.a /for/ .b`) are now properly parsed as a combinator.
11+
* It is now possible to look up a node based on the source location of a character in that node and to query nodes if they contain some character.
12+
* Several bug fixes that caused the parser to hang and run out of memory when a `/` was encountered have been fixed.
13+
* The minimum supported version of Node is now `v6.0.0`.
14+
15+
### Changes to the Descendent Combinator
16+
17+
In prior releases, the value of a descendant combinator with multiple spaces included all the spaces.
18+
19+
* `.a .b`: Extra spaces are now stored as space before.
20+
- Old & Busted:
21+
- `combinator.value === " "`
22+
- New hotness:
23+
- `combinator.value === " " && combinator.spaces.before === " "`
24+
* `.a /*comment*/.b`: A comment at the end of the combinator causes extra space to become after space.
25+
- Old & Busted:
26+
- `combinator.value === " "`
27+
- `combinator.raws.value === " /*comment/"`
28+
- New hotness:
29+
- `combinator.value === " "`
30+
- `combinator.spaces.after === " "`
31+
- `combinator.raws.spaces.after === " /*comment*/"`
32+
* `.a<newline>.b`: whitespace that doesn't start or end with a single space character is stored as a raw value.
33+
- Old & Busted:
34+
- `combinator.value === "\n"`
35+
- `combinator.raws.value === undefined`
36+
- New hotness:
37+
- `combinator.value === " "`
38+
- `combinator.raws.value === "\n"`
39+
40+
### Support for "Named Combinators"
41+
42+
Although, nonstandard and unlikely to ever become a standard, combinators like `/deep/` and `/for/` are now properly supported.
43+
44+
Because they've been taken off the standardization track, there is no spec-official name for combinators of the form `/<ident>/`. However, I talked to [Tab Atkins](https://twitter.com/tabatkins) and we agreed to call them "named combinators" so now they are called that.
45+
46+
Before this release such named combinators were parsed without intention and generated three nodes of type `"tag"` where the first and last nodes had a value of `"/"`.
47+
48+
* `.a /for/ .b` is parsed as a combinator.
49+
- Old & Busted:
50+
- `root.nodes[0].nodes[1].type === "tag"`
51+
- `root.nodes[0].nodes[1].value === "/"`
52+
- New hotness:
53+
- `root.nodes[0].nodes[1].type === "combinator"`
54+
- `root.nodes[0].nodes[1].value === "/for/"`
55+
* `.a /F\6fR/ .b` escapes are handled and uppercase is normalized.
56+
- Old & Busted:
57+
- `root.nodes[0].nodes[2].type === "tag"`
58+
- `root.nodes[0].nodes[2].value === "F\\6fR"`
59+
- New hotness:
60+
- `root.nodes[0].nodes[1].type === "combinator"`
61+
- `root.nodes[0].nodes[1].value === "/for/"`
62+
- `root.nodes[0].nodes[1].raws.value === "/F\\6fR/"`
63+
64+
### Source position checks and lookups
65+
66+
A new API was added to look up a node based on the source location.
67+
68+
```js
69+
const selectorParser = require("postcss-selector-parser");
70+
// You can find the most specific node for any given character
71+
let combinator = selectorParser.astSync(".a > .b").atPosition(1,4);
72+
combinator.toString() === " > ";
73+
// You can check if a node includes a specific character
74+
// Whitespace surrounding the node that is owned by that node
75+
// is included in the check.
76+
[2,3,4,5,6].map(column => combinator.isAtPosition(1, column));
77+
// => [false, true, true, true, false]
78+
```
79+
80+
# 4.0.0
81+
82+
This release has **BREAKING CHANGES** that were required to fix bugs regarding values with escape sequences. Please read carefully.
83+
84+
* **Identifiers with escapes** - CSS escape sequences are now hidden from the public API by default.
85+
The normal value of a node like a class name or ID, or an aspect of a node such as attribute
86+
selector's value, is unescaped. Escapes representing Non-ascii characters are unescaped into
87+
unicode characters. For example: `bu\tton, .\31 00, #i\2764\FE0Fu, [attr="value is \"quoted\""]`
88+
will parse respectively to the values `button`, `100`, `i❤️u`, `value is "quoted"`.
89+
The original escape sequences for these values can be found in the corresponding property name
90+
in `node.raws`. Where possible, deprecation warnings were added, but the nature
91+
of escape handling makes it impossible to detect what is escaped or not. Our expectation is
92+
that most users are neither expecting nor handling escape sequences in their use of this library,
93+
and so for them, this is a bug fix. Users who are taking care to handle escapes correctly can
94+
now update their code to remove the escape handling and let us do it for them.
95+
96+
* **Mutating values with escapes** - When you make an update to a node property that has escape handling
97+
The value is assumed to be unescaped, and any special characters are escaped automatically and
98+
the corresponding `raws` value is immediately updated. This can result in changes to the original
99+
escape format. Where the exact value of the escape sequence is important there are methods that
100+
allow both values to be set in conjunction. There are a number of new convenience methods for
101+
manipulating values that involve escapes, especially for attributes values where the quote mark
102+
is involved. See https://github.com/postcss/postcss-selector-parser/pull/133 for an extensive
103+
write-up on these changes.
104+
105+
106+
**Upgrade/API Example**
107+
108+
In `3.x` there was no unescape handling and internal consistency of several properties was the caller's job to maintain. It was very easy for the developer
109+
to create a CSS file that did not parse correctly when some types of values
110+
were in use.
111+
112+
```js
113+
const selectorParser = require("postcss-selector-parser");
114+
let attr = selectorParser.attribute({attribute: "id", operator: "=", value: "a-value"});
115+
attr.value; // => "a-value"
116+
attr.toString(); // => [id=a-value]
117+
// Add quotes to an attribute's value.
118+
// All these values have to be set by the caller to be consistent:
119+
// no internal consistency is maintained.
120+
attr.raws.unquoted = attr.value
121+
attr.value = "'" + attr.value + "'";
122+
attr.value; // => "'a-value'"
123+
attr.quoted = true;
124+
attr.toString(); // => "[id='a-value']"
125+
```
126+
127+
In `4.0` there is a convenient API for setting and mutating values
128+
that may need escaping. Especially for attributes.
129+
130+
```js
131+
const selectorParser = require("postcss-selector-parser");
132+
133+
// The constructor requires you specify the exact escape sequence
134+
let className = selectorParser.className({value: "illegal class name", raws: {value: "illegal\\ class\\ name"}});
135+
className.toString(); // => '.illegal\\ class\\ name'
136+
137+
// So it's better to set the value as a property
138+
className = selectorParser.className();
139+
// Most properties that deal with identifiers work like this
140+
className.value = "escape for me";
141+
className.value; // => 'escape for me'
142+
className.toString(); // => '.escape\\ for\\ me'
143+
144+
// emoji and all non-ascii are escaped to ensure it works in every css file.
145+
className.value = "😱🦄😍";
146+
className.value; // => '😱🦄😍'
147+
className.toString(); // => '.\\1F631\\1F984\\1F60D'
148+
149+
// you can control the escape sequence if you want, or do bad bad things
150+
className.setPropertyAndEscape('value', 'xxxx', 'yyyy');
151+
className.value; // => "xxxx"
152+
className.toString(); // => ".yyyy"
153+
154+
// Pass a value directly through to the css output without escaping it.
155+
className.setPropertyWithoutEscape('value', '$REPLACE_ME$');
156+
className.value; // => "$REPLACE_ME$"
157+
className.toString(); // => ".$REPLACE_ME$"
158+
159+
// The biggest changes are to the Attribute class
160+
// passing quoteMark explicitly is required to avoid a deprecation warning.
161+
let attr = selectorParser.attribute({attribute: "id", operator: "=", value: "a-value", quoteMark: null});
162+
attr.toString(); // => "[id=a-value]"
163+
// Get the value with quotes on it and any necessary escapes.
164+
// This is the same as reading attr.value in 3.x.
165+
attr.getQuotedValue(); // => "a-value";
166+
attr.quoteMark; // => null
167+
168+
// Add quotes to an attribute's value.
169+
attr.quoteMark = "'"; // This is all that's required.
170+
attr.toString(); // => "[id='a-value']"
171+
attr.quoted; // => true
172+
// The value is still the same, only the quotes have changed.
173+
attr.value; // => a-value
174+
attr.getQuotedValue(); // => "'a-value'";
175+
176+
// deprecated assignment, no warning because there's no escapes
177+
attr.value = "new-value";
178+
// no quote mark is needed so it is removed
179+
attr.getQuotedValue(); // => "new-value";
180+
181+
// deprecated assignment,
182+
attr.value = "\"a 'single quoted' value\"";
183+
// > (node:27859) DeprecationWarning: Assigning an attribute a value containing characters that might need to be escaped is deprecated. Call attribute.setValue() instead.
184+
attr.getQuotedValue(); // => '"a \'single quoted\' value"';
185+
// quote mark inferred from first and last characters.
186+
attr.quoteMark; // => '"'
187+
188+
// setValue takes options to make manipulating the value simple.
189+
attr.setValue('foo', {smart: true});
190+
// foo doesn't require any escapes or quotes.
191+
attr.toString(); // => '[id=foo]'
192+
attr.quoteMark; // => null
193+
194+
// An explicit quote mark can be specified
195+
attr.setValue('foo', {quoteMark: '"'});
196+
attr.toString(); // => '[id="foo"]'
197+
198+
// preserves quote mark by default
199+
attr.setValue('bar');
200+
attr.toString(); // => '[id="bar"]'
201+
attr.quoteMark = null;
202+
attr.toString(); // => '[id=bar]'
203+
204+
// with no arguments, it preserves quote mark even when it's not a great idea
205+
attr.setValue('a value \n that should be quoted');
206+
attr.toString(); // => '[id=a\\ value\\ \\A\\ that\\ should\\ be\\ quoted]'
207+
208+
// smart preservation with a specified default
209+
attr.setValue('a value \n that should be quoted', {smart: true, preferCurrentQuoteMark: true, quoteMark: "'"});
210+
// => "[id='a value \\A that should be quoted']"
211+
attr.quoteMark = '"';
212+
// => '[id="a value \\A that should be quoted"]'
213+
214+
// this keeps double quotes because it wants to quote the value and the existing value has double quotes.
215+
attr.setValue('this should be quoted', {smart: true, preferCurrentQuoteMark: true, quoteMark: "'"});
216+
// => '[id="this should be quoted"]'
217+
218+
// picks single quotes because the value has double quotes
219+
attr.setValue('a "double quoted" value', {smart: true, preferCurrentQuoteMark: true, quoteMark: "'"});
220+
// => "[id='a "double quoted" value']"
221+
222+
// setPropertyAndEscape lets you do anything you want. Even things that are a bad idea and illegal.
223+
attr.setPropertyAndEscape('value', 'xxxx', 'the password is 42');
224+
attr.value; // => "xxxx"
225+
attr.toString(); // => "[id=the password is 42]"
226+
227+
// Pass a value directly through to the css output without escaping it.
228+
attr.setPropertyWithoutEscape('value', '$REPLACEMENT$');
229+
attr.value; // => "$REPLACEMENT$"
230+
attr.toString(); // => "[id=$REPLACEMENT$]"
231+
```
232+
1233
# 3.1.2
2234

3235
* Fix: Removed dot-prop dependency since it's no longer written in es5.

integration/stylelint

Submodule stylelint added at 923878c

integration_test.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/bash
2+
if [[ $INTEGRATION == "false" ]]; then
3+
exit 0;
4+
fi
5+
git submodule update --init --recursive
6+
npm link
7+
cd integration/stylelint
8+
npm link postcss-selector-parser
9+
npm install
10+
NODE_VERSION=`node -e "console.log(process.version.replace(/v(\d).*/,function(m){return m[1]}))"`
11+
CI="tests $NODE_VERSION"
12+
npm run jest -- --maxWorkers=2 --testPathIgnorePatterns lib/__tests__/standalone-cache.test.js || exit $?

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)