@acemir/cssom 0.9.18 → 0.9.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/build/CSSOM.js +470 -99
- package/lib/CSSConditionRule.js +1 -1
- package/lib/CSSContainerRule.js +1 -0
- package/lib/CSSDocumentRule.js +2 -1
- package/lib/CSSGroupingRule.js +2 -1
- package/lib/CSSHostRule.js +3 -2
- package/lib/CSSKeyframesRule.js +2 -1
- package/lib/CSSLayerBlockRule.js +1 -1
- package/lib/CSSMediaRule.js +1 -0
- package/lib/CSSRule.js +22 -1
- package/lib/CSSRuleList.js +26 -0
- package/lib/CSSScopeRule.js +53 -0
- package/lib/CSSStartingStyleRule.js +5 -4
- package/lib/CSSStyleRule.js +2 -1
- package/lib/CSSStyleSheet.js +18 -2
- package/lib/CSSSupportsRule.js +1 -0
- package/lib/clone.js +1 -0
- package/lib/index.js +2 -0
- package/lib/parse.js +360 -88
- package/package.json +1 -1
package/lib/parse.js
CHANGED
|
@@ -50,6 +50,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
50
50
|
"conditionBlock": true,
|
|
51
51
|
"counterStyleBlock": true,
|
|
52
52
|
'documentRule-begin': true,
|
|
53
|
+
"scopeBlock": true,
|
|
53
54
|
"layerBlock": true
|
|
54
55
|
};
|
|
55
56
|
|
|
@@ -68,7 +69,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
68
69
|
var ancestorRules = [];
|
|
69
70
|
var prevScope;
|
|
70
71
|
|
|
71
|
-
var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
|
|
72
|
+
var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
|
|
72
73
|
|
|
73
74
|
// Track defined namespace prefixes for validation
|
|
74
75
|
var definedNamespacePrefixes = {};
|
|
@@ -78,11 +79,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
78
79
|
// var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
|
|
79
80
|
var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
|
|
80
81
|
var beforeRuleValidationRegExp = /^[\s{};]*(\*\/\s*)?$/; // Match that the portion before the rule is empty or contains only whitespace, semicolons, opening/closing braces, and optionally a comment ending (*/) followed by whitespace
|
|
81
|
-
var forwardRuleValidationRegExp = /(?:\(
|
|
82
|
+
var forwardRuleValidationRegExp = /(?:\s|\/\*|\{|\()/; // Match that the rule is followed by any whitespace, a opening comment, a condition opening parenthesis or a opening brace
|
|
82
83
|
var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
|
|
83
84
|
var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
|
|
84
85
|
var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
|
|
85
86
|
var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
|
|
87
|
+
var startsWithCombinatorRegExp = /^\s*[>+~]/; // Checks if a selector starts with a CSS combinator (>, +, ~)
|
|
86
88
|
|
|
87
89
|
/**
|
|
88
90
|
* Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
|
|
@@ -174,24 +176,214 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
174
176
|
return i;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Parses the scope prelude and extracts start and end selectors.
|
|
181
|
+
* @param {string} preludeContent - The scope prelude content (without @scope keyword)
|
|
182
|
+
* @returns {object} Object with startSelector and endSelector properties
|
|
183
|
+
*/
|
|
184
|
+
function parseScopePrelude(preludeContent) {
|
|
185
|
+
var parts = preludeContent.split(/\s*\)\s*to\s+\(/);
|
|
186
|
+
|
|
187
|
+
// Restore the parentheses that were consumed by the split
|
|
188
|
+
if (parts.length === 2) {
|
|
189
|
+
parts[0] = parts[0] + ')';
|
|
190
|
+
parts[1] = '(' + parts[1];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var hasStart = parts[0] &&
|
|
194
|
+
parts[0].charAt(0) === '(' &&
|
|
195
|
+
parts[0].charAt(parts[0].length - 1) === ')';
|
|
196
|
+
var hasEnd = parts[1] &&
|
|
197
|
+
parts[1].charAt(0) === '(' &&
|
|
198
|
+
parts[1].charAt(parts[1].length - 1) === ')';
|
|
199
|
+
|
|
200
|
+
// Handle case: @scope to (<end>)
|
|
201
|
+
var hasOnlyEnd = !hasStart &&
|
|
202
|
+
!hasEnd &&
|
|
203
|
+
parts[0].indexOf('to (') === 0 &&
|
|
204
|
+
parts[0].charAt(parts[0].length - 1) === ')';
|
|
205
|
+
|
|
206
|
+
var startSelector = '';
|
|
207
|
+
var endSelector = '';
|
|
208
|
+
|
|
209
|
+
if (hasStart) {
|
|
210
|
+
startSelector = parts[0].slice(1, -1).trim();
|
|
191
211
|
}
|
|
212
|
+
if (hasEnd) {
|
|
213
|
+
endSelector = parts[1].slice(1, -1).trim();
|
|
214
|
+
}
|
|
215
|
+
if (hasOnlyEnd) {
|
|
216
|
+
endSelector = parts[0].slice(4, -1).trim();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
startSelector: startSelector,
|
|
221
|
+
endSelector: endSelector,
|
|
222
|
+
hasStart: hasStart,
|
|
223
|
+
hasEnd: hasEnd,
|
|
224
|
+
hasOnlyEnd: hasOnlyEnd
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Checks if a selector contains pseudo-elements.
|
|
230
|
+
* @param {string} selector - The CSS selector to check
|
|
231
|
+
* @returns {boolean} True if the selector contains pseudo-elements
|
|
232
|
+
*/
|
|
233
|
+
function hasPseudoElement(selector) {
|
|
234
|
+
// Match only double-colon (::) pseudo-elements
|
|
235
|
+
// Also match legacy single-colon pseudo-elements: :before, :after, :first-line, :first-letter
|
|
236
|
+
// These must NOT be followed by alphanumeric characters (to avoid matching :before-x or similar)
|
|
237
|
+
var pseudoElementRegex = /::[a-zA-Z][\w-]*|:(before|after|first-line|first-letter)(?![a-zA-Z0-9_-])/;
|
|
238
|
+
return pseudoElementRegex.test(selector);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Validates balanced parentheses, brackets, and quotes in a selector.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} selector - The CSS selector to validate
|
|
245
|
+
* @param {boolean} trackAttributes - Whether to track attribute selector context
|
|
246
|
+
* @param {boolean} useStack - Whether to use a stack for parentheses (needed for nested validation)
|
|
247
|
+
* @returns {boolean} True if the syntax is valid (all brackets, parentheses, and quotes are balanced)
|
|
248
|
+
*/
|
|
249
|
+
function validateBalancedSyntax(selector, trackAttributes, useStack) {
|
|
250
|
+
var parenDepth = 0;
|
|
251
|
+
var bracketDepth = 0;
|
|
252
|
+
var inSingleQuote = false;
|
|
253
|
+
var inDoubleQuote = false;
|
|
254
|
+
var inAttr = false;
|
|
255
|
+
var stack = useStack ? [] : null;
|
|
256
|
+
|
|
257
|
+
for (var i = 0; i < selector.length; i++) {
|
|
258
|
+
var char = selector[i];
|
|
259
|
+
var prevChar = i > 0 ? selector[i - 1] : '';
|
|
260
|
+
|
|
261
|
+
if (inSingleQuote) {
|
|
262
|
+
if (char === "'" && prevChar !== "\\") {
|
|
263
|
+
inSingleQuote = false;
|
|
264
|
+
}
|
|
265
|
+
} else if (inDoubleQuote) {
|
|
266
|
+
if (char === '"' && prevChar !== "\\") {
|
|
267
|
+
inDoubleQuote = false;
|
|
268
|
+
}
|
|
269
|
+
} else if (trackAttributes && inAttr) {
|
|
270
|
+
if (char === "]") {
|
|
271
|
+
inAttr = false;
|
|
272
|
+
} else if (char === "'") {
|
|
273
|
+
inSingleQuote = true;
|
|
274
|
+
} else if (char === '"') {
|
|
275
|
+
inDoubleQuote = true;
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
if (trackAttributes && char === "[") {
|
|
279
|
+
inAttr = true;
|
|
280
|
+
} else if (char === "'") {
|
|
281
|
+
inSingleQuote = true;
|
|
282
|
+
} else if (char === '"') {
|
|
283
|
+
inDoubleQuote = true;
|
|
284
|
+
} else if (char === '(') {
|
|
285
|
+
if (useStack) {
|
|
286
|
+
stack.push("(");
|
|
287
|
+
} else {
|
|
288
|
+
parenDepth++;
|
|
289
|
+
}
|
|
290
|
+
} else if (char === ')') {
|
|
291
|
+
if (useStack) {
|
|
292
|
+
if (!stack.length || stack.pop() !== "(") {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
parenDepth--;
|
|
297
|
+
if (parenDepth < 0) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} else if (char === '[') {
|
|
302
|
+
bracketDepth++;
|
|
303
|
+
} else if (char === ']') {
|
|
304
|
+
bracketDepth--;
|
|
305
|
+
if (bracketDepth < 0) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check if everything is balanced
|
|
313
|
+
if (useStack) {
|
|
314
|
+
return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
|
|
315
|
+
} else {
|
|
316
|
+
return parenDepth === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Checks for basic syntax errors in selectors (mismatched parentheses, brackets, quotes).
|
|
322
|
+
* @param {string} selector - The CSS selector to check
|
|
323
|
+
* @returns {boolean} True if there are syntax errors
|
|
324
|
+
*/
|
|
325
|
+
function hasBasicSyntaxError(selector) {
|
|
326
|
+
return !validateBalancedSyntax(selector, false, false);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Checks for invalid combinator patterns in selectors.
|
|
331
|
+
* @param {string} selector - The CSS selector to check
|
|
332
|
+
* @returns {boolean} True if the selector contains invalid combinators
|
|
333
|
+
*/
|
|
334
|
+
function hasInvalidCombinators(selector) {
|
|
335
|
+
// Check for invalid combinator patterns:
|
|
336
|
+
// - <> (not a valid combinator)
|
|
337
|
+
// - >> (deep descendant combinator, deprecated and invalid)
|
|
338
|
+
// - Multiple consecutive combinators like >>, >~, etc.
|
|
339
|
+
if (/<>/.test(selector)) return true;
|
|
340
|
+
if (/>>/.test(selector)) return true;
|
|
341
|
+
// Check for other invalid consecutive combinator patterns
|
|
342
|
+
if (/[>+~]\s*[>+~]/.test(selector)) return true;
|
|
343
|
+
return false;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Checks for invalid pseudo-like syntax (function calls without proper pseudo prefix).
|
|
348
|
+
* @param {string} selector - The CSS selector to check
|
|
349
|
+
* @returns {boolean} True if the selector contains invalid pseudo-like syntax
|
|
350
|
+
*/
|
|
351
|
+
function hasInvalidPseudoSyntax(selector) {
|
|
352
|
+
// Check for specific known pseudo-elements used without : or :: prefix
|
|
353
|
+
// Examples: slotted(div), part(name), cue(selector)
|
|
354
|
+
// These are ONLY valid as ::slotted(), ::part(), ::cue()
|
|
355
|
+
var invalidPatterns = [
|
|
356
|
+
/(?:^|[\s>+~,\[])slotted\s*\(/i,
|
|
357
|
+
/(?:^|[\s>+~,\[])part\s*\(/i,
|
|
358
|
+
/(?:^|[\s>+~,\[])cue\s*\(/i,
|
|
359
|
+
/(?:^|[\s>+~,\[])cue-region\s*\(/i
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
for (var i = 0; i < invalidPatterns.length; i++) {
|
|
363
|
+
if (invalidPatterns[i].test(selector)) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Checks for invalid nesting selector (&) usage.
|
|
372
|
+
* The & selector cannot be directly followed by a type selector without a delimiter.
|
|
373
|
+
* Valid: &.class, &#id, &[attr], &:hover, &::before, & div, &>div
|
|
374
|
+
* Invalid: &div, &span
|
|
375
|
+
* @param {string} selector - The CSS selector to check
|
|
376
|
+
* @returns {boolean} True if the selector contains invalid & usage
|
|
377
|
+
*/
|
|
378
|
+
function hasInvalidNestingSelector(selector) {
|
|
379
|
+
// Check for & followed directly by a letter (type selector) without any delimiter
|
|
380
|
+
// This regex matches & followed by a letter (start of type selector) that's not preceded by an escape
|
|
381
|
+
// We need to exclude valid cases like &.class, &#id, &[attr], &:pseudo, &::pseudo, & (with space), &>
|
|
382
|
+
var invalidNestingPattern = /&(?![.\#\[:>\+~\s])[a-zA-Z]/;
|
|
383
|
+
return invalidNestingPattern.test(selector);
|
|
192
384
|
};
|
|
193
385
|
|
|
194
|
-
|
|
386
|
+
function validateAtRule(atRuleKey, validCallback, cannotBeNested) {
|
|
195
387
|
var isValid = false;
|
|
196
388
|
var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
|
|
197
389
|
var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
|
|
@@ -212,6 +404,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
212
404
|
isValid = true;
|
|
213
405
|
}
|
|
214
406
|
}
|
|
407
|
+
|
|
408
|
+
// Additional validation for @scope rule
|
|
409
|
+
if (isValid && atRuleKey === "@scope") {
|
|
410
|
+
var openBraceIndex = ruleSlice.indexOf('{');
|
|
411
|
+
if (openBraceIndex !== -1) {
|
|
412
|
+
// Extract the scope prelude (everything between @scope and {)
|
|
413
|
+
var scopePrelude = ruleSlice.slice(0, openBraceIndex).trim();
|
|
414
|
+
|
|
415
|
+
// Skip past '@scope' keyword and whitespace
|
|
416
|
+
var preludeContent = scopePrelude.slice(6).trim();
|
|
417
|
+
|
|
418
|
+
if (preludeContent.length > 0) {
|
|
419
|
+
// Parse the scope prelude
|
|
420
|
+
var parsedScopePrelude = parseScopePrelude(preludeContent);
|
|
421
|
+
var startSelector = parsedScopePrelude.startSelector;
|
|
422
|
+
var endSelector = parsedScopePrelude.endSelector;
|
|
423
|
+
var hasStart = parsedScopePrelude.hasStart;
|
|
424
|
+
var hasEnd = parsedScopePrelude.hasEnd;
|
|
425
|
+
var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
|
|
426
|
+
|
|
427
|
+
// Validation rules for @scope:
|
|
428
|
+
// 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
|
|
429
|
+
if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
|
|
430
|
+
isValid = false;
|
|
431
|
+
}
|
|
432
|
+
// 2. Pseudo-elements are invalid in scope selectors
|
|
433
|
+
else if ((startSelector && hasPseudoElement(startSelector)) || (endSelector && hasPseudoElement(endSelector))) {
|
|
434
|
+
isValid = false;
|
|
435
|
+
}
|
|
436
|
+
// 3. Basic syntax errors (mismatched parens, brackets, quotes)
|
|
437
|
+
else if ((startSelector && hasBasicSyntaxError(startSelector)) || (endSelector && hasBasicSyntaxError(endSelector))) {
|
|
438
|
+
isValid = false;
|
|
439
|
+
}
|
|
440
|
+
// 4. Invalid combinator patterns
|
|
441
|
+
else if ((startSelector && hasInvalidCombinators(startSelector)) || (endSelector && hasInvalidCombinators(endSelector))) {
|
|
442
|
+
isValid = false;
|
|
443
|
+
}
|
|
444
|
+
// 5. Invalid pseudo-like syntax (function without : or :: prefix)
|
|
445
|
+
else if ((startSelector && hasInvalidPseudoSyntax(startSelector)) || (endSelector && hasInvalidPseudoSyntax(endSelector))) {
|
|
446
|
+
isValid = false;
|
|
447
|
+
}
|
|
448
|
+
// 6. Invalid structure (no proper parentheses found when prelude is not empty)
|
|
449
|
+
else if (!hasStart && !hasOnlyEnd) {
|
|
450
|
+
isValid = false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Empty prelude (@scope {}) is valid
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
215
457
|
if (!isValid) {
|
|
216
458
|
// If it's invalid the browser will simply ignore the entire invalid block
|
|
217
459
|
// Use regex to find the closing brace of the invalid rule
|
|
@@ -270,52 +512,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
270
512
|
* @returns {boolean}
|
|
271
513
|
*/
|
|
272
514
|
function basicSelectorValidator(selector) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
var inSingleQuote = false;
|
|
278
|
-
var inDoubleQuote = false;
|
|
515
|
+
// Validate balanced syntax with attribute tracking and stack-based parentheses matching
|
|
516
|
+
if (!validateBalancedSyntax(selector, true, true)) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
279
519
|
|
|
280
|
-
|
|
281
|
-
|
|
520
|
+
// Check for invalid combinator patterns
|
|
521
|
+
if (hasInvalidCombinators(selector)) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
282
524
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
} else if (inDoubleQuote) {
|
|
288
|
-
if (char === '"' && selector[i - 1] !== "\\") {
|
|
289
|
-
inDoubleQuote = false;
|
|
290
|
-
}
|
|
291
|
-
} else if (inAttr) {
|
|
292
|
-
if (char === "]") {
|
|
293
|
-
inAttr = false;
|
|
294
|
-
} else if (char === "'") {
|
|
295
|
-
inSingleQuote = true;
|
|
296
|
-
} else if (char === '"') {
|
|
297
|
-
inDoubleQuote = true;
|
|
298
|
-
}
|
|
299
|
-
} else {
|
|
300
|
-
if (char === "[") {
|
|
301
|
-
inAttr = true;
|
|
302
|
-
} else if (char === "'") {
|
|
303
|
-
inSingleQuote = true;
|
|
304
|
-
} else if (char === '"') {
|
|
305
|
-
inDoubleQuote = true;
|
|
306
|
-
} else if (char === "(") {
|
|
307
|
-
stack.push("(");
|
|
308
|
-
} else if (char === ")") {
|
|
309
|
-
if (!stack.length || stack.pop() !== "(") {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
i++;
|
|
525
|
+
// Check for invalid pseudo-like syntax
|
|
526
|
+
if (hasInvalidPseudoSyntax(selector)) {
|
|
527
|
+
return false;
|
|
315
528
|
}
|
|
316
529
|
|
|
317
|
-
//
|
|
318
|
-
if (
|
|
530
|
+
// Check for invalid nesting selector (&) usage
|
|
531
|
+
if (hasInvalidNestingSelector(selector)) {
|
|
319
532
|
return false;
|
|
320
533
|
}
|
|
321
534
|
|
|
@@ -326,7 +539,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
326
539
|
var looseSelectorRegExp = /^((?:(?:\*|[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|)\|)?[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|(?:(?:\*|[a-zA-Z_\u00A0-\uFFFF\\][a-zA-Z0-9_\u00A0-\uFFFF\-\\]*|)\|)?\*|#[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\.[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+|\[(?:[^\[\]'"]|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")*(?:\s+[iI])?\]|::?[a-zA-Z0-9_\u00A0-\uFFFF\-\\]+(?:\((.*)\))?|&|\s*[>+~]\s*|\s+)+$/;
|
|
327
540
|
return looseSelectorRegExp.test(selector);
|
|
328
541
|
}
|
|
329
|
-
|
|
542
|
+
|
|
330
543
|
/**
|
|
331
544
|
* Regular expression to match CSS pseudo-classes with arguments.
|
|
332
545
|
*
|
|
@@ -357,48 +570,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
357
570
|
* @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
|
|
358
571
|
*/
|
|
359
572
|
function parseAndSplitNestedSelectors(selector) {
|
|
360
|
-
var depth = 0;
|
|
361
|
-
var buffer = "";
|
|
362
|
-
var parts = [];
|
|
363
|
-
var inSingleQuote = false;
|
|
364
|
-
var inDoubleQuote = false;
|
|
573
|
+
var depth = 0; // Track parenthesis nesting depth
|
|
574
|
+
var buffer = ""; // Accumulate characters for current selector part
|
|
575
|
+
var parts = []; // Array of split selector parts
|
|
576
|
+
var inSingleQuote = false; // Track if we're inside single quotes
|
|
577
|
+
var inDoubleQuote = false; // Track if we're inside double quotes
|
|
365
578
|
var i, char;
|
|
366
579
|
|
|
367
580
|
for (i = 0; i < selector.length; i++) {
|
|
368
581
|
char = selector.charAt(i);
|
|
369
582
|
|
|
583
|
+
// Handle single quote strings
|
|
370
584
|
if (char === "'" && !inDoubleQuote) {
|
|
371
585
|
inSingleQuote = !inSingleQuote;
|
|
372
586
|
buffer += char;
|
|
373
|
-
}
|
|
587
|
+
}
|
|
588
|
+
// Handle double quote strings
|
|
589
|
+
else if (char === '"' && !inSingleQuote) {
|
|
374
590
|
inDoubleQuote = !inDoubleQuote;
|
|
375
591
|
buffer += char;
|
|
376
|
-
}
|
|
592
|
+
}
|
|
593
|
+
// Process characters outside of quoted strings
|
|
594
|
+
else if (!inSingleQuote && !inDoubleQuote) {
|
|
377
595
|
if (char === '(') {
|
|
596
|
+
// Entering a nested level (e.g., :is(...))
|
|
378
597
|
depth++;
|
|
379
598
|
buffer += char;
|
|
380
599
|
} else if (char === ')') {
|
|
600
|
+
// Exiting a nested level
|
|
381
601
|
depth--;
|
|
382
602
|
buffer += char;
|
|
383
|
-
if (depth === 0) {
|
|
384
|
-
parts.push(buffer.replace(/^\s+|\s+$/g, ""));
|
|
385
|
-
buffer = "";
|
|
386
|
-
}
|
|
387
603
|
} else if (char === ',' && depth === 0) {
|
|
388
|
-
|
|
389
|
-
|
|
604
|
+
// Found a top-level comma separator - split here
|
|
605
|
+
if (buffer.trim()) {
|
|
606
|
+
parts.push(buffer.trim());
|
|
390
607
|
}
|
|
391
608
|
buffer = "";
|
|
392
609
|
} else {
|
|
610
|
+
// Regular character - add to buffer
|
|
393
611
|
buffer += char;
|
|
394
612
|
}
|
|
395
|
-
}
|
|
613
|
+
}
|
|
614
|
+
// Characters inside quoted strings - add to buffer
|
|
615
|
+
else {
|
|
396
616
|
buffer += char;
|
|
397
617
|
}
|
|
398
618
|
}
|
|
399
619
|
|
|
400
|
-
|
|
401
|
-
|
|
620
|
+
// Add any remaining content in buffer as the last part
|
|
621
|
+
if (buffer.trim()) {
|
|
622
|
+
parts.push(buffer.trim());
|
|
402
623
|
}
|
|
403
624
|
|
|
404
625
|
return parts;
|
|
@@ -415,8 +636,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
415
636
|
* @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
|
|
416
637
|
*/
|
|
417
638
|
|
|
418
|
-
// Cache to store validated selectors
|
|
419
|
-
var validatedSelectorsCache =
|
|
639
|
+
// Cache to store validated selectors (ES5-compliant object)
|
|
640
|
+
var validatedSelectorsCache = {};
|
|
420
641
|
|
|
421
642
|
// Only pseudo-classes that accept selector lists should recurse
|
|
422
643
|
var selectorListPseudoClasses = {
|
|
@@ -427,8 +648,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
427
648
|
};
|
|
428
649
|
|
|
429
650
|
function validateSelector(selector) {
|
|
430
|
-
if (validatedSelectorsCache.
|
|
431
|
-
return validatedSelectorsCache
|
|
651
|
+
if (validatedSelectorsCache.hasOwnProperty(selector)) {
|
|
652
|
+
return validatedSelectorsCache[selector];
|
|
432
653
|
}
|
|
433
654
|
|
|
434
655
|
// Use a non-global regex to find all pseudo-classes with arguments
|
|
@@ -445,15 +666,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
445
666
|
var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
|
|
446
667
|
for (var i = 0; i < nestedSelectors.length; i++) {
|
|
447
668
|
var nestedSelector = nestedSelectors[i];
|
|
448
|
-
if (!validatedSelectorsCache.
|
|
669
|
+
if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
|
|
449
670
|
var nestedSelectorValidation = validateSelector(nestedSelector);
|
|
450
|
-
validatedSelectorsCache
|
|
671
|
+
validatedSelectorsCache[nestedSelector] = nestedSelectorValidation;
|
|
451
672
|
if (!nestedSelectorValidation) {
|
|
452
|
-
validatedSelectorsCache
|
|
673
|
+
validatedSelectorsCache[selector] = false;
|
|
453
674
|
return false;
|
|
454
675
|
}
|
|
455
|
-
} else if (!validatedSelectorsCache
|
|
456
|
-
validatedSelectorsCache
|
|
676
|
+
} else if (!validatedSelectorsCache[nestedSelector]) {
|
|
677
|
+
validatedSelectorsCache[selector] = false;
|
|
457
678
|
return false;
|
|
458
679
|
}
|
|
459
680
|
}
|
|
@@ -461,7 +682,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
461
682
|
}
|
|
462
683
|
|
|
463
684
|
var basicSelectorValidation = basicSelectorValidator(selector);
|
|
464
|
-
validatedSelectorsCache
|
|
685
|
+
validatedSelectorsCache[selector] = basicSelectorValidation;
|
|
465
686
|
|
|
466
687
|
return basicSelectorValidation;
|
|
467
688
|
}
|
|
@@ -553,6 +774,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
553
774
|
return true;
|
|
554
775
|
}
|
|
555
776
|
|
|
777
|
+
function parseError(message) {
|
|
778
|
+
var lines = token.substring(0, i).split('\n');
|
|
779
|
+
var lineCount = lines.length;
|
|
780
|
+
var charCount = lines.pop().length + 1;
|
|
781
|
+
var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
|
|
782
|
+
error.line = lineCount;
|
|
783
|
+
/* jshint sub : true */
|
|
784
|
+
error['char'] = charCount;
|
|
785
|
+
error.styleSheet = styleSheet;
|
|
786
|
+
// Print the error but continue parsing the sheet
|
|
787
|
+
try {
|
|
788
|
+
throw error;
|
|
789
|
+
} catch(e) {
|
|
790
|
+
errorHandler && errorHandler(e);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
556
794
|
var endingIndex = token.length - 1;
|
|
557
795
|
|
|
558
796
|
for (var character; (character = token.charAt(i)); i++) {
|
|
@@ -697,6 +935,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
697
935
|
}, true);
|
|
698
936
|
buffer = "";
|
|
699
937
|
break;
|
|
938
|
+
} else if (token.indexOf("@scope", i) === i) {
|
|
939
|
+
validateAtRule("@scope", function(){
|
|
940
|
+
state = "scopeBlock";
|
|
941
|
+
scopeRule = new CSSOM.CSSScopeRule();
|
|
942
|
+
scopeRule.__starts = i;
|
|
943
|
+
i += "scope".length;
|
|
944
|
+
});
|
|
945
|
+
buffer = "";
|
|
946
|
+
break;
|
|
700
947
|
} else if (token.indexOf("@layer", i) === i) {
|
|
701
948
|
validateAtRule("@layer", function(){
|
|
702
949
|
state = "layerBlock"
|
|
@@ -852,6 +1099,27 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
852
1099
|
supportsRule.__parentStyleSheet = styleSheet;
|
|
853
1100
|
buffer = "";
|
|
854
1101
|
state = "before-selector";
|
|
1102
|
+
} else if (state === "scopeBlock") {
|
|
1103
|
+
var parsedScopePrelude = parseScopePrelude(buffer.trim());
|
|
1104
|
+
|
|
1105
|
+
if (parsedScopePrelude.hasStart) {
|
|
1106
|
+
scopeRule.__start = parsedScopePrelude.startSelector;
|
|
1107
|
+
}
|
|
1108
|
+
if (parsedScopePrelude.hasEnd) {
|
|
1109
|
+
scopeRule.__end = parsedScopePrelude.endSelector;
|
|
1110
|
+
}
|
|
1111
|
+
if (parsedScopePrelude.hasOnlyEnd) {
|
|
1112
|
+
scopeRule.__end = parsedScopePrelude.endSelector;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (parentRule) {
|
|
1116
|
+
scopeRule.__parentRule = parentRule;
|
|
1117
|
+
ancestorRules.push(parentRule);
|
|
1118
|
+
}
|
|
1119
|
+
currentScope = parentRule = scopeRule;
|
|
1120
|
+
scopeRule.__parentStyleSheet = styleSheet;
|
|
1121
|
+
buffer = "";
|
|
1122
|
+
state = "before-selector";
|
|
855
1123
|
} else if (state === "layerBlock") {
|
|
856
1124
|
layerBlockRule.name = buffer.trim();
|
|
857
1125
|
|
|
@@ -950,8 +1218,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
950
1218
|
styleRule.selectorText = processedSelectorText;
|
|
951
1219
|
} else {
|
|
952
1220
|
styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
|
|
953
|
-
|
|
954
|
-
|
|
1221
|
+
// Add & at the beginning if there's no & in the selector, or if it starts with a combinator
|
|
1222
|
+
return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
|
|
1223
|
+
}).join(', ');
|
|
955
1224
|
}
|
|
956
1225
|
styleRule.style.__starts = i - buffer.length;
|
|
957
1226
|
styleRule.__parentRule = parentRule;
|
|
@@ -1187,6 +1456,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
1187
1456
|
|| parentRule.constructor.name === "CSSMediaRule"
|
|
1188
1457
|
|| parentRule.constructor.name === "CSSSupportsRule"
|
|
1189
1458
|
|| parentRule.constructor.name === "CSSContainerRule"
|
|
1459
|
+
|| parentRule.constructor.name === "CSSScopeRule"
|
|
1190
1460
|
|| parentRule.constructor.name === "CSSLayerBlockRule"
|
|
1191
1461
|
|| parentRule.constructor.name === "CSSStartingStyleRule"
|
|
1192
1462
|
) {
|
|
@@ -1256,6 +1526,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
|
|
|
1256
1526
|
|| parentRule.constructor.name === "CSSMediaRule"
|
|
1257
1527
|
|| parentRule.constructor.name === "CSSSupportsRule"
|
|
1258
1528
|
|| parentRule.constructor.name === "CSSContainerRule"
|
|
1529
|
+
|| parentRule.constructor.name === "CSSScopeRule"
|
|
1259
1530
|
|| parentRule.constructor.name === "CSSLayerBlockRule"
|
|
1260
1531
|
|| parentRule.constructor.name === "CSSStartingStyleRule"
|
|
1261
1532
|
) {
|
|
@@ -1337,6 +1608,7 @@ CSSOM.CSSKeyframeRule = require('./CSSKeyframeRule').CSSKeyframeRule;
|
|
|
1337
1608
|
CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
|
|
1338
1609
|
CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
|
|
1339
1610
|
CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
|
|
1611
|
+
CSSOM.CSSScopeRule = require('./CSSScopeRule').CSSScopeRule;
|
|
1340
1612
|
CSSOM.CSSLayerBlockRule = require("./CSSLayerBlockRule").CSSLayerBlockRule;
|
|
1341
1613
|
CSSOM.CSSLayerStatementRule = require("./CSSLayerStatementRule").CSSLayerStatementRule;
|
|
1342
1614
|
// Use cssstyle if available
|