@vltpkg/dss-parser 1.3.7 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/grammar.d.ts +15 -0
- package/dist/grammar.js +86 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +99 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,3 +14,29 @@ import { parse } from '@vltpkg/dss-parser'
|
|
|
14
14
|
// Parse a selector string into an AST
|
|
15
15
|
const ast = parse(':root > *')
|
|
16
16
|
```
|
|
17
|
+
|
|
18
|
+
## Validation
|
|
19
|
+
|
|
20
|
+
`parse()` throws a `SyntaxError` on invalid DSS, with `cause.code`
|
|
21
|
+
`EQUERY` and the offending input in `cause.found`. Rejected:
|
|
22
|
+
|
|
23
|
+
- malformed syntax, e.g. `:outdated(`, `[name=`
|
|
24
|
+
- empty / whitespace-only queries and empty selectors (`#a,`,
|
|
25
|
+
`:not()`)
|
|
26
|
+
- unknown pseudo-classes, combinators and attribute operators
|
|
27
|
+
- dangling combinators, e.g. `:root >`, `> >`
|
|
28
|
+
- bare words, strings, empty ids / attributes, e.g. `foo`, `.dev`,
|
|
29
|
+
`"foo"`, `#`, `[]`
|
|
30
|
+
|
|
31
|
+
`:is()` arguments are a forgiving list: only the last group above is
|
|
32
|
+
rejected there.
|
|
33
|
+
|
|
34
|
+
Pass `{ loose: true }` to skip the DSS checks (malformed syntax still
|
|
35
|
+
throws):
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
parse('#a, :unknown', { loose: true }) // no throw
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Supported names are exported as `pseudoClassNames`, `combinatorNames`
|
|
42
|
+
and `attributeOperatorNames`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pseudo-class names supported by DSS, without the leading `:`.
|
|
3
|
+
*/
|
|
4
|
+
export declare const pseudoClassNames: readonly ["abandoned", "attr", "built", "confused", "cve", "cwe", "debug", "deprecated", "dev", "diff", "dist", "dynamic", "empty", "entropic", "env", "eval", "fs", "has", "host", "hostname", "is", "license", "link", "malware", "minified", "missing", "native", "network", "not", "obfuscated", "optional", "outdated", "overridden", "path", "peer", "prerelease", "private", "prod", "project", "published", "registry", "root", "scanned", "scope", "score", "scripts", "semver", "sev", "severity", "shell", "shrinkwrap", "spec", "squat", "suspicious", "tracker", "trivial", "type", "undesirable", "unknown", "unmaintained", "unpopular", "unstable", "v", "vuln", "vulnerable", "workspace"];
|
|
5
|
+
export type PseudoClassName = (typeof pseudoClassNames)[number];
|
|
6
|
+
/**
|
|
7
|
+
* Combinators supported by DSS.
|
|
8
|
+
*/
|
|
9
|
+
export declare const combinatorNames: readonly [">", "~", " "];
|
|
10
|
+
export type CombinatorName = (typeof combinatorNames)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Attribute selector operators supported by DSS.
|
|
13
|
+
*/
|
|
14
|
+
export declare const attributeOperatorNames: readonly ["=", "^=", "$=", "~=", "*=", "|="];
|
|
15
|
+
export type AttributeOperatorName = (typeof attributeOperatorNames)[number];
|
package/dist/grammar.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pseudo-class names supported by DSS, without the leading `:`.
|
|
3
|
+
*/
|
|
4
|
+
export const pseudoClassNames = [
|
|
5
|
+
'abandoned',
|
|
6
|
+
'attr',
|
|
7
|
+
'built',
|
|
8
|
+
'confused',
|
|
9
|
+
'cve',
|
|
10
|
+
'cwe',
|
|
11
|
+
'debug',
|
|
12
|
+
'deprecated',
|
|
13
|
+
'dev',
|
|
14
|
+
'diff',
|
|
15
|
+
'dist',
|
|
16
|
+
'dynamic',
|
|
17
|
+
'empty',
|
|
18
|
+
'entropic',
|
|
19
|
+
'env',
|
|
20
|
+
'eval',
|
|
21
|
+
'fs',
|
|
22
|
+
'has',
|
|
23
|
+
'host',
|
|
24
|
+
'hostname',
|
|
25
|
+
'is',
|
|
26
|
+
'license',
|
|
27
|
+
'link',
|
|
28
|
+
'malware',
|
|
29
|
+
'minified',
|
|
30
|
+
'missing',
|
|
31
|
+
'native',
|
|
32
|
+
'network',
|
|
33
|
+
'not',
|
|
34
|
+
'obfuscated',
|
|
35
|
+
'optional',
|
|
36
|
+
'outdated',
|
|
37
|
+
'overridden',
|
|
38
|
+
'path',
|
|
39
|
+
'peer',
|
|
40
|
+
'prerelease',
|
|
41
|
+
'private',
|
|
42
|
+
'prod',
|
|
43
|
+
'project',
|
|
44
|
+
'published',
|
|
45
|
+
'registry',
|
|
46
|
+
'root',
|
|
47
|
+
'scanned',
|
|
48
|
+
'scope',
|
|
49
|
+
'score',
|
|
50
|
+
'scripts',
|
|
51
|
+
'semver',
|
|
52
|
+
'sev',
|
|
53
|
+
'severity',
|
|
54
|
+
'shell',
|
|
55
|
+
'shrinkwrap',
|
|
56
|
+
'spec',
|
|
57
|
+
'squat',
|
|
58
|
+
'suspicious',
|
|
59
|
+
'tracker',
|
|
60
|
+
'trivial',
|
|
61
|
+
'type',
|
|
62
|
+
'undesirable',
|
|
63
|
+
'unknown',
|
|
64
|
+
'unmaintained',
|
|
65
|
+
'unpopular',
|
|
66
|
+
'unstable',
|
|
67
|
+
'v',
|
|
68
|
+
'vuln',
|
|
69
|
+
'vulnerable',
|
|
70
|
+
'workspace',
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Combinators supported by DSS.
|
|
74
|
+
*/
|
|
75
|
+
export const combinatorNames = ['>', '~', ' '];
|
|
76
|
+
/**
|
|
77
|
+
* Attribute selector operators supported by DSS.
|
|
78
|
+
*/
|
|
79
|
+
export const attributeOperatorNames = [
|
|
80
|
+
'=',
|
|
81
|
+
'^=',
|
|
82
|
+
'$=',
|
|
83
|
+
'~=',
|
|
84
|
+
'*=',
|
|
85
|
+
'|=',
|
|
86
|
+
];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Root } from 'postcss-selector-parser';
|
|
2
2
|
export * from './types.ts';
|
|
3
|
+
export * from './grammar.ts';
|
|
3
4
|
/**
|
|
4
5
|
* Escapes forward slashes in specific patterns matching @scoped/name paths
|
|
5
6
|
* This will allow usage of unescaped forward slashes necessary for scoped
|
|
@@ -8,8 +9,13 @@ export * from './types.ts';
|
|
|
8
9
|
export declare const escapeScopedNamesSlashes: (query: string) => string;
|
|
9
10
|
export declare const escapeDots: (query: string) => string;
|
|
10
11
|
export declare const unescapeDots: (query: string) => string;
|
|
12
|
+
export type ParseOptions = {
|
|
13
|
+
/** skip DSS grammar checks, malformed syntax still throws */
|
|
14
|
+
loose?: boolean;
|
|
15
|
+
};
|
|
11
16
|
/**
|
|
12
|
-
* Parses a
|
|
17
|
+
* Parses a DSS query string into an AST
|
|
13
18
|
* Handles escaping of forward slashes in specific patterns
|
|
19
|
+
* Throws an `EQUERY` SyntaxError on invalid DSS
|
|
14
20
|
*/
|
|
15
|
-
export declare const parse: (query: string) => Root;
|
|
21
|
+
export declare const parse: (query: string, { loose }?: ParseOptions) => Root;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import postcssSelectorParser from 'postcss-selector-parser';
|
|
2
|
-
import {
|
|
2
|
+
import { syntaxError } from '@vltpkg/error-cause';
|
|
3
|
+
import { attributeOperatorNames, combinatorNames, pseudoClassNames, } from "./grammar.js";
|
|
4
|
+
import { asSelectorNode, isAttributeNode, isCombinatorNode, isPseudoNode, isStringNode, isTagNode, } from "./types.js";
|
|
3
5
|
export * from "./types.js";
|
|
6
|
+
export * from "./grammar.js";
|
|
4
7
|
/**
|
|
5
8
|
* Escapes forward slashes in specific patterns matching @scoped/name paths
|
|
6
9
|
* This will allow usage of unescaped forward slashes necessary for scoped
|
|
@@ -21,11 +24,78 @@ const pseudoCleanUpNeeded = new Set([
|
|
|
21
24
|
':vulnerable',
|
|
22
25
|
]);
|
|
23
26
|
const hasParamsToEscape = (node) => pseudoCleanUpNeeded.has(node.value);
|
|
27
|
+
const pseudoClasses = new Set(pseudoClassNames);
|
|
28
|
+
const combinators = new Set(combinatorNames);
|
|
29
|
+
const attributeOperators = new Set(attributeOperatorNames);
|
|
30
|
+
// pseudo-classes taking selector args, all others take strings
|
|
31
|
+
const selectorPseudos = new Set(['has', 'is', 'not']);
|
|
32
|
+
const invalid = (message, found) => syntaxError(message, { code: 'EQUERY', found });
|
|
24
33
|
/**
|
|
25
|
-
*
|
|
34
|
+
* Checks a parsed selector against the DSS grammar.
|
|
35
|
+
* `forgiving`: inside `:is()`, only bare words / strings / empty
|
|
36
|
+
* `#` `[]` are rejected.
|
|
37
|
+
*/
|
|
38
|
+
const validate = (selector, query, forgiving = false) => {
|
|
39
|
+
const nodes = selector.nodes.filter(n => n.type !== 'comment');
|
|
40
|
+
if (!nodes.length && !forgiving) {
|
|
41
|
+
throw invalid('Empty selector', query);
|
|
42
|
+
}
|
|
43
|
+
nodes.forEach((node, i) => {
|
|
44
|
+
// `#` / `[]`: nothing to match
|
|
45
|
+
if ((node.type === 'id' && !node.value) ||
|
|
46
|
+
(node.type === 'attribute' && !node.attribute)) {
|
|
47
|
+
throw invalid('Unsupported selector', node.type === 'id' ? '#' : '[]');
|
|
48
|
+
}
|
|
49
|
+
if (isCombinatorNode(node)) {
|
|
50
|
+
if (forgiving)
|
|
51
|
+
return;
|
|
52
|
+
if (!combinators.has(node.value)) {
|
|
53
|
+
throw syntaxError(`Unsupported combinator: ${node.value}`, {
|
|
54
|
+
code: 'EQUERY',
|
|
55
|
+
found: node.value,
|
|
56
|
+
validOptions: combinatorNames,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const next = nodes[i + 1];
|
|
60
|
+
if (!next || isCombinatorNode(next)) {
|
|
61
|
+
throw invalid('Dangling combinator', query);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (isPseudoNode(node)) {
|
|
65
|
+
const name = node.value.slice(1);
|
|
66
|
+
if (!pseudoClasses.has(name)) {
|
|
67
|
+
if (forgiving)
|
|
68
|
+
return;
|
|
69
|
+
throw invalid(`Unsupported pseudo-class: ${node.value}`, node.value);
|
|
70
|
+
}
|
|
71
|
+
if (selectorPseudos.has(name)) {
|
|
72
|
+
for (const n of node.nodes) {
|
|
73
|
+
validate(asSelectorNode(n), query, forgiving || name === 'is');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (isAttributeNode(node)) {
|
|
78
|
+
if (!forgiving &&
|
|
79
|
+
node.operator &&
|
|
80
|
+
!attributeOperators.has(node.operator)) {
|
|
81
|
+
throw invalid(`Unsupported attribute operator: ${node.operator}`, node.operator);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if ((isTagNode(node) && node.value !== '{' && node.value !== '}') ||
|
|
85
|
+
isStringNode(node)) {
|
|
86
|
+
// `{` / `}` tags are a noop in the query engine
|
|
87
|
+
throw invalid('Unsupported selector', node.value);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Parses a DSS query string into an AST
|
|
26
93
|
* Handles escaping of forward slashes in specific patterns
|
|
94
|
+
* Throws an `EQUERY` SyntaxError on invalid DSS
|
|
27
95
|
*/
|
|
28
|
-
export const parse = (query) => {
|
|
96
|
+
export const parse = (query, { loose = false } = {}) => {
|
|
97
|
+
if (!loose && !query.trim())
|
|
98
|
+
throw invalid('Empty query', query);
|
|
29
99
|
const escapedQuery = escapeDots(escapeScopedNamesSlashes(query));
|
|
30
100
|
const transformAst = (root) => {
|
|
31
101
|
root.walk((node) => {
|
|
@@ -41,6 +111,8 @@ export const parse = (query) => {
|
|
|
41
111
|
for (const n of node.nodes) {
|
|
42
112
|
// the parameters have a selector node that wraps them up
|
|
43
113
|
const selector = asSelectorNode(n);
|
|
114
|
+
if (!selector.nodes.length)
|
|
115
|
+
continue;
|
|
44
116
|
selector.nodes.forEach((currentNode, index, arr) => {
|
|
45
117
|
// get the next node, we'll update it later
|
|
46
118
|
const nextNode = arr[index + 1];
|
|
@@ -89,5 +161,28 @@ export const parse = (query) => {
|
|
|
89
161
|
}
|
|
90
162
|
});
|
|
91
163
|
};
|
|
92
|
-
|
|
164
|
+
let ast;
|
|
165
|
+
try {
|
|
166
|
+
ast = postcssSelectorParser(transformAst).astSync(escapedQuery);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
// postcss crashes with a TypeError when input ends after `(`
|
|
170
|
+
const reason = err instanceof TypeError ?
|
|
171
|
+
'unexpected end of input'
|
|
172
|
+
: err.message;
|
|
173
|
+
throw syntaxError(`Invalid query syntax: ${reason}`, {
|
|
174
|
+
code: 'EQUERY',
|
|
175
|
+
found: query,
|
|
176
|
+
cause: err,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (!loose) {
|
|
180
|
+
// postcss silently drops a trailing top-level comma
|
|
181
|
+
if (/(^|[^\\])(\\\\)*,\s*$/.test(query)) {
|
|
182
|
+
throw invalid('Empty selector', query);
|
|
183
|
+
}
|
|
184
|
+
for (const s of ast.nodes)
|
|
185
|
+
validate(s, query);
|
|
186
|
+
}
|
|
187
|
+
return ast;
|
|
93
188
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vltpkg/dss-parser",
|
|
3
3
|
"description": "The Dependency Selector Syntax (DSS) parser",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vltpkg/vltpkg.git",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "http://vlt.sh"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@vltpkg/error-cause": "1.
|
|
16
|
+
"@vltpkg/error-cause": "1.4.0",
|
|
17
17
|
"postcss-selector-parser": "^7.1.1"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|