Skip to content

Commit ebc49d0

Browse files
committed
Update to match CSS selectors 4
This commit updates `css-selector-parser`, which is completely different. You’ll get some different errors if you do weird things. Otherwise, this changes: * no longer supports whitespace only, either pass an empty string or an actual selector * change to remove invalid attribute selectors such as `[a=b,c]`, use `[a="b,c"]` instead
1 parent f0249ad commit ebc49d0

File tree

3 files changed

+86
-77
lines changed

3 files changed

+86
-77
lines changed

lib/index.js

+69-64
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/**
2-
* @typedef {import('css-selector-parser').RuleAttr} RuleAttr
3-
* @typedef {import('css-selector-parser').RulePseudo} RulePseudo
4-
* @typedef {import('css-selector-parser').Rule} Rule
2+
* @typedef {import('css-selector-parser').AstAttribute} AstAttribute
3+
* @typedef {import('css-selector-parser').AstRule} AstRule
54
*
65
* @typedef {import('hast').Element} HastElement
76
* @typedef {import('hast').Properties} HastProperties
@@ -23,14 +22,11 @@
2322
* Current space.
2423
*/
2524

25+
import {ok as assert} from 'devlop'
2626
import {h, s} from 'hastscript'
27-
import {CssSelectorParser} from 'css-selector-parser'
27+
import {createParser} from 'css-selector-parser'
2828

29-
const parser = new CssSelectorParser()
30-
31-
parser.registerNestingOperators('>', '+', '~')
32-
// Register these so we can throw nicer errors.
33-
parser.registerAttrEqualityMods('~', '|', '^', '$', '*')
29+
const cssSelectorParse = createParser({syntax: 'selectors-4'})
3430

3531
// To do: remove `space` shortcut.
3632
/**
@@ -52,34 +48,35 @@ export function fromSelector(selector, space) {
5248
'html'
5349
}
5450

55-
const query = parser.parse(selector || '')
51+
const query = cssSelectorParse(selector || '*')
5652

57-
if (query && query.type === 'selectors') {
53+
if (query.rules.length > 1) {
5854
throw new Error('Cannot handle selector list')
5955
}
6056

61-
const result = query ? rule(query.rule, state) : []
57+
const head = query.rules[0]
58+
assert(head, 'expected rule')
6259

6360
if (
64-
query &&
65-
query.rule.rule &&
66-
(query.rule.rule.nestingOperator === '+' ||
67-
query.rule.rule.nestingOperator === '~')
61+
head.nestedRule &&
62+
(head.nestedRule.combinator === '+' || head.nestedRule.combinator === '~')
6863
) {
6964
throw new Error(
7065
'Cannot handle sibling combinator `' +
71-
query.rule.rule.nestingOperator +
66+
head.nestedRule.combinator +
7267
'` at root'
7368
)
7469
}
7570

76-
return result[0] || build(state.space)('')
71+
const result = rule(head, state)
72+
73+
return result[0]
7774
}
7875

7976
/**
8077
* Turn a rule into one or more elements.
8178
*
82-
* @param {Rule} query
79+
* @param {AstRule} query
8380
* Selector.
8481
* @param {State} state
8582
* Info on current context.
@@ -88,82 +85,90 @@ export function fromSelector(selector, space) {
8885
*/
8986
function rule(query, state) {
9087
const space =
91-
state.space === 'html' && query.tagName === 'svg' ? 'svg' : state.space
88+
state.space === 'html' &&
89+
query.tag &&
90+
query.tag.type === 'TagName' &&
91+
query.tag.name === 'svg'
92+
? 'svg'
93+
: state.space
94+
95+
const pseudoClass = query.pseudoClasses ? query.pseudoClasses[0] : undefined
96+
97+
if (pseudoClass) {
98+
if (pseudoClass.name) {
99+
throw new Error('Cannot handle pseudo class `' + pseudoClass.name + '`')
100+
/* c8 ignore next 4 -- types say this can occur, but I don’t understand how */
101+
}
102+
103+
throw new Error('Cannot handle empty pseudo class')
104+
}
92105

93-
checkPseudos(query.pseudos || [])
106+
if (query.pseudoElement) {
107+
throw new Error(
108+
'Cannot handle pseudo element `' + query.pseudoElement + '`'
109+
)
110+
}
94111

95-
const node = build(space)(query.tagName === '*' ? '' : query.tagName || '', {
96-
id: query.id,
112+
const name = query.tag && query.tag.type === 'TagName' ? query.tag.name : ''
113+
114+
const node = build(space)(name, {
115+
id: query.ids ? query.ids[query.ids.length - 1] : undefined,
97116
className: query.classNames,
98-
...attrsToHast(query.attrs || [])
117+
...attributesToHast(query.attributes)
99118
})
100119
const results = [node]
101120

102-
if (query.rule) {
121+
if (query.nestedRule) {
103122
// Sibling.
104123
if (
105-
query.rule.nestingOperator === '+' ||
106-
query.rule.nestingOperator === '~'
124+
query.nestedRule.combinator === '+' ||
125+
query.nestedRule.combinator === '~'
107126
) {
108-
results.push(...rule(query.rule, state))
127+
results.push(...rule(query.nestedRule, state))
109128
}
110129
// Descendant.
111130
else {
112-
node.children.push(...rule(query.rule, {space}))
131+
node.children.push(...rule(query.nestedRule, {space}))
113132
}
114133
}
115134

116135
return results
117136
}
118137

119-
/**
120-
* Check pseudo selectors.
121-
*
122-
* @param {Array<RulePseudo>} pseudos
123-
* Pseudo selectors.
124-
* @returns {void}
125-
* Nothing.
126-
* @throws {Error}
127-
* When a pseudo is defined.
128-
*/
129-
function checkPseudos(pseudos) {
130-
const pseudo = pseudos[0]
131-
132-
if (pseudo) {
133-
if (pseudo.name) {
134-
throw new Error('Cannot handle pseudo-selector `' + pseudo.name + '`')
135-
}
136-
137-
throw new Error('Cannot handle pseudo-element or empty pseudo-class')
138-
}
139-
}
140-
141138
/**
142139
* Turn attribute selectors into properties.
143140
*
144-
* @param {Array<RuleAttr>} attrs
141+
* @param {Array<AstAttribute> | undefined} attributes
145142
* Attribute selectors.
146143
* @returns {HastProperties}
147144
* Properties.
148145
*/
149-
function attrsToHast(attrs) {
146+
function attributesToHast(attributes) {
150147
/** @type {HastProperties} */
151148
const props = {}
152149
let index = -1
153150

154-
while (++index < attrs.length) {
155-
const attr = attrs[index]
156-
157-
if ('operator' in attr) {
158-
if (attr.operator === '=') {
159-
props[attr.name] = attr.value
151+
if (attributes) {
152+
while (++index < attributes.length) {
153+
const attr = attributes[index]
154+
155+
if ('operator' in attr) {
156+
if (attr.operator === '=') {
157+
const value = attr.value
158+
159+
// eslint-disable-next-line max-depth
160+
if (value) {
161+
assert(value.type === 'String', 'substitution are not enabled')
162+
props[attr.name] = value.value
163+
}
164+
} else {
165+
throw new Error(
166+
'Cannot handle attribute equality modifier `' + attr.operator + '`'
167+
)
168+
}
160169
} else {
161-
throw new Error(
162-
'Cannot handle attribute equality modifier `' + attr.operator + '`'
163-
)
170+
props[attr.name] = true
164171
}
165-
} else {
166-
props[attr.name] = true
167172
}
168173
}
169174

package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
],
3838
"dependencies": {
3939
"@types/hast": "^3.0.0",
40-
"css-selector-parser": "^1.0.0",
40+
"css-selector-parser": "^2.0.0",
41+
"devlop": "^1.0.0",
4142
"hastscript": "^8.0.0"
4243
},
4344
"devDependencies": {
@@ -68,7 +69,10 @@
6869
"trailingComma": "none"
6970
},
7071
"xo": {
71-
"prettier": true
72+
"prettier": true,
73+
"rules": {
74+
"unicorn/prefer-at": "off"
75+
}
7276
},
7377
"remarkConfig": {
7478
"plugins": [

test.js

+11-11
Original file line numberDiff line numberDiff line change
@@ -15,77 +15,77 @@ test('fromSelector()', () => {
1515
() => {
1616
fromSelector('@supports (transform-origin: 5% 5%) {}')
1717
},
18-
/Error: Rule expected but "@" found/,
18+
/Expected rule but "@" found/,
1919
'should throw w/ invalid selector'
2020
)
2121

2222
assert.throws(
2323
() => {
2424
fromSelector('a, b')
2525
},
26-
/Error: Cannot handle selector list/,
26+
/Cannot handle selector list/,
2727
'should throw w/ multiple selector'
2828
)
2929

3030
assert.throws(
3131
() => {
3232
fromSelector('a + b')
3333
},
34-
/Error: Cannot handle sibling combinator `\+` at root/,
34+
/Cannot handle sibling combinator `\+` at root/,
3535
'should throw w/ next-sibling combinator at root'
3636
)
3737

3838
assert.throws(
3939
() => {
4040
fromSelector('a ~ b')
4141
},
42-
/Error: Cannot handle sibling combinator `~` at root/,
42+
/Cannot handle sibling combinator `~` at root/,
4343
'should throw w/ subsequent-sibling combinator at root'
4444
)
4545

4646
assert.throws(
4747
() => {
4848
fromSelector('[foo%=bar]')
4949
},
50-
/Error: Expected "=" but "%" found./,
50+
/Expected a valid attribute selector operator/,
5151
'should throw w/ attribute modifiers'
5252
)
5353

5454
assert.throws(
5555
() => {
5656
fromSelector('[foo~=bar]')
5757
},
58-
/Error: Cannot handle attribute equality modifier `~=`/,
58+
/Cannot handle attribute equality modifier `~=`/,
5959
'should throw w/ attribute modifiers'
6060
)
6161

6262
assert.throws(
6363
() => {
6464
fromSelector(':active')
6565
},
66-
/Error: Cannot handle pseudo-selector `active`/,
66+
/Cannot handle pseudo class `active`/,
6767
'should throw on pseudo classes'
6868
)
6969

7070
assert.throws(
7171
() => {
7272
fromSelector(':nth-foo(2n+1)')
7373
},
74-
/Error: Cannot handle pseudo-selector `nth-foo`/,
74+
/Unknown pseudo-class/,
7575
'should throw on pseudo class “functions”'
7676
)
7777

7878
assert.throws(
7979
() => {
8080
fromSelector('::before')
8181
},
82-
/Error: Cannot handle pseudo-element or empty pseudo-class/,
82+
/Cannot handle pseudo element `before`/,
8383
'should throw on invalid pseudo elements'
8484
)
8585

8686
assert.deepEqual(fromSelector(), h(''), 'should support no selector')
8787
assert.deepEqual(fromSelector(''), h(''), 'should support the empty string')
88-
assert.deepEqual(fromSelector(' '), h(''), 'should support whitespace only')
88+
8989
assert.deepEqual(
9090
fromSelector('*'),
9191
h(''),
@@ -184,7 +184,7 @@ test('fromSelector()', () => {
184184

185185
assert.deepEqual(
186186
fromSelector(
187-
'p svg[viewbox=0 0 10 10] circle[cx=10][cy=10][r=10] altGlyph'
187+
'p svg[viewbox="0 0 10 10"] circle[cx=10][cy=10][r=10] altGlyph'
188188
),
189189
h('p', [
190190
s('svg', {viewBox: '0 0 10 10'}, [

0 commit comments

Comments
 (0)