postcss-selector-parser
Advanced tools
Comparing version
44
API.md
@@ -81,2 +81,14 @@ # API Documentation | ||
Notes: | ||
* **Descendant Combinators** The value of descendant combinators created by the | ||
parser always just a single space (`" "`). For descendant selectors with no | ||
comments, additional space is now stored in `node.spaces.before`. Depending | ||
on the location of comments, additional spaces may be stored in | ||
`node.raws.spaces.before`, `node.raws.spaces.after`, or `node.raws.value`. | ||
* **Named Combinators** Although, nonstandard and unlikely to ever become a standard, | ||
named combinators like `/deep/` and `/for/` are parsed as combinators. The | ||
`node.value` is name after being unescaped and normalized as lowercase. The | ||
original value for the combinator name is stored in `node.raws.value`. | ||
### `parser.comment([props])` | ||
@@ -279,2 +291,13 @@ | ||
### `node.isAtPosition(line, column)` | ||
Return a `boolean` indicating whether this node includes the character at the | ||
position of the given line and column. Returns `undefined` if the nodes lack | ||
sufficient source metadata to determine the position. | ||
Arguments: | ||
* `line`: 1-index based line number relative to the start of the selector. | ||
* `column`: 1-index based column number relative to the start of the selector. | ||
### `node.spaces` | ||
@@ -290,3 +313,3 @@ | ||
However, *combinating* spaces will form a `combinator` node: | ||
For descendent selectors, the value is always a single space. | ||
@@ -297,5 +320,3 @@ ```css | ||
A `combinator` node may only have the `spaces` property set if the combinator | ||
value is a non-whitespace character, such as `+`, `~` or `>`. Otherwise, the | ||
combinator value will contain all of the spaces between selectors. | ||
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. | ||
@@ -376,2 +397,15 @@ ### `node.source` | ||
### `container.atPosition(line, column)` | ||
Returns the node at the source position `index`. | ||
```js | ||
selector.at(0) === selector.first; | ||
selector.at(0) === selector.nodes[0]; | ||
``` | ||
Arguments: | ||
* `index`: The index of the node to return. | ||
### `container.index(node)` | ||
@@ -559,3 +593,3 @@ | ||
A selector node represents a single compound selector. For example, this | ||
A selector node represents a single complex selector. For example, this | ||
selector string `h1 h2 h3, [href] > p`, is represented as two selector nodes. | ||
@@ -562,0 +596,0 @@ It has no special functionality of its own. |
232
CHANGELOG.md
@@ -0,1 +1,233 @@ | ||
# 5.0.0-rc.0 | ||
This release has **BREAKING CHANGES** that were required to fix regressions | ||
in 4.0.0 and to make the Combinator Node API consistent for all combinator | ||
types. Please read carefully. | ||
## Summary of Changes | ||
* 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`. | ||
### Changes to the Descendent Combinator | ||
In prior releases, the value of a descendant combinator with multiple spaces included all the spaces. | ||
* `.a .b`: Extra spaces are now stored as space before. | ||
- Old & Busted: | ||
- `combinator.value === " "` | ||
- New hotness: | ||
- `combinator.value === " " && combinator.spaces.before === " "` | ||
* `.a /*comment*/.b`: A comment at the end of the combinator causes extra space to become after space. | ||
- Old & Busted: | ||
- `combinator.value === " "` | ||
- `combinator.raws.value === " /*comment/"` | ||
- New hotness: | ||
- `combinator.value === " "` | ||
- `combinator.spaces.after === " "` | ||
- `combinator.raws.spaces.after === " /*comment*/"` | ||
* `.a<newline>.b`: whitespace that doesn't start or end with a single space character is stored as a raw value. | ||
- Old & Busted: | ||
- `combinator.value === "\n"` | ||
- `combinator.raws.value === undefined` | ||
- New hotness: | ||
- `combinator.value === " "` | ||
- `combinator.raws.value === "\n"` | ||
### Support for "Named Combinators" | ||
Although, nonstandard and unlikely to ever become a standard, combinators like `/deep/` and `/for/` are now properly supported. | ||
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. | ||
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 `"/"`. | ||
* `.a /for/ .b` is parsed as a combinator. | ||
- Old & Busted: | ||
- `root.nodes[0].nodes[1].type === "tag"` | ||
- `root.nodes[0].nodes[1].value === "/"` | ||
- New hotness: | ||
- `root.nodes[0].nodes[1].type === "combinator"` | ||
- `root.nodes[0].nodes[1].value === "/for/"` | ||
* `.a /F\6fR/ .b` escapes are handled and uppercase is normalized. | ||
- Old & Busted: | ||
- `root.nodes[0].nodes[2].type === "tag"` | ||
- `root.nodes[0].nodes[2].value === "F\\6fR"` | ||
- New hotness: | ||
- `root.nodes[0].nodes[1].type === "combinator"` | ||
- `root.nodes[0].nodes[1].value === "/for/"` | ||
- `root.nodes[0].nodes[1].raws.value === "/F\\6fR/"` | ||
### Source position checks and lookups | ||
A new API was added to look up a node based on the source location. | ||
```js | ||
const selectorParser = require("postcss-selector-parser"); | ||
// You can find the most specific node for any given character | ||
let combinator = selectorParser.astSync(".a > .b").atPosition(1,4); | ||
combinator.toString() === " > "; | ||
// You can check if a node includes a specific character | ||
// Whitespace surrounding the node that is owned by that node | ||
// is included in the check. | ||
[2,3,4,5,6].map(column => combinator.isAtPosition(1, column)); | ||
// => [false, true, true, true, false] | ||
``` | ||
# 4.0.0 | ||
This release has **BREAKING CHANGES** that were required to fix bugs regarding values with escape sequences. Please read carefully. | ||
* **Identifiers with escapes** - CSS escape sequences are now hidden from the public API by default. | ||
The normal value of a node like a class name or ID, or an aspect of a node such as attribute | ||
selector's value, is unescaped. Escapes representing Non-ascii characters are unescaped into | ||
unicode characters. For example: `bu\tton, .\31 00, #i\2764\FE0Fu, [attr="value is \"quoted\""]` | ||
will parse respectively to the values `button`, `100`, `i❤️u`, `value is "quoted"`. | ||
The original escape sequences for these values can be found in the corresponding property name | ||
in `node.raws`. Where possible, deprecation warnings were added, but the nature | ||
of escape handling makes it impossible to detect what is escaped or not. Our expectation is | ||
that most users are neither expecting nor handling escape sequences in their use of this library, | ||
and so for them, this is a bug fix. Users who are taking care to handle escapes correctly can | ||
now update their code to remove the escape handling and let us do it for them. | ||
* **Mutating values with escapes** - When you make an update to a node property that has escape handling | ||
The value is assumed to be unescaped, and any special characters are escaped automatically and | ||
the corresponding `raws` value is immediately updated. This can result in changes to the original | ||
escape format. Where the exact value of the escape sequence is important there are methods that | ||
allow both values to be set in conjunction. There are a number of new convenience methods for | ||
manipulating values that involve escapes, especially for attributes values where the quote mark | ||
is involved. See https://github.com/postcss/postcss-selector-parser/pull/133 for an extensive | ||
write-up on these changes. | ||
**Upgrade/API Example** | ||
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 | ||
to create a CSS file that did not parse correctly when some types of values | ||
were in use. | ||
```js | ||
const selectorParser = require("postcss-selector-parser"); | ||
let attr = selectorParser.attribute({attribute: "id", operator: "=", value: "a-value"}); | ||
attr.value; // => "a-value" | ||
attr.toString(); // => [id=a-value] | ||
// Add quotes to an attribute's value. | ||
// All these values have to be set by the caller to be consistent: | ||
// no internal consistency is maintained. | ||
attr.raws.unquoted = attr.value | ||
attr.value = "'" + attr.value + "'"; | ||
attr.value; // => "'a-value'" | ||
attr.quoted = true; | ||
attr.toString(); // => "[id='a-value']" | ||
``` | ||
In `4.0` there is a convenient API for setting and mutating values | ||
that may need escaping. Especially for attributes. | ||
```js | ||
const selectorParser = require("postcss-selector-parser"); | ||
// The constructor requires you specify the exact escape sequence | ||
let className = selectorParser.className({value: "illegal class name", raws: {value: "illegal\\ class\\ name"}}); | ||
className.toString(); // => '.illegal\\ class\\ name' | ||
// So it's better to set the value as a property | ||
className = selectorParser.className(); | ||
// Most properties that deal with identifiers work like this | ||
className.value = "escape for me"; | ||
className.value; // => 'escape for me' | ||
className.toString(); // => '.escape\\ for\\ me' | ||
// emoji and all non-ascii are escaped to ensure it works in every css file. | ||
className.value = "😱🦄😍"; | ||
className.value; // => '😱🦄😍' | ||
className.toString(); // => '.\\1F631\\1F984\\1F60D' | ||
// you can control the escape sequence if you want, or do bad bad things | ||
className.setPropertyAndEscape('value', 'xxxx', 'yyyy'); | ||
className.value; // => "xxxx" | ||
className.toString(); // => ".yyyy" | ||
// Pass a value directly through to the css output without escaping it. | ||
className.setPropertyWithoutEscape('value', '$REPLACE_ME$'); | ||
className.value; // => "$REPLACE_ME$" | ||
className.toString(); // => ".$REPLACE_ME$" | ||
// The biggest changes are to the Attribute class | ||
// passing quoteMark explicitly is required to avoid a deprecation warning. | ||
let attr = selectorParser.attribute({attribute: "id", operator: "=", value: "a-value", quoteMark: null}); | ||
attr.toString(); // => "[id=a-value]" | ||
// Get the value with quotes on it and any necessary escapes. | ||
// This is the same as reading attr.value in 3.x. | ||
attr.getQuotedValue(); // => "a-value"; | ||
attr.quoteMark; // => null | ||
// Add quotes to an attribute's value. | ||
attr.quoteMark = "'"; // This is all that's required. | ||
attr.toString(); // => "[id='a-value']" | ||
attr.quoted; // => true | ||
// The value is still the same, only the quotes have changed. | ||
attr.value; // => a-value | ||
attr.getQuotedValue(); // => "'a-value'"; | ||
// deprecated assignment, no warning because there's no escapes | ||
attr.value = "new-value"; | ||
// no quote mark is needed so it is removed | ||
attr.getQuotedValue(); // => "new-value"; | ||
// deprecated assignment, | ||
attr.value = "\"a 'single quoted' value\""; | ||
// > (node:27859) DeprecationWarning: Assigning an attribute a value containing characters that might need to be escaped is deprecated. Call attribute.setValue() instead. | ||
attr.getQuotedValue(); // => '"a \'single quoted\' value"'; | ||
// quote mark inferred from first and last characters. | ||
attr.quoteMark; // => '"' | ||
// setValue takes options to make manipulating the value simple. | ||
attr.setValue('foo', {smart: true}); | ||
// foo doesn't require any escapes or quotes. | ||
attr.toString(); // => '[id=foo]' | ||
attr.quoteMark; // => null | ||
// An explicit quote mark can be specified | ||
attr.setValue('foo', {quoteMark: '"'}); | ||
attr.toString(); // => '[id="foo"]' | ||
// preserves quote mark by default | ||
attr.setValue('bar'); | ||
attr.toString(); // => '[id="bar"]' | ||
attr.quoteMark = null; | ||
attr.toString(); // => '[id=bar]' | ||
// with no arguments, it preserves quote mark even when it's not a great idea | ||
attr.setValue('a value \n that should be quoted'); | ||
attr.toString(); // => '[id=a\\ value\\ \\A\\ that\\ should\\ be\\ quoted]' | ||
// smart preservation with a specified default | ||
attr.setValue('a value \n that should be quoted', {smart: true, preferCurrentQuoteMark: true, quoteMark: "'"}); | ||
// => "[id='a value \\A that should be quoted']" | ||
attr.quoteMark = '"'; | ||
// => '[id="a value \\A that should be quoted"]' | ||
// this keeps double quotes because it wants to quote the value and the existing value has double quotes. | ||
attr.setValue('this should be quoted', {smart: true, preferCurrentQuoteMark: true, quoteMark: "'"}); | ||
// => '[id="this should be quoted"]' | ||
// picks single quotes because the value has double quotes | ||
attr.setValue('a "double quoted" value', {smart: true, preferCurrentQuoteMark: true, quoteMark: "'"}); | ||
// => "[id='a "double quoted" value']" | ||
// setPropertyAndEscape lets you do anything you want. Even things that are a bad idea and illegal. | ||
attr.setPropertyAndEscape('value', 'xxxx', 'the password is 42'); | ||
attr.value; // => "xxxx" | ||
attr.toString(); // => "[id=the password is 42]" | ||
// Pass a value directly through to the css output without escaping it. | ||
attr.setPropertyWithoutEscape('value', '$REPLACEMENT$'); | ||
attr.value; // => "$REPLACEMENT$" | ||
attr.toString(); // => "[id=$REPLACEMENT$]" | ||
``` | ||
# 3.1.2 | ||
@@ -2,0 +234,0 @@ |
@@ -95,2 +95,16 @@ 'use strict'; | ||
function tokenStart(token) { | ||
return { | ||
line: token[_tokenize.FIELDS.START_LINE], | ||
column: token[_tokenize.FIELDS.START_COL] | ||
}; | ||
} | ||
function tokenEnd(token) { | ||
return { | ||
line: token[_tokenize.FIELDS.END_LINE], | ||
column: token[_tokenize.FIELDS.END_COL] | ||
}; | ||
} | ||
function getSource(startLine, startColumn, endLine, endColumn) { | ||
@@ -113,2 +127,9 @@ return { | ||
function getTokenSourceSpan(startToken, endToken) { | ||
if (!startToken) { | ||
return undefined; | ||
} | ||
return getSource(startToken[_tokenize.FIELDS.START_LINE], startToken[_tokenize.FIELDS.START_COL], endToken[_tokenize.FIELDS.END_LINE], endToken[_tokenize.FIELDS.END_COL]); | ||
} | ||
function unescapeProp(node, prop) { | ||
@@ -129,16 +150,2 @@ var value = node[prop]; | ||
function convertWhitespaceNodesToSpace(nodes) { | ||
var space = ""; | ||
var rawSpace = ""; | ||
nodes.forEach(function (n) { | ||
space += n.spaces.before + n.spaces.after; | ||
rawSpace += n.toString(); | ||
}); | ||
if (rawSpace === space) { | ||
rawSpace = undefined; | ||
} | ||
var result = { space: space, rawSpace: rawSpace }; | ||
return result; | ||
} | ||
var Parser = function () { | ||
@@ -153,9 +160,3 @@ function Parser(rule) { | ||
this.position = 0; | ||
this.root = new _root2.default(); | ||
this.root.errorGenerator = this._errorGenerator(); | ||
var selector = new _selector2.default(); | ||
this.root.append(selector); | ||
this.current = selector; | ||
this.css = typeof this.rule === 'string' ? this.rule : this.rule.selector; | ||
@@ -169,2 +170,10 @@ | ||
var rootSource = getTokenSourceSpan(this.tokens[0], this.tokens[this.tokens.length - 1]); | ||
this.root = new _root2.default({ source: rootSource }); | ||
this.root.errorGenerator = this._errorGenerator(); | ||
var selector = new _selector2.default({ source: { start: { line: 1, column: 1 } } }); | ||
this.root.append(selector); | ||
this.current = selector; | ||
this.loop(); | ||
@@ -477,5 +486,58 @@ } | ||
Parser.prototype.combinator = function combinator() { | ||
/** | ||
* | ||
* @param {*} nodes | ||
*/ | ||
Parser.prototype.convertWhitespaceNodesToSpace = function convertWhitespaceNodesToSpace(nodes) { | ||
var _this2 = this; | ||
var requiredSpace = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; | ||
var space = ""; | ||
var rawSpace = ""; | ||
nodes.forEach(function (n) { | ||
var spaceBefore = _this2.lossySpace(n.spaces.before, requiredSpace); | ||
var rawSpaceBefore = _this2.lossySpace(n.rawSpaceBefore, requiredSpace); | ||
space += spaceBefore + _this2.lossySpace(n.spaces.after, requiredSpace && spaceBefore.length === 0); | ||
rawSpace += spaceBefore + n.value + _this2.lossySpace(n.rawSpaceAfter, requiredSpace && rawSpaceBefore.length === 0); | ||
}); | ||
if (rawSpace === space) { | ||
rawSpace = undefined; | ||
} | ||
var result = { space: space, rawSpace: rawSpace }; | ||
return result; | ||
}; | ||
Parser.prototype.isNamedCombinator = function isNamedCombinator() { | ||
var position = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.position; | ||
return this.tokens[position + 0] && this.tokens[position + 0][_tokenize.FIELDS.TYPE] === tokens.slash && this.tokens[position + 1] && this.tokens[position + 1][_tokenize.FIELDS.TYPE] === tokens.word && this.tokens[position + 2] && this.tokens[position + 2][_tokenize.FIELDS.TYPE] === tokens.slash; | ||
}; | ||
Parser.prototype.namedCombinator = function namedCombinator() { | ||
if (this.isNamedCombinator()) { | ||
var nameRaw = this.content(this.tokens[this.position + 1]); | ||
var name = (0, _util.unesc)(nameRaw).toLowerCase(); | ||
var raws = {}; | ||
if (name !== nameRaw) { | ||
raws.value = '/' + nameRaw + '/'; | ||
} | ||
var node = new _combinator2.default({ | ||
value: '/' + name + '/', | ||
source: getSource(this.currToken[_tokenize.FIELDS.START_LINE], this.currToken[_tokenize.FIELDS.START_COL], this.tokens[this.position + 2][_tokenize.FIELDS.END_LINE], this.tokens[this.position + 2][_tokenize.FIELDS.END_COL]), | ||
sourceIndex: this.currToken[_tokenize.FIELDS.START_POS], | ||
raws: raws | ||
}); | ||
this.position = this.position + 3; | ||
return node; | ||
} else { | ||
this.unexpected(); | ||
} | ||
}; | ||
Parser.prototype.combinator = function combinator() { | ||
var _this3 = this; | ||
if (this.content() === '|') { | ||
@@ -492,3 +554,3 @@ return this.namespace(); | ||
if (last) { | ||
var _convertWhitespaceNod = convertWhitespaceNodesToSpace(nodes), | ||
var _convertWhitespaceNod = this.convertWhitespaceNodesToSpace(nodes), | ||
space = _convertWhitespaceNod.space, | ||
@@ -503,3 +565,3 @@ rawSpace = _convertWhitespaceNod.rawSpace; | ||
nodes.forEach(function (n) { | ||
return _this2.newNode(n); | ||
return _this3.newNode(n); | ||
}); | ||
@@ -511,34 +573,67 @@ } | ||
var spaceBeforeCombinator = undefined; | ||
if (nextSigTokenPos > this.position && this.tokens[nextSigTokenPos][_tokenize.FIELDS.TYPE] === tokens.combinator) { | ||
spaceBeforeCombinator = convertWhitespaceNodesToSpace(this.parseWhitespaceEquivalentTokens(nextSigTokenPos)); | ||
var firstToken = this.currToken; | ||
var spaceOrDescendantSelectorNodes = undefined; | ||
if (nextSigTokenPos > this.position) { | ||
spaceOrDescendantSelectorNodes = this.parseWhitespaceEquivalentTokens(nextSigTokenPos); | ||
} | ||
var current = this.currToken; | ||
var node = new _combinator2.default({ | ||
value: '', | ||
source: getTokenSource(current), | ||
sourceIndex: current[_tokenize.FIELDS.START_POS] | ||
}); | ||
while (this.position < this.tokens.length && this.currToken && (this.currToken[_tokenize.FIELDS.TYPE] === tokens.space || this.currToken[_tokenize.FIELDS.TYPE] === tokens.combinator)) { | ||
var content = this.content(); | ||
if (this.nextToken && this.nextToken[_tokenize.FIELDS.TYPE] === tokens.combinator) { | ||
node.spaces.before = this.optionalSpace(content); | ||
node.source = getTokenSource(this.nextToken); | ||
node.sourceIndex = this.nextToken[_tokenize.FIELDS.START_POS]; | ||
} else if (this.prevToken && this.prevToken[_tokenize.FIELDS.TYPE] === tokens.combinator) { | ||
node.spaces.after = this.optionalSpace(content); | ||
} else if (this.currToken[_tokenize.FIELDS.TYPE] === tokens.combinator) { | ||
node.value = content; | ||
} else if (this.currToken[_tokenize.FIELDS.TYPE] === tokens.space) { | ||
node.value = this.requiredSpace(content); | ||
} | ||
var node = void 0; | ||
if (this.isNamedCombinator()) { | ||
node = this.namedCombinator(); | ||
} else if (this.currToken[_tokenize.FIELDS.TYPE] === tokens.combinator) { | ||
node = new _combinator2.default({ | ||
value: this.content(), | ||
source: getTokenSource(this.currToken), | ||
sourceIndex: this.currToken[_tokenize.FIELDS.START_POS] | ||
}); | ||
this.position++; | ||
} else if (WHITESPACE_TOKENS[this.currToken[_tokenize.FIELDS.TYPE]]) { | ||
// pass | ||
} else if (!spaceOrDescendantSelectorNodes) { | ||
this.unexpected(); | ||
} | ||
if (spaceBeforeCombinator) { | ||
if (spaceBeforeCombinator.rawSpace !== undefined) { | ||
node.rawSpaceBefore = spaceBeforeCombinator.rawSpace + node.rawSpaceBefore; | ||
if (node) { | ||
if (spaceOrDescendantSelectorNodes) { | ||
var _convertWhitespaceNod2 = this.convertWhitespaceNodesToSpace(spaceOrDescendantSelectorNodes), | ||
_space = _convertWhitespaceNod2.space, | ||
_rawSpace = _convertWhitespaceNod2.rawSpace; | ||
node.spaces.before = _space; | ||
node.rawSpaceBefore = _rawSpace; | ||
} | ||
node.spaces.before = spaceBeforeCombinator.space + node.spaces.before; | ||
} else { | ||
// descendant combinator | ||
var _convertWhitespaceNod3 = this.convertWhitespaceNodesToSpace(spaceOrDescendantSelectorNodes, true), | ||
_space2 = _convertWhitespaceNod3.space, | ||
_rawSpace2 = _convertWhitespaceNod3.rawSpace; | ||
if (!_rawSpace2) { | ||
_rawSpace2 = _space2; | ||
} | ||
var spaces = {}; | ||
var raws = { spaces: {} }; | ||
if (_space2.endsWith(' ') && _rawSpace2.endsWith(' ')) { | ||
spaces.before = _space2.slice(0, _space2.length - 1); | ||
raws.spaces.before = _rawSpace2.slice(0, _rawSpace2.length - 1); | ||
} else if (_space2.startsWith(' ') && _rawSpace2.startsWith(' ')) { | ||
spaces.after = _space2.slice(1); | ||
raws.spaces.after = _rawSpace2.slice(1); | ||
} else { | ||
raws.value = _rawSpace2; | ||
} | ||
node = new _combinator2.default({ | ||
value: ' ', | ||
source: getTokenSourceSpan(firstToken, this.tokens[this.position - 1]), | ||
sourceIndex: firstToken[_tokenize.FIELDS.START_POS], | ||
spaces: spaces, | ||
raws: raws | ||
}); | ||
} | ||
if (this.currToken[_tokenize.FIELDS.TYPE] === tokens.space) { | ||
node.spaces.after = this.optionalSpace(this.content()); | ||
this.position++; | ||
} | ||
return this.newNode(node); | ||
@@ -553,3 +648,4 @@ }; | ||
} | ||
var selector = new _selector2.default(); | ||
this.current._inferEndPosition(); | ||
var selector = new _selector2.default({ source: { start: tokenStart(this.tokens[this.position + 1]) } }); | ||
this.current.parent.append(selector); | ||
@@ -588,2 +684,6 @@ this.current = selector; | ||
Parser.prototype.unexpected = function unexpected() { | ||
return this.error('Unexpected \'' + this.content() + '\'. Escaping special characters with \\ may help.', this.currToken[_tokenize.FIELDS.START_POS]); | ||
}; | ||
Parser.prototype.namespace = function namespace() { | ||
@@ -619,21 +719,21 @@ var before = this.prevToken && this.content(this.prevToken) || true; | ||
var last = this.current.last; | ||
var balanced = 1; | ||
var unbalanced = 1; | ||
this.position++; | ||
if (last && last.type === types.PSEUDO) { | ||
var selector = new _selector2.default(); | ||
var selector = new _selector2.default({ source: { start: tokenStart(this.tokens[this.position - 1]) } }); | ||
var cache = this.current; | ||
last.append(selector); | ||
this.current = selector; | ||
while (this.position < this.tokens.length && balanced) { | ||
while (this.position < this.tokens.length && unbalanced) { | ||
if (this.currToken[_tokenize.FIELDS.TYPE] === tokens.openParenthesis) { | ||
balanced++; | ||
unbalanced++; | ||
} | ||
if (this.currToken[_tokenize.FIELDS.TYPE] === tokens.closeParenthesis) { | ||
balanced--; | ||
unbalanced--; | ||
} | ||
if (balanced) { | ||
if (unbalanced) { | ||
this.parse(); | ||
} else { | ||
selector.parent.source.end.line = this.currToken[3]; | ||
selector.parent.source.end.column = this.currToken[4]; | ||
this.current.source.end = tokenEnd(this.currToken); | ||
this.current.parent.source.end = tokenEnd(this.currToken); | ||
this.position++; | ||
@@ -649,8 +749,8 @@ } | ||
var parenEnd = void 0; | ||
while (this.position < this.tokens.length && balanced) { | ||
while (this.position < this.tokens.length && unbalanced) { | ||
if (this.currToken[_tokenize.FIELDS.TYPE] === tokens.openParenthesis) { | ||
balanced++; | ||
unbalanced++; | ||
} | ||
if (this.currToken[_tokenize.FIELDS.TYPE] === tokens.closeParenthesis) { | ||
balanced--; | ||
unbalanced--; | ||
} | ||
@@ -671,3 +771,3 @@ parenEnd = this.currToken; | ||
} | ||
if (balanced) { | ||
if (unbalanced) { | ||
return this.expected('closing parenthesis', this.currToken[_tokenize.FIELDS.START_POS]); | ||
@@ -678,3 +778,3 @@ } | ||
Parser.prototype.pseudo = function pseudo() { | ||
var _this3 = this; | ||
var _this4 = this; | ||
@@ -693,10 +793,10 @@ var pseudoStr = ''; | ||
pseudoStr += first; | ||
_this3.newNode(new _pseudo2.default({ | ||
_this4.newNode(new _pseudo2.default({ | ||
value: pseudoStr, | ||
source: getSource(startingToken[1], startingToken[2], _this3.currToken[3], _this3.currToken[4]), | ||
source: getTokenSourceSpan(startingToken, _this4.currToken), | ||
sourceIndex: startingToken[_tokenize.FIELDS.START_POS] | ||
})); | ||
if (length > 1 && _this3.nextToken && _this3.nextToken[_tokenize.FIELDS.TYPE] === tokens.openParenthesis) { | ||
_this3.error('Misplaced parenthesis.', { | ||
index: _this3.nextToken[_tokenize.FIELDS.START_POS] | ||
if (length > 1 && _this4.nextToken && _this4.nextToken[_tokenize.FIELDS.TYPE] === tokens.openParenthesis) { | ||
_this4.error('Misplaced parenthesis.', { | ||
index: _this4.nextToken[_tokenize.FIELDS.START_POS] | ||
}); | ||
@@ -750,3 +850,3 @@ } | ||
Parser.prototype.splitWord = function splitWord(namespace, firstCallback) { | ||
var _this4 = this; | ||
var _this5 = this; | ||
@@ -782,6 +882,6 @@ var nextToken = this.nextToken; | ||
if (i === 0 && firstCallback) { | ||
return firstCallback.call(_this4, value, indices.length); | ||
return firstCallback.call(_this5, value, indices.length); | ||
} | ||
var node = void 0; | ||
var current = _this4.currToken; | ||
var current = _this5.currToken; | ||
var sourceIndex = current[_tokenize.FIELDS.START_POS] + indices[i]; | ||
@@ -812,3 +912,3 @@ var source = getSource(current[1], current[2] + ind, current[3], current[2] + (index - 1)); | ||
} | ||
_this4.newNode(node, namespace); | ||
_this5.newNode(node, namespace); | ||
// Ensure that the namespace is used only once | ||
@@ -833,2 +933,3 @@ namespace = null; | ||
} | ||
this.current._inferEndPosition(); | ||
return this.root; | ||
@@ -874,2 +975,3 @@ }; | ||
break; | ||
case tokens.slash: | ||
case tokens.combinator: | ||
@@ -886,2 +988,4 @@ this.combinator(); | ||
this.missingBackslash(); | ||
default: | ||
this.unexpected(); | ||
} | ||
@@ -914,2 +1018,10 @@ }; | ||
Parser.prototype.lossySpace = function lossySpace(space, required) { | ||
if (this.options.lossy) { | ||
return required ? ' ' : ''; | ||
} else { | ||
return space; | ||
} | ||
}; | ||
Parser.prototype.parseParenthesisToken = function parseParenthesisToken(token) { | ||
@@ -916,0 +1028,0 @@ var content = this.content(token); |
@@ -139,2 +139,50 @@ 'use strict'; | ||
Container.prototype._findChildAtPosition = function _findChildAtPosition(line, col) { | ||
var found = undefined; | ||
this.each(function (node) { | ||
if (node.atPosition) { | ||
var foundChild = node.atPosition(line, col); | ||
if (foundChild) { | ||
found = foundChild; | ||
return false; | ||
} | ||
} else if (node.isAtPosition(line, col)) { | ||
found = node; | ||
return false; | ||
} | ||
}); | ||
return found; | ||
}; | ||
/** | ||
* Return the most specific node at the line and column number given. | ||
* The source location is based on the original parsed location, locations aren't | ||
* updated as selector nodes are mutated. | ||
* | ||
* Note that this location is relative to the location of the first character | ||
* of the selector, and not the location of the selector in the overall document | ||
* when used in conjunction with postcss. | ||
* | ||
* If not found, returns undefined. | ||
* @param {number} line The line number of the node to find. (1-based index) | ||
* @param {number} col The column number of the node to find. (1-based index) | ||
*/ | ||
Container.prototype.atPosition = function atPosition(line, col) { | ||
if (this.isAtPosition(line, col)) { | ||
return this._findChildAtPosition(line, col) || this; | ||
} else { | ||
return undefined; | ||
} | ||
}; | ||
Container.prototype._inferEndPosition = function _inferEndPosition() { | ||
if (this.last && this.last.source && this.last.source.end) { | ||
this.source = this.source || {}; | ||
this.source.end = this.source.end || {}; | ||
Object.assign(this.source.end, this.last.source.end); | ||
} | ||
}; | ||
Container.prototype.each = function each(callback) { | ||
@@ -141,0 +189,0 @@ if (!this.lastEach) { |
@@ -150,2 +150,28 @@ 'use strict'; | ||
/** | ||
* | ||
* @param {number} line The number (starting with 1) | ||
* @param {number} column The column number (starting with 1) | ||
*/ | ||
Node.prototype.isAtPosition = function isAtPosition(line, column) { | ||
if (this.source && this.source.start && this.source.end) { | ||
if (this.source.start.line > line) { | ||
return false; | ||
} | ||
if (this.source.end.line < line) { | ||
return false; | ||
} | ||
if (this.source.start.line === line && this.source.start.column > column) { | ||
return false; | ||
} | ||
if (this.source.end.line === line && this.source.end.column < column) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
return undefined; | ||
}; | ||
Node.prototype.stringifyProperty = function stringifyProperty(name) { | ||
@@ -152,0 +178,0 @@ return this.raws && this.raws[name] || this[name]; |
@@ -147,3 +147,3 @@ 'use strict'; | ||
endLine = line; | ||
endColumn = start - offset; | ||
endColumn = next - offset - 1; | ||
end = next; | ||
@@ -234,2 +234,8 @@ break; | ||
endColumn = next - nextOffset; | ||
} else if (code === t.slash) { | ||
next = start; | ||
tokenType = code; | ||
endLine = line; | ||
endColumn = start - offset; | ||
end = next + 1; | ||
} else { | ||
@@ -236,0 +242,0 @@ next = consumeWord(css, start); |
@@ -32,2 +32,11 @@ 'use strict'; | ||
var _stripComments = require('./stripComments'); | ||
Object.defineProperty(exports, 'stripComments', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_stripComments).default; | ||
} | ||
}); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } |
{ | ||
"name": "postcss-selector-parser", | ||
"version": "4.0.0", | ||
"version": "5.0.0-rc.0", | ||
"devDependencies": { | ||
@@ -37,3 +37,3 @@ "ava": "^0.24.0", | ||
"pretest": "eslint src", | ||
"prepublish": "del-cli dist && BABEL_ENV=publish babel src --out-dir dist --ignore /__tests__/", | ||
"prepare": "del-cli dist && BABEL_ENV=publish babel src --out-dir dist --ignore /__tests__/", | ||
"lintfix": "eslint --fix src", | ||
@@ -51,3 +51,3 @@ "report": "nyc report --reporter=html", | ||
"engines": { | ||
"node": ">=4" | ||
"node": ">=6" | ||
}, | ||
@@ -54,0 +54,0 @@ "homepage": "https://github.com/postcss/postcss-selector-parser", |
@@ -168,2 +168,9 @@ // Type definitions for postcss-selector-parser 2.2.3 | ||
/** | ||
* Return whether this node includes the character at the position of the given line and column. | ||
* Returns undefined if the nodes lack sufficient source metadata to determine the position. | ||
* @param line 1-index based line number relative to the start of the selector. | ||
* @param column 1-index based column number relative to the start of the selector. | ||
*/ | ||
isAtPosition(line: number, column: number): boolean | undefined; | ||
/** | ||
* Some non-standard syntax doesn't follow normal escaping rules for css, | ||
@@ -205,2 +212,16 @@ * this allows the escaped value to be specified directly, allowing illegal characters to be | ||
at(index: number): Node; | ||
/** | ||
* Return the most specific node at the line and column number given. | ||
* The source location is based on the original parsed location, locations aren't | ||
* updated as selector nodes are mutated. | ||
* | ||
* Note that this location is relative to the location of the first character | ||
* of the selector, and not the location of the selector in the overall document | ||
* when used in conjunction with postcss. | ||
* | ||
* If not found, returns undefined. | ||
* @param line The line number of the node to find. (1-based index) | ||
* @param col The column number of the node to find. (1-based index) | ||
*/ | ||
atPosition(line: number, column: number): Node; | ||
index(child: Node): number; | ||
@@ -264,2 +285,3 @@ readonly first: Node; | ||
error(message: string, options?: ErrorOptions): Error; | ||
nodeAt(line: number, column: number): Node | ||
} | ||
@@ -266,0 +288,0 @@ function root(opts: ContainerOptions): Root; |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
195351
12.34%36
2.86%3352
6.85%1
Infinity%