1
1
/**
2
- * @typedef {import('css-selector-parser').RuleAttr } CssRuleAttr
3
- * @typedef {import('css-selector-parser').RulePseudo } CssRulePseudo
4
- * @typedef {import('css-selector-parser').Selectors } CssSelectors
5
- * @typedef {import('css-selector-parser').RuleSet } CssRuleSet
6
- * @typedef {import('css-selector-parser').Rule } CssRule
2
+ * @typedef {import('css-selector-parser').RuleAttr } RuleAttr
3
+ * @typedef {import('css-selector-parser').RulePseudo } RulePseudo
4
+ * @typedef {import('css-selector-parser').Rule } Rule
7
5
*
8
6
* @typedef {import('hast').Element } HastElement
9
7
* @typedef {import('hast').Properties } HastProperties
10
8
*
11
- * @typedef {'html'|'svg' } Space
9
+ * @typedef {'html' | 'svg' } Space
10
+ * Name of namespace.
12
11
*
13
12
* @typedef Options
13
+ * Configuration.
14
14
* @property {Space } [space]
15
+ * Which space first element in the selector is in.
15
16
*
16
- * @typedef Context
17
+ * When an `svg` element is created in HTML, the space is automatically
18
+ * switched to SVG.
19
+ *
20
+ * @typedef State
21
+ * Info on current context.
17
22
* @property {Space } space
18
- * @property { boolean } root
23
+ * Current space.
19
24
*/
20
25
21
26
import { h , s } from 'hastscript'
22
- import { zwitch } from 'zwitch'
23
27
import { CssSelectorParser } from 'css-selector-parser'
24
28
25
- const compile = zwitch ( 'type' , { handlers : { selectors, ruleSet, rule} } )
26
-
27
29
const parser = new CssSelectorParser ( )
28
30
29
31
parser . registerNestingOperators ( '>' , '+' , '~' )
30
32
// Register these so we can throw nicer errors.
31
33
parser . registerAttrEqualityMods ( '~' , '|' , '^' , '$' , '*' )
32
34
33
35
/**
34
- * @param {string } [selector='']
35
- * @param {Space|Options } [space='html']
36
+ * Turn a selector into a tree.
37
+ *
38
+ * @param {string | null | undefined } [selector='']
39
+ * CSS selector.
40
+ * @param {Space | Options | null | undefined } [space='html']
41
+ * Space or configuration.
36
42
* @returns {HastElement }
43
+ * Built tree.
37
44
*/
38
45
export function fromSelector ( selector , space ) {
39
- /** @type {Context } */
40
- const config = {
46
+ /** @type {State } */
47
+ const state = {
41
48
space :
42
49
( space && typeof space === 'object' && space . space ) ||
43
50
( typeof space === 'string' && space ) ||
44
- 'html' ,
45
- root : true
51
+ 'html'
46
52
}
47
53
48
- return (
49
- compile ( parser . parse ( selector || '' ) , config ) || build ( config . space ) ( '' )
50
- )
51
- }
54
+ const query = parser . parse ( selector || '' )
52
55
53
- /**
54
- * @param {CssSelectors } _
55
- */
56
- function selectors ( _ ) {
57
- throw new Error ( 'Cannot handle selector list' )
58
- }
56
+ if ( query && query . type === 'selectors' ) {
57
+ throw new Error ( 'Cannot handle selector list' )
58
+ }
59
59
60
- /**
61
- * @param {CssRuleSet } query
62
- * @param {Context } config
63
- * @returns {HastElement|Array<HastElement> }
64
- */
65
- function ruleSet ( query , config ) {
66
- // @ts -expect-error Assume one or more elements is returned.
67
- return compile ( query . rule , config )
60
+ const result = query ? rule ( query . rule , state ) : [ ]
61
+
62
+ if (
63
+ query &&
64
+ query . rule . rule &&
65
+ ( query . rule . rule . nestingOperator === '+' ||
66
+ query . rule . rule . nestingOperator === '~' )
67
+ ) {
68
+ throw new Error (
69
+ 'Cannot handle sibling combinator `' +
70
+ query . rule . rule . nestingOperator +
71
+ '` at root'
72
+ )
73
+ }
74
+
75
+ return result [ 0 ] || build ( state . space ) ( '' )
68
76
}
69
77
70
78
/**
71
- * @param {CssRule } query
72
- * @param {Context } config
73
- * @returns {HastElement|Array<HastElement> }
79
+ * Turn a rule into one or more elements.
80
+ *
81
+ * @param {Rule } query
82
+ * Selector.
83
+ * @param {State } state
84
+ * Info on current context.
85
+ * @returns {Array<HastElement> }
86
+ * One or more elements.
74
87
*/
75
- function rule ( query , config ) {
76
- const parentSpace = config . space
77
- const name = query . tagName === '*' ? '' : query . tagName || ''
78
- const space = parentSpace === 'html' && name === 'svg' ? 'svg' : parentSpace
79
- /** @type {boolean|undefined } */
80
- let sibling
88
+ function rule ( query , state ) {
89
+ const space =
90
+ state . space === 'html' && query . tagName === 'svg' ? 'svg' : state . space
91
+
92
+ checkPseudos ( query . pseudos || [ ] )
93
+
94
+ const node = build ( space ) ( query . tagName === '*' ? '' : query . tagName || '' , {
95
+ id : query . id ,
96
+ className : query . classNames ,
97
+ ...attrsToHast ( query . attrs || [ ] )
98
+ } )
99
+ const results = [ node ]
81
100
82
101
if ( query . rule ) {
83
- sibling =
84
- query . rule . nestingOperator === '+' || query . rule . nestingOperator === '~'
85
-
86
- if ( sibling && config . root ) {
87
- throw new Error (
88
- 'Cannot handle sibling combinator `' +
89
- query . rule . nestingOperator +
90
- '` at root'
91
- )
102
+ // Sibling.
103
+ if (
104
+ query . rule . nestingOperator === '+' ||
105
+ query . rule . nestingOperator === '~'
106
+ ) {
107
+ results . push ( ...rule ( query . rule , state ) )
108
+ }
109
+ // Descendant.
110
+ else {
111
+ node . children . push ( ...rule ( query . rule , { space} ) )
92
112
}
93
113
}
94
114
95
- const node = build ( space ) (
96
- name ,
97
- Object . assign (
98
- { id : query . id , className : query . classNames } ,
99
- pseudosToHast ( query . pseudos || [ ] ) ,
100
- attrsToHast ( query . attrs || [ ] )
101
- ) ,
102
- ! query . rule || sibling ? [ ] : compile ( query . rule , { space} )
103
- )
104
-
105
- return sibling ? [ node , compile ( query . rule , { space : parentSpace } ) ] : node
115
+ return results
106
116
}
107
117
108
118
/**
109
- * @param {Array<CssRulePseudo> } pseudos
110
- * @returns {HastProperties }
119
+ * Check pseudo selectors.
120
+ *
121
+ * @param {Array<RulePseudo> } pseudos
122
+ * Pseudo selectors.
123
+ * @returns {void }
124
+ * Nothing.
125
+ * @throws {Error }
126
+ * When a pseudo is defined.
111
127
*/
112
- function pseudosToHast ( pseudos ) {
128
+ function checkPseudos ( pseudos ) {
113
129
const pseudo = pseudos [ 0 ]
114
130
115
131
if ( pseudo ) {
@@ -119,23 +135,23 @@ function pseudosToHast(pseudos) {
119
135
120
136
throw new Error ( 'Cannot handle pseudo-element or empty pseudo-class' )
121
137
}
122
-
123
- return { }
124
138
}
125
139
126
140
/**
127
- * @param {Array<CssRuleAttr> } attrs
141
+ * Turn attribute selectors into properties.
142
+ *
143
+ * @param {Array<RuleAttr> } attrs
144
+ * Attribute selectors.
128
145
* @returns {HastProperties }
146
+ * Properties.
129
147
*/
130
148
function attrsToHast ( attrs ) {
131
- let index = - 1
132
149
/** @type {HastProperties } */
133
150
const props = { }
134
- /** @type {CssRuleAttr } */
135
- let attr
151
+ let index = - 1
136
152
137
153
while ( ++ index < attrs . length ) {
138
- attr = attrs [ index ]
154
+ const attr = attrs [ index ]
139
155
140
156
if ( 'operator' in attr ) {
141
157
if ( attr . operator === '=' ) {
@@ -155,7 +171,9 @@ function attrsToHast(attrs) {
155
171
156
172
/**
157
173
* @param {Space } space
174
+ * Space.
158
175
* @returns {typeof h }
176
+ * `h`.
159
177
*/
160
178
function build ( space ) {
161
179
return space === 'html' ? h : s
0 commit comments