-
-
Notifications
You must be signed in to change notification settings - Fork 134
/
Copy pathselector.ts
266 lines (236 loc) · 7.09 KB
/
selector.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* The namespace for selector related utilities.
*/
export namespace Selector {
/**
* Calculate the specificity of a single CSS selector.
*
* @param selector - The CSS selector of interest.
*
* @returns The specificity of the selector.
*
* #### Undefined Behavior
* The selector is invalid.
*
* #### Notes
* This is based on https://www.w3.org/TR/css3-selectors/#specificity
*
* A larger number represents a more specific selector.
*
* The smallest possible specificity is `0`.
*
* The result is represented as a hex number `0x<aa><bb><cc>` where
* each component is the count of the respective selector clause.
*
* If the selector contains commas, only the first clause is used.
*
* The computed result is cached, so subsequent calculations for the
* same selector are extremely fast.
*/
export function calculateSpecificity(selector: string): number {
if (selector in Private.specificityCache) {
return Private.specificityCache[selector];
}
let result = Private.calculateSingle(selector);
return (Private.specificityCache[selector] = result);
}
/**
* Test whether a selector is a valid CSS selector.
*
* @param selector - The CSS selector of interest.
*
* @returns `true` if the selector is valid, `false` otherwise.
*
* #### Notes
* The computed result is cached, so subsequent tests for the same
* selector are extremely fast.
*/
export function isValid(selector: string): boolean {
if (selector in Private.validityCache) {
return Private.validityCache[selector];
}
let result = true;
try {
Private.testElem.querySelector(selector);
} catch (err) {
result = false;
}
return (Private.validityCache[selector] = result);
}
/**
* Test whether an element matches a CSS selector.
*
* @param element - The element of interest.
*
* @param selector - The valid CSS selector of interest.
*
* @returns `true` if the element is a match, `false` otherwise.
*
* #### Notes
* This function uses the builtin browser capabilities when possible,
* falling back onto a document query otherwise.
*/
export function matches(element: Element, selector: string): boolean {
return Private.protoMatchFunc.call(element, selector);
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* A type alias for an object hash.
*/
export type StringMap<T> = { [key: string]: T };
/**
* A cache of computed selector specificity values.
*/
export const specificityCache: StringMap<number> = Object.create(null);
/**
* A cache of computed selector validity.
*/
export const validityCache: StringMap<boolean> = Object.create(null);
/**
* An empty element for testing selector validity.
*/
export const testElem =
typeof document !== 'undefined'
? document.createElement('div')
: ({} as any);
/**
* A cross-browser CSS selector matching prototype function.
*/
export const protoMatchFunc = (() => {
let proto =
typeof Element === 'undefined' ? ({} as any) : Element.prototype;
return (
proto.matches ||
proto.matchesSelector ||
proto.mozMatchesSelector ||
proto.msMatchesSelector ||
proto.oMatchesSelector ||
proto.webkitMatchesSelector ||
function (selector: string) {
let elem = this as Element;
let matches = elem.ownerDocument
? elem.ownerDocument.querySelectorAll(selector)
: [];
return Array.prototype.indexOf.call(matches, elem) !== -1;
}
);
})();
/**
* Calculate the specificity of a single selector.
*
* The behavior is undefined if the selector is invalid.
*/
export function calculateSingle(selector: string): number {
// Ignore anything after the first comma.
selector = selector.split(',', 1)[0];
// Setup the aggregate counters.
let a = 0;
let b = 0;
let c = 0;
// Apply a regex to the front of the selector. If it succeeds, that
// portion of the selector is removed. Returns a success/fail flag.
function match(re: RegExp): boolean {
let match = selector.match(re);
if (match === null) {
return false;
}
selector = selector.slice(match[0].length);
return true;
}
// Replace the negation pseudo-class (which is ignored),
// but keep its inner content (which is not ignored).
selector = selector.replace(NEGATION_RE, ' $1 ');
// Continue matching until the selector is consumed.
while (selector.length > 0) {
// Match an ID selector.
if (match(ID_RE)) {
a++;
continue;
}
// Match a class selector.
if (match(CLASS_RE)) {
b++;
continue;
}
// Match an attribute selector.
if (match(ATTR_RE)) {
b++;
continue;
}
// Match a pseudo-element selector. This is done before matching
// a pseudo-class since this regex overlaps with that regex.
if (match(PSEUDO_ELEM_RE)) {
c++;
continue;
}
// Match a pseudo-class selector.
if (match(PSEDUO_CLASS_RE)) {
b++;
continue;
}
// Match a plain type selector.
if (match(TYPE_RE)) {
c++;
continue;
}
// Finally, match any ignored characters.
if (match(IGNORE_RE)) {
continue;
}
// At this point, the selector is assumed to be invalid.
return 0;
}
// Clamp each component to a reasonable base.
a = Math.min(a, 0xff);
b = Math.min(b, 0xff);
c = Math.min(c, 0xff);
// Combine the components into a single result.
return (a << 16) | (b << 8) | c;
}
/**
* A regex which matches an ID selector at string start.
*/
const ID_RE = /^#[^\s\+>~#\.\[:]+/;
/**
* A regex which matches a class selector at string start.
*/
const CLASS_RE = /^\.[^\s\+>~#\.\[:]+/;
/**
* A regex which matches an attribute selector at string start.
*/
const ATTR_RE = /^\[[^\]]+\]/;
/**
* A regex which matches a type selector at string start.
*/
const TYPE_RE = /^[^\s\+>~#\.\[:]+/;
/**
* A regex which matches a pseudo-element selector at string start.
*/
const PSEUDO_ELEM_RE =
/^(::[^\s\+>~#\.\[:]+|:first-line|:first-letter|:before|:after)/;
/**
* A regex which matches a pseudo-class selector at string start.
*/
const PSEDUO_CLASS_RE = /^:[^\s\+>~#\.\[:]+/;
/**
* A regex which matches ignored characters at string start.
*/
const IGNORE_RE = /^[\s\+>~\*]+/;
/**
* A regex which matches the negation pseudo-class globally.
*/
const NEGATION_RE = /:not\(([^\)]+)\)/g;
}