@emasoft/svg-matrix 1.0.18 → 1.0.20
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 +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/scripts/postinstall.js +6 -9
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Specificity Calculation
|
|
3
|
+
*
|
|
4
|
+
* Calculates CSS selector specificity according to W3C spec.
|
|
5
|
+
* https://www.w3.org/TR/selectors-3/#specificity
|
|
6
|
+
*
|
|
7
|
+
* Note: This module deals with integer counts (not precision-critical),
|
|
8
|
+
* but follows project patterns for consistency.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Selector component types
|
|
12
|
+
const SELECTOR_TYPES = {
|
|
13
|
+
ID: 'id', // #foo
|
|
14
|
+
CLASS: 'class', // .foo
|
|
15
|
+
ATTRIBUTE: 'attr', // [foo]
|
|
16
|
+
PSEUDO_CLASS: 'pseudo-class', // :hover
|
|
17
|
+
PSEUDO_ELEMENT: 'pseudo-element', // ::before
|
|
18
|
+
TYPE: 'type', // div
|
|
19
|
+
UNIVERSAL: 'universal' // *
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a CSS selector string into its component parts.
|
|
24
|
+
*
|
|
25
|
+
* Handles:
|
|
26
|
+
* - Tag names (div, span)
|
|
27
|
+
* - Classes (.foo)
|
|
28
|
+
* - IDs (#bar)
|
|
29
|
+
* - Attribute selectors ([type="text"])
|
|
30
|
+
* - Pseudo-classes (:hover, :not())
|
|
31
|
+
* - Pseudo-elements (::before, :after)
|
|
32
|
+
* - Combinators (>, +, ~, space)
|
|
33
|
+
*
|
|
34
|
+
* @param {string} selectorString - CSS selector to parse
|
|
35
|
+
* @returns {Array<Object>} Array of parsed selector components
|
|
36
|
+
* @throws {Error} If selector syntax is invalid
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* parseSelector('div.class#id:hover::before')
|
|
40
|
+
* // Returns array of component objects with type and value
|
|
41
|
+
*/
|
|
42
|
+
export function parseSelector(selectorString) {
|
|
43
|
+
if (typeof selectorString !== 'string' || !selectorString.trim()) {
|
|
44
|
+
throw new Error('Selector must be a non-empty string');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const selector = selectorString.trim();
|
|
48
|
+
const components = [];
|
|
49
|
+
let i = 0;
|
|
50
|
+
|
|
51
|
+
// Split by combinators first (>, +, ~, space) to handle complex selectors
|
|
52
|
+
const parts = splitByCombinators(selector);
|
|
53
|
+
|
|
54
|
+
for (const part of parts) {
|
|
55
|
+
if (!part.trim()) continue;
|
|
56
|
+
|
|
57
|
+
const partComponents = parseSimpleSelector(part.trim());
|
|
58
|
+
components.push(...partComponents);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return components;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Split selector by combinators while preserving them.
|
|
66
|
+
* Handles: descendant (space), child (>), adjacent sibling (+), general sibling (~)
|
|
67
|
+
*
|
|
68
|
+
* @param {string} selector - Selector string
|
|
69
|
+
* @returns {Array<string>} Parts split by combinators
|
|
70
|
+
*/
|
|
71
|
+
function splitByCombinators(selector) {
|
|
72
|
+
// Match combinators outside of brackets and parentheses
|
|
73
|
+
const parts = [];
|
|
74
|
+
let current = '';
|
|
75
|
+
let depth = 0; // Track nesting in [] and ()
|
|
76
|
+
let inBracket = false;
|
|
77
|
+
let inParen = false;
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < selector.length; i++) {
|
|
80
|
+
const char = selector[i];
|
|
81
|
+
|
|
82
|
+
if (char === '[') {
|
|
83
|
+
inBracket = true;
|
|
84
|
+
depth++;
|
|
85
|
+
} else if (char === ']') {
|
|
86
|
+
depth--;
|
|
87
|
+
if (depth === 0) inBracket = false;
|
|
88
|
+
} else if (char === '(') {
|
|
89
|
+
inParen = true;
|
|
90
|
+
depth++;
|
|
91
|
+
} else if (char === ')') {
|
|
92
|
+
depth--;
|
|
93
|
+
if (depth === 0) inParen = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Only split on combinators when not inside brackets or parens
|
|
97
|
+
if (depth === 0 && !inBracket && !inParen) {
|
|
98
|
+
if (char === '>' || char === '+' || char === '~') {
|
|
99
|
+
if (current.trim()) parts.push(current.trim());
|
|
100
|
+
parts.push(char); // Keep combinator as separate part for context
|
|
101
|
+
current = '';
|
|
102
|
+
continue;
|
|
103
|
+
} else if (char === ' ' && current.trim()) {
|
|
104
|
+
// Space combinator (descendant)
|
|
105
|
+
parts.push(current.trim());
|
|
106
|
+
current = '';
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
current += char;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (current.trim()) {
|
|
115
|
+
parts.push(current.trim());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parts;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse a simple selector (no combinators).
|
|
123
|
+
*
|
|
124
|
+
* @param {string} selector - Simple selector string
|
|
125
|
+
* @returns {Array<Object>} Array of component objects
|
|
126
|
+
*/
|
|
127
|
+
function parseSimpleSelector(selector) {
|
|
128
|
+
const components = [];
|
|
129
|
+
let i = 0;
|
|
130
|
+
|
|
131
|
+
while (i < selector.length) {
|
|
132
|
+
const char = selector[i];
|
|
133
|
+
|
|
134
|
+
// ID selector
|
|
135
|
+
if (char === '#') {
|
|
136
|
+
const match = selector.slice(i).match(/^#([\w-]+)/);
|
|
137
|
+
if (!match) throw new Error(`Invalid ID selector at position ${i}`);
|
|
138
|
+
components.push({ type: SELECTOR_TYPES.ID, value: match[1] });
|
|
139
|
+
i += match[0].length;
|
|
140
|
+
}
|
|
141
|
+
// Class selector
|
|
142
|
+
else if (char === '.') {
|
|
143
|
+
const match = selector.slice(i).match(/^\.([\w-]+)/);
|
|
144
|
+
if (!match) throw new Error(`Invalid class selector at position ${i}`);
|
|
145
|
+
components.push({ type: SELECTOR_TYPES.CLASS, value: match[1] });
|
|
146
|
+
i += match[0].length;
|
|
147
|
+
}
|
|
148
|
+
// Attribute selector
|
|
149
|
+
else if (char === '[') {
|
|
150
|
+
const endIdx = findMatchingBracket(selector, i);
|
|
151
|
+
if (endIdx === -1) throw new Error(`Unclosed attribute selector at position ${i}`);
|
|
152
|
+
const attrContent = selector.slice(i + 1, endIdx);
|
|
153
|
+
components.push({ type: SELECTOR_TYPES.ATTRIBUTE, value: attrContent });
|
|
154
|
+
i = endIdx + 1;
|
|
155
|
+
}
|
|
156
|
+
// Pseudo-element (::) or pseudo-class (:)
|
|
157
|
+
else if (char === ':') {
|
|
158
|
+
if (selector[i + 1] === ':') {
|
|
159
|
+
// Pseudo-element
|
|
160
|
+
const match = selector.slice(i).match(/^::([\w-]+)/);
|
|
161
|
+
if (!match) throw new Error(`Invalid pseudo-element at position ${i}`);
|
|
162
|
+
components.push({ type: SELECTOR_TYPES.PSEUDO_ELEMENT, value: match[1] });
|
|
163
|
+
i += match[0].length;
|
|
164
|
+
} else {
|
|
165
|
+
// Pseudo-class (may have arguments like :not())
|
|
166
|
+
const match = selector.slice(i).match(/^:([\w-]+)/);
|
|
167
|
+
if (!match) throw new Error(`Invalid pseudo-class at position ${i}`);
|
|
168
|
+
|
|
169
|
+
let pseudoValue = match[1];
|
|
170
|
+
i += match[0].length;
|
|
171
|
+
|
|
172
|
+
// Check for function notation like :not()
|
|
173
|
+
if (selector[i] === '(') {
|
|
174
|
+
const endIdx = findMatchingParen(selector, i);
|
|
175
|
+
if (endIdx === -1) throw new Error(`Unclosed pseudo-class function at position ${i}`);
|
|
176
|
+
pseudoValue += selector.slice(i, endIdx + 1);
|
|
177
|
+
i = endIdx + 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
components.push({ type: SELECTOR_TYPES.PSEUDO_CLASS, value: pseudoValue });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Universal selector
|
|
184
|
+
else if (char === '*') {
|
|
185
|
+
components.push({ type: SELECTOR_TYPES.UNIVERSAL, value: '*' });
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
// Type selector (element name)
|
|
189
|
+
else if (/[a-zA-Z]/.test(char)) {
|
|
190
|
+
const match = selector.slice(i).match(/^([\w-]+)/);
|
|
191
|
+
if (!match) throw new Error(`Invalid type selector at position ${i}`);
|
|
192
|
+
components.push({ type: SELECTOR_TYPES.TYPE, value: match[1] });
|
|
193
|
+
i += match[0].length;
|
|
194
|
+
}
|
|
195
|
+
// Skip combinators (should not be in simple selector)
|
|
196
|
+
else if (char === '>' || char === '+' || char === '~' || char === ' ') {
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
throw new Error(`Unexpected character '${char}' at position ${i}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return components;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Find matching closing bracket for attribute selector.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} str - String to search
|
|
211
|
+
* @param {number} startIdx - Index of opening bracket
|
|
212
|
+
* @returns {number} Index of closing bracket or -1 if not found
|
|
213
|
+
*/
|
|
214
|
+
function findMatchingBracket(str, startIdx) {
|
|
215
|
+
let depth = 0;
|
|
216
|
+
for (let i = startIdx; i < str.length; i++) {
|
|
217
|
+
if (str[i] === '[') depth++;
|
|
218
|
+
if (str[i] === ']') {
|
|
219
|
+
depth--;
|
|
220
|
+
if (depth === 0) return i;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return -1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Find matching closing parenthesis for pseudo-class function.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} str - String to search
|
|
230
|
+
* @param {number} startIdx - Index of opening parenthesis
|
|
231
|
+
* @returns {number} Index of closing parenthesis or -1 if not found
|
|
232
|
+
*/
|
|
233
|
+
function findMatchingParen(str, startIdx) {
|
|
234
|
+
let depth = 0;
|
|
235
|
+
for (let i = startIdx; i < str.length; i++) {
|
|
236
|
+
if (str[i] === '(') depth++;
|
|
237
|
+
if (str[i] === ')') {
|
|
238
|
+
depth--;
|
|
239
|
+
if (depth === 0) return i;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return -1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Calculate CSS specificity for a selector.
|
|
247
|
+
* Returns [a, b, c] where:
|
|
248
|
+
* - a = count of ID selectors
|
|
249
|
+
* - b = count of class selectors, attribute selectors, and pseudo-classes
|
|
250
|
+
* - c = count of type selectors and pseudo-elements
|
|
251
|
+
*
|
|
252
|
+
* Universal selector (*) and combinators do not contribute to specificity.
|
|
253
|
+
*
|
|
254
|
+
* Note: :not() pseudo-class itself doesn't count, but its argument does.
|
|
255
|
+
*
|
|
256
|
+
* @param {string|Array<Object>} selector - CSS selector string or parsed components
|
|
257
|
+
* @returns {Array<number>} [a, b, c] specificity values
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* calculateSpecificity('div.class#id:hover::before')
|
|
261
|
+
* // Returns [1, 2, 2] (1 ID, 2 class+pseudo-class, 2 type+pseudo-element)
|
|
262
|
+
*/
|
|
263
|
+
export function calculateSpecificity(selector) {
|
|
264
|
+
// Parse if string, otherwise assume it's already parsed
|
|
265
|
+
const components = typeof selector === 'string'
|
|
266
|
+
? parseSelector(selector)
|
|
267
|
+
: selector;
|
|
268
|
+
|
|
269
|
+
let a = 0; // IDs
|
|
270
|
+
let b = 0; // Classes, attributes, pseudo-classes
|
|
271
|
+
let c = 0; // Types, pseudo-elements
|
|
272
|
+
|
|
273
|
+
for (const component of components) {
|
|
274
|
+
switch (component.type) {
|
|
275
|
+
case SELECTOR_TYPES.ID:
|
|
276
|
+
a++;
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
case SELECTOR_TYPES.CLASS:
|
|
280
|
+
case SELECTOR_TYPES.ATTRIBUTE:
|
|
281
|
+
b++;
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
case SELECTOR_TYPES.PSEUDO_CLASS:
|
|
285
|
+
// Handle :not() - it doesn't count itself, but its argument does
|
|
286
|
+
if (component.value.startsWith('not(')) {
|
|
287
|
+
const notContent = component.value.slice(4, -1); // Extract content inside :not()
|
|
288
|
+
const notSpec = calculateSpecificity(notContent);
|
|
289
|
+
a += notSpec[0];
|
|
290
|
+
b += notSpec[1];
|
|
291
|
+
c += notSpec[2];
|
|
292
|
+
} else {
|
|
293
|
+
b++;
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case SELECTOR_TYPES.TYPE:
|
|
298
|
+
case SELECTOR_TYPES.PSEUDO_ELEMENT:
|
|
299
|
+
c++;
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case SELECTOR_TYPES.UNIVERSAL:
|
|
303
|
+
// Universal selector doesn't contribute to specificity
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
default:
|
|
307
|
+
// Unknown type, ignore
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return [a, b, c];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Compare two specificity values.
|
|
317
|
+
*
|
|
318
|
+
* @param {Array<number>} spec1 - First specificity [a, b, c]
|
|
319
|
+
* @param {Array<number>} spec2 - Second specificity [a, b, c]
|
|
320
|
+
* @returns {number} -1 if spec1 < spec2, 0 if equal, 1 if spec1 > spec2
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* compareSpecificity([1, 0, 0], [0, 2, 1]) // Returns 1 (ID beats classes)
|
|
324
|
+
* compareSpecificity([0, 1, 2], [0, 1, 2]) // Returns 0 (equal)
|
|
325
|
+
*/
|
|
326
|
+
export function compareSpecificity(spec1, spec2) {
|
|
327
|
+
if (!Array.isArray(spec1) || spec1.length !== 3) {
|
|
328
|
+
throw new Error('spec1 must be an array of 3 numbers');
|
|
329
|
+
}
|
|
330
|
+
if (!Array.isArray(spec2) || spec2.length !== 3) {
|
|
331
|
+
throw new Error('spec2 must be an array of 3 numbers');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Compare lexicographically: a first, then b, then c
|
|
335
|
+
for (let i = 0; i < 3; i++) {
|
|
336
|
+
if (spec1[i] < spec2[i]) return -1;
|
|
337
|
+
if (spec1[i] > spec2[i]) return 1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return 0; // Equal
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Sort CSS rules by specificity (ascending order).
|
|
345
|
+
* Uses stable sort to preserve source order for rules with equal specificity.
|
|
346
|
+
*
|
|
347
|
+
* @param {Array<Object>} rules - Array of rule objects with 'selector' property
|
|
348
|
+
* @returns {Array<Object>} Sorted array of rules
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* const rules = [
|
|
352
|
+
* { selector: '.foo', style: 'color: red' },
|
|
353
|
+
* { selector: '#bar', style: 'color: blue' },
|
|
354
|
+
* { selector: 'div', style: 'color: green' }
|
|
355
|
+
* ];
|
|
356
|
+
* sortBySpecificity(rules)
|
|
357
|
+
* // Returns rules sorted: div, .foo, #bar
|
|
358
|
+
*/
|
|
359
|
+
export function sortBySpecificity(rules) {
|
|
360
|
+
if (!Array.isArray(rules)) {
|
|
361
|
+
throw new Error('rules must be an array');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Create array of [rule, specificity, originalIndex] tuples
|
|
365
|
+
const withSpec = rules.map((rule, index) => {
|
|
366
|
+
if (!rule || typeof rule.selector !== 'string') {
|
|
367
|
+
throw new Error(`Rule at index ${index} must have a 'selector' property`);
|
|
368
|
+
}
|
|
369
|
+
return [rule, calculateSpecificity(rule.selector), index];
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Stable sort by specificity, using original index to preserve source order
|
|
373
|
+
withSpec.sort((a, b) => {
|
|
374
|
+
const cmp = compareSpecificity(a[1], b[1]);
|
|
375
|
+
if (cmp !== 0) return cmp;
|
|
376
|
+
// If specificity is equal, maintain original order (stable sort)
|
|
377
|
+
return a[2] - b[2];
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Extract just the rules
|
|
381
|
+
return withSpec.map(tuple => tuple[0]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Stringify parsed selector components back to selector string.
|
|
386
|
+
* Used for verification (round-trip testing).
|
|
387
|
+
*
|
|
388
|
+
* @param {Array<Object>} components - Parsed selector components
|
|
389
|
+
* @returns {string} Selector string
|
|
390
|
+
*/
|
|
391
|
+
export function stringifySelector(components) {
|
|
392
|
+
if (!Array.isArray(components)) {
|
|
393
|
+
throw new Error('components must be an array');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return components.map(component => {
|
|
397
|
+
switch (component.type) {
|
|
398
|
+
case SELECTOR_TYPES.ID:
|
|
399
|
+
return `#${component.value}`;
|
|
400
|
+
case SELECTOR_TYPES.CLASS:
|
|
401
|
+
return `.${component.value}`;
|
|
402
|
+
case SELECTOR_TYPES.ATTRIBUTE:
|
|
403
|
+
return `[${component.value}]`;
|
|
404
|
+
case SELECTOR_TYPES.PSEUDO_CLASS:
|
|
405
|
+
return `:${component.value}`;
|
|
406
|
+
case SELECTOR_TYPES.PSEUDO_ELEMENT:
|
|
407
|
+
return `::${component.value}`;
|
|
408
|
+
case SELECTOR_TYPES.TYPE:
|
|
409
|
+
return component.value;
|
|
410
|
+
case SELECTOR_TYPES.UNIVERSAL:
|
|
411
|
+
return '*';
|
|
412
|
+
default:
|
|
413
|
+
return '';
|
|
414
|
+
}
|
|
415
|
+
}).join('');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Verify selector parsing by round-trip test.
|
|
420
|
+
* Parse selector, stringify it, and compare (ignoring whitespace).
|
|
421
|
+
*
|
|
422
|
+
* @param {string} selector - Selector to verify
|
|
423
|
+
* @returns {boolean} True if round-trip matches
|
|
424
|
+
*/
|
|
425
|
+
export function verifySelector(selector) {
|
|
426
|
+
const components = parseSelector(selector);
|
|
427
|
+
const reconstructed = stringifySelector(components);
|
|
428
|
+
|
|
429
|
+
// Normalize whitespace for comparison
|
|
430
|
+
const normalize = s => s.replace(/\s+/g, '');
|
|
431
|
+
|
|
432
|
+
return normalize(selector) === normalize(reconstructed);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export default {
|
|
436
|
+
SELECTOR_TYPES,
|
|
437
|
+
parseSelector,
|
|
438
|
+
calculateSpecificity,
|
|
439
|
+
compareSpecificity,
|
|
440
|
+
sortBySpecificity,
|
|
441
|
+
stringifySelector,
|
|
442
|
+
verifySelector
|
|
443
|
+
};
|