@acemir/cssom 0.9.0 → 0.9.2

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/lib/parse.js CHANGED
@@ -6,7 +6,8 @@ var CSSOM = {};
6
6
  /**
7
7
  * @param {string} token
8
8
  */
9
- CSSOM.parse = function parse(token) {
9
+ CSSOM.parse = function parse(token, errorHandler) {
10
+ errorHandler = errorHandler === undefined && (console && console.error);
10
11
 
11
12
  var i = 0;
12
13
 
@@ -28,6 +29,8 @@ CSSOM.parse = function parse(token) {
28
29
  var valueParenthesisDepth = 0;
29
30
 
30
31
  var SIGNIFICANT_WHITESPACE = {
32
+ "name": true,
33
+ "before-name": true,
31
34
  "selector": true,
32
35
  "value": true,
33
36
  "value-parenthesis": true,
@@ -50,17 +53,57 @@ CSSOM.parse = function parse(token) {
50
53
  var parentRule;
51
54
 
52
55
  var ancestorRules = [];
53
- var hasAncestors = false;
54
56
  var prevScope;
55
57
 
56
- var name, priority="", styleRule, mediaRule, containerRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, nestedSelectorRule;
58
+ var name, priority="", styleRule, mediaRule, containerRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule;
57
59
 
58
60
  var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; // Match @keyframes and vendor-prefixed @keyframes
59
- var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
61
+ // Regex above is not ES5 compliant
62
+ // var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
60
63
  var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
61
64
  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
62
65
  var forwardRuleValidationRegExp = /(?:\(|\s|\/\*)/; // Match that the rule is followed by any whitespace, a opening comment or a condition opening parenthesis
66
+ var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
63
67
  var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
68
+ var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
69
+ var layerRuleNameRegExp = /^(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
70
+
71
+ /**
72
+ * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
73
+ * that is not inside a brace block within the given string. Mimics the behavior of a
74
+ * regular expression match for such terminators, including any trailing whitespace.
75
+ * @param {string} str - The string to search for at-rule statement terminators.
76
+ * @returns {object | null} {0: string, index: number} or null if no match is found.
77
+ */
78
+ function atRulesStatemenRegExpES5Alternative(ruleSlice) {
79
+ for (var i = 0; i < ruleSlice.length; i++) {
80
+ var char = ruleSlice[i];
81
+
82
+ if (char === ';' || char === '}') {
83
+ // Simulate negative lookbehind: check if there is a { before this position
84
+ var sliceBefore = ruleSlice.substring(0, i);
85
+ var openBraceIndex = sliceBefore.indexOf('{');
86
+
87
+ if (openBraceIndex === -1) {
88
+ // No { found before, so we treat it as a valid match
89
+ var match = char;
90
+ var j = i + 1;
91
+
92
+ while (j < ruleSlice.length && /\s/.test(ruleSlice[j])) {
93
+ match += ruleSlice[j];
94
+ j++;
95
+ }
96
+
97
+ var matchObj = [match];
98
+ matchObj.index = i;
99
+ matchObj.input = ruleSlice;
100
+ return matchObj;
101
+ }
102
+ }
103
+ }
104
+
105
+ return null;
106
+ }
64
107
 
65
108
  /**
66
109
  * Finds the first balanced block (including nested braces) in the string, starting from fromIndex.
@@ -69,17 +112,18 @@ CSSOM.parse = function parse(token) {
69
112
  * @param {number} [fromIndex=0] - The index to start searching from.
70
113
  * @returns {object|null} - { 0: matchedString, index: startIndex, input: str } or null if not found.
71
114
  */
72
- function matchBalancedBlock(str, fromIndex = 0) {
73
- const openIndex = str.indexOf('{', fromIndex);
115
+ function matchBalancedBlock(str, fromIndex) {
116
+ fromIndex = fromIndex || 0;
117
+ var openIndex = str.indexOf('{', fromIndex);
74
118
  if (openIndex === -1) return null;
75
- let depth = 0;
76
- for (let i = openIndex; i < str.length; i++) {
119
+ var depth = 0;
120
+ for (var i = openIndex; i < str.length; i++) {
77
121
  if (str[i] === '{') {
78
122
  depth++;
79
123
  } else if (str[i] === '}') {
80
124
  depth--;
81
125
  if (depth === 0) {
82
- const matchedString = str.slice(openIndex, i + 1);
126
+ var matchedString = str.slice(openIndex, i + 1);
83
127
  return {
84
128
  0: matchedString,
85
129
  index: openIndex,
@@ -91,6 +135,29 @@ CSSOM.parse = function parse(token) {
91
135
  return null;
92
136
  }
93
137
 
138
+ /**
139
+ * Advances the index `i` to skip over a balanced block of curly braces in the given string.
140
+ * This is typically used to ignore the contents of a CSS rule block.
141
+ *
142
+ * @param {number} i - The current index in the string to start searching from.
143
+ * @param {string} str - The string containing the CSS code.
144
+ * @param {number} fromIndex - The index in the string where the balanced block search should begin.
145
+ * @returns {number} The updated index after skipping the balanced block.
146
+ */
147
+ function ignoreBalancedBlock(i, str, fromIndex) {
148
+ var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
149
+ if (ruleClosingMatch) {
150
+ var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
151
+ i+= ignoreRange;
152
+ if (token.charAt(i) === '}') {
153
+ i -= 1;
154
+ }
155
+ } else {
156
+ i += str.length;
157
+ }
158
+ return i;
159
+ }
160
+
94
161
  var parseError = function(message) {
95
162
  var lines = token.substring(0, i).split('\n');
96
163
  var lineCount = lines.length;
@@ -100,12 +167,18 @@ CSSOM.parse = function parse(token) {
100
167
  /* jshint sub : true */
101
168
  error['char'] = charCount;
102
169
  error.styleSheet = styleSheet;
103
- throw error;
170
+ // Print the error but continue parsing the sheet
171
+ try {
172
+ throw error;
173
+ } catch(e) {
174
+ errorHandler && errorHandler(e);
175
+ }
104
176
  };
105
177
 
106
178
  var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
107
179
  var isValid = false;
108
- var ruleRegExp = new RegExp(atRuleKey + forwardRuleValidationRegExp.source, forwardRuleValidationRegExp.flags);
180
+ var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
181
+ var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
109
182
  var ruleSlice = token.slice(i);
110
183
  // Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
111
184
  var shouldPerformCheck = cannotBeNested && currentScope !== styleSheet ? false : true;
@@ -127,7 +200,9 @@ CSSOM.parse = function parse(token) {
127
200
  // If it's invalid the browser will simply ignore the entire invalid block
128
201
  // Use regex to find the closing brace of the invalid rule
129
202
 
130
- var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp);
203
+ // Regex used above is not ES5 compliant. Using alternative.
204
+ // var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
205
+ var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
131
206
 
132
207
  // If it's a statement inside a nested rule, ignore only the statement
133
208
  if (ruleStatementMatch && currentScope !== styleSheet) {
@@ -136,25 +211,179 @@ CSSOM.parse = function parse(token) {
136
211
  return;
137
212
  }
138
213
 
139
- // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
140
- var ruleClosingMatch = matchBalancedBlock(ruleSlice);
141
- if (ruleClosingMatch) {
142
- const ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
143
- i+= ignoreRange;
144
- if (token.charAt(i) === '}') {
145
- i -= 1;
214
+ // Check if there's a semicolon before the invalid at-rule and the first opening brace
215
+ if (atRuleKey === "@layer") {
216
+ var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
217
+ if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";" ) {
218
+ // Ignore the rule block until the semicolon
219
+ i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
220
+ state = "before-selector";
221
+ return;
146
222
  }
147
- } else {
148
- i += ruleSlice.length;
149
223
  }
224
+
225
+ // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
226
+ i = ignoreBalancedBlock(i, ruleSlice);
150
227
  state = "before-selector";
151
228
  } else {
152
229
  validCallback.call(this);
153
230
  }
154
231
  }
155
232
 
156
- for (var character; (character = token.charAt(i)); i++) {
233
+ /**
234
+ * Regular expression to match a basic CSS selector.
235
+ *
236
+ * This regex matches the following selector components:
237
+ * - Type selectors (e.g., `div`, `span`)
238
+ * - Universal selector (`*`)
239
+ * - ID selectors (e.g., `#header`)
240
+ * - Class selectors (e.g., `.container`)
241
+ * - Attribute selectors (e.g., `[type="text"]`)
242
+ * - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`)
243
+ * - The parent selector (`&`)
244
+ * - Combinators (`>`, `+`, `~`) with optional whitespace
245
+ * - Whitespace (descendant combinator)
246
+ *
247
+ * The pattern ensures that a string consists only of valid basic selector components,
248
+ * possibly repeated and combined, but does not match full CSS selector groups separated by commas.
249
+ *
250
+ * @type {RegExp}
251
+ */
252
+ var basicSelectorRegExp = /^([a-zA-Z][a-zA-Z0-9_-]*|\*|#[a-zA-Z0-9_-]+|\.[a-zA-Z0-9_-]+|\[[^\[\]]*\]|::?[a-zA-Z0-9_-]+(?:\([^\(\)]*\))?|&|\s*[>+~]\s*|\s+)+$/;
253
+
254
+ /**
255
+ * Regular expression to match CSS pseudo-classes with arguments.
256
+ *
257
+ * Matches patterns like `:pseudo-class(argument)`, capturing the pseudo-class name and its argument.
258
+ *
259
+ * Capture groups:
260
+ * 1. The pseudo-class name (letters and hyphens).
261
+ * 2. The argument inside the parentheses (any characters except a closing parenthesis).
262
+ *
263
+ * Global flag (`g`) is used to find all matches in the input string.
264
+ *
265
+ * Example match: `:nth-child(2n+1)`
266
+ * - Group 1: "nth-child"
267
+ * - Group 2: "2n+1"
268
+ * @type {RegExp}
269
+ */
270
+ var globalPseudoClassRegExp = /:([a-zA-Z-]+)\(([^)]*)\)/g;
271
+
272
+ /**
273
+ * Parses a CSS selector string and splits it into parts, handling nested parentheses.
274
+ *
275
+ * This function is useful for splitting selectors that may contain nested function-like
276
+ * syntax (e.g., :not(.foo, .bar)), ensuring that commas inside parentheses do not split
277
+ * the selector.
278
+ *
279
+ * @param {string} selector - The CSS selector string to parse.
280
+ * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
281
+ */
282
+ function parseNestedSelectors(selector) {
283
+ var depth = 0;
284
+ var buffer = "";
285
+ var parts = [];
286
+ var i, char;
287
+
288
+ for (i = 0; i < selector.length; i++) {
289
+ char = selector.charAt(i);
290
+
291
+ if (char === '(') {
292
+ depth++;
293
+ buffer += char;
294
+ } else if (char === ')') {
295
+ depth--;
296
+ buffer += char;
297
+ if (depth === 0) {
298
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
299
+ buffer = "";
300
+ }
301
+ } else if (char === ',' && depth === 0) {
302
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
303
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
304
+ }
305
+ buffer = "";
306
+ } else {
307
+ buffer += char;
308
+ }
309
+ }
310
+
311
+ if (buffer.replace(/^\s+|\s+$/g, "")) {
312
+ parts.push(buffer.replace(/^\s+|\s+$/g, ""));
313
+ }
314
+
315
+ return parts;
316
+ }
317
+
318
+ /**
319
+ * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
320
+ *
321
+ * This function checks if the provided selector is valid according to the rules defined by
322
+ * `basicSelectorRegExp`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
323
+ * it recursively validates each nested selector using the same validation logic.
324
+ *
325
+ * @param {string} selector - The CSS selector string to validate.
326
+ * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
327
+ */
328
+ function validateSelector(selector) {
329
+ var match, nestedSelectors, i;
330
+
331
+ // Only pseudo-classes that accept selector lists should recurse
332
+ var selectorListPseudoClasses = {
333
+ 'not': true,
334
+ 'is': true,
335
+ 'has': true,
336
+ 'where': true
337
+ };
157
338
 
339
+ // Reset regex lastIndex for global regex in ES5 loop
340
+ var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags);
341
+ while ((match = pseudoClassRegExp.exec(selector)) !== null) {
342
+ var pseudoClass = match[1];
343
+ if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
344
+ nestedSelectors = parseNestedSelectors(match[2]);
345
+ // Validate each nested selector
346
+ for (i = 0; i < nestedSelectors.length; i++) {
347
+ if (!validateSelector(nestedSelectors[i])) {
348
+ return false;
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ // Allow "&" anywhere in the selector for nested selectors
355
+ return basicSelectorRegExp.test(selector);
356
+ }
357
+
358
+ /**
359
+ * Checks if a given CSS selector text is valid by splitting it by commas
360
+ * and validating each individual selector using the `validateSelector` function.
361
+ *
362
+ * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas.
363
+ * @returns {boolean} Returns true if all selectors are valid, otherwise false.
364
+ */
365
+ function isValidSelectorText(selectorText) {
366
+ // Split selectorText by commas and validate each part
367
+ var selectors = selectorText.split(',');
368
+ for (var i = 0; i < selectors.length; i++) {
369
+ if (!validateSelector(selectors[i].replace(/^\s+|\s+$/g, ""))) {
370
+ return false;
371
+ }
372
+ }
373
+ return true;
374
+ }
375
+
376
+ var endingIndex = token.length - 1;
377
+
378
+ for (var character; (character = token.charAt(i)); i++) {
379
+ if (i === endingIndex) {
380
+ switch (state) {
381
+ case "importRule":
382
+ case "layerBlock":
383
+ token += ";"
384
+ }
385
+ }
386
+
158
387
  switch (character) {
159
388
 
160
389
  case " ":
@@ -176,6 +405,9 @@ CSSOM.parse = function parse(token) {
176
405
  parseError('Unmatched "');
177
406
  }
178
407
  } while (token[index - 2] === '\\');
408
+ if (index === 0) {
409
+ break;
410
+ }
179
411
  buffer += token.slice(i, index);
180
412
  i = index - 1;
181
413
  switch (state) {
@@ -184,6 +416,9 @@ CSSOM.parse = function parse(token) {
184
416
  break;
185
417
  case 'importRule-begin':
186
418
  state = 'importRule';
419
+ if (i === endingIndex) {
420
+ token += ';'
421
+ }
187
422
  break;
188
423
  }
189
424
  break;
@@ -196,6 +431,9 @@ CSSOM.parse = function parse(token) {
196
431
  parseError("Unmatched '");
197
432
  }
198
433
  } while (token[index - 2] === '\\');
434
+ if (index === 0) {
435
+ break;
436
+ }
199
437
  buffer += token.slice(i, index);
200
438
  i = index - 1;
201
439
  switch (state) {
@@ -331,8 +569,13 @@ CSSOM.parse = function parse(token) {
331
569
  if (currentScope === styleSheet) {
332
570
  nestedSelectorRule = null;
333
571
  }
572
+ if (state === 'before-selector') {
573
+ parseError("Unexpected {");
574
+ i = ignoreBalancedBlock(i, token.slice(i));
575
+ break;
576
+ }
334
577
  if (state === "selector" || state === "atRule") {
335
- if (!nestedSelectorRule && buffer.includes(";")) {
578
+ if (!nestedSelectorRule && buffer.indexOf(";") !== -1) {
336
579
  var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
337
580
  if (ruleClosingMatch) {
338
581
  styleRule = null;
@@ -349,6 +592,7 @@ CSSOM.parse = function parse(token) {
349
592
  }
350
593
 
351
594
  currentScope = parentRule = styleRule;
595
+ console.log('sel out', buffer);
352
596
  styleRule.selectorText = buffer.trim();
353
597
  styleRule.style.__starts = i;
354
598
  styleRule.parentStyleSheet = styleSheet;
@@ -390,15 +634,19 @@ CSSOM.parse = function parse(token) {
390
634
  buffer = "";
391
635
  state = "before-selector";
392
636
  } else if (state === "layerBlock") {
393
- layerBlockRule.layerNameText = buffer.trim();
637
+ layerBlockRule.name = buffer.trim();
394
638
 
395
- if (parentRule) {
396
- layerBlockRule.parentRule = parentRule;
397
- ancestorRules.push(parentRule);
398
- }
639
+ var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(layerRuleNameRegExp) !== null;
399
640
 
400
- currentScope = parentRule = layerBlockRule;
401
- layerBlockRule.parentStyleSheet = styleSheet;
641
+ if (isValidName) {
642
+ if (parentRule) {
643
+ layerBlockRule.parentRule = parentRule;
644
+ ancestorRules.push(parentRule);
645
+ }
646
+
647
+ currentScope = parentRule = layerBlockRule;
648
+ layerBlockRule.parentStyleSheet = styleSheet;
649
+ }
402
650
  buffer = "";
403
651
  state = "before-selector";
404
652
  } else if (state === "hostRule-begin") {
@@ -456,7 +704,7 @@ CSSOM.parse = function parse(token) {
456
704
  documentRule.parentStyleSheet = styleSheet;
457
705
  buffer = "";
458
706
  state = "before-selector";
459
- } else if (state === "name") {
707
+ } else if (state === "before-name" || state === "name") {
460
708
  if (styleRule.constructor.name === "CSSNestedDeclarations") {
461
709
  if (styleRule.style.length) {
462
710
  parentRule.cssRules.push(styleRule);
@@ -473,9 +721,12 @@ CSSOM.parse = function parse(token) {
473
721
  styleRule.parentStyleSheet = styleSheet;
474
722
  }
475
723
 
476
-
477
724
  styleRule = new CSSOM.CSSStyleRule();
478
- styleRule.selectorText = buffer.trim();
725
+ console.log('sel in', buffer);
726
+ // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
727
+ styleRule.selectorText = parseNestedSelectors(buffer.trim()).map(function(sel) {
728
+ return sel.indexOf('&') === -1 ? '& ' + sel : sel;
729
+ }).join(', ');
479
730
  styleRule.style.__starts = i - buffer.length;
480
731
  styleRule.parentRule = parentRule;
481
732
  nestedSelectorRule = styleRule;
@@ -550,8 +801,14 @@ CSSOM.parse = function parse(token) {
550
801
 
551
802
  case ";":
552
803
  switch (state) {
804
+ case "before-value":
805
+ case "before-name":
806
+ parseError("Unexpected ;");
807
+ buffer = "";
808
+ state = "before-name";
809
+ break;
553
810
  case "value":
554
- styleRule.style.setProperty(name, buffer.trim(), priority);
811
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
555
812
  priority = "";
556
813
  buffer = "";
557
814
  state = "before-name";
@@ -561,10 +818,34 @@ CSSOM.parse = function parse(token) {
561
818
  state = "before-selector";
562
819
  break;
563
820
  case "importRule":
564
- importRule = new CSSOM.CSSImportRule();
565
- importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet;
566
- importRule.cssText = buffer + character;
567
- styleSheet.cssRules.push(importRule);
821
+ var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) {
822
+ return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
823
+ });
824
+ if (isValid) {
825
+ importRule = new CSSOM.CSSImportRule();
826
+ importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet;
827
+ importRule.cssText = buffer + character;
828
+ styleSheet.cssRules.push(importRule);
829
+ }
830
+ buffer = "";
831
+ state = "before-selector";
832
+ break;
833
+ case "layerBlock":
834
+ var nameListStr = buffer.trim().split(",").map(function (name) {
835
+ return name.trim();
836
+ });
837
+ var isInvalid = parentRule !== undefined || nameListStr.some(function (name) {
838
+ return name.trim().match(layerRuleNameRegExp) === null;
839
+ });
840
+
841
+ if (!isInvalid) {
842
+ layerStatementRule = new CSSOM.CSSLayerStatementRule();
843
+ layerStatementRule.parentStyleSheet = styleSheet;
844
+ layerStatementRule.__starts = layerBlockRule.__starts;
845
+ layerStatementRule.__ends = i;
846
+ layerStatementRule.nameList = nameListStr;
847
+ styleSheet.cssRules.push(layerStatementRule);
848
+ }
568
849
  buffer = "";
569
850
  state = "before-selector";
570
851
  break;
@@ -577,9 +858,10 @@ CSSOM.parse = function parse(token) {
577
858
  case "}":
578
859
  switch (state) {
579
860
  case "value":
580
- styleRule.style.setProperty(name, buffer.trim(), priority);
861
+ styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
581
862
  priority = "";
582
863
  /* falls through */
864
+ case "before-value":
583
865
  case "before-name":
584
866
  case "name":
585
867
  styleRule.__ends = i + 1;
@@ -597,7 +879,14 @@ CSSOM.parse = function parse(token) {
597
879
  currentScope = parentRule || styleSheet;
598
880
  }
599
881
 
600
- currentScope.cssRules.push(styleRule);
882
+ if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
883
+ if (styleRule === nestedSelectorRule) {
884
+ nestedSelectorRule = null;
885
+ }
886
+ parseError('Invalid CSSStyleRule.selectorText');
887
+ } else {
888
+ currentScope.cssRules.push(styleRule);
889
+ }
601
890
  buffer = "";
602
891
  if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
603
892
  state = "keyframeRule-begin";
@@ -619,12 +908,16 @@ CSSOM.parse = function parse(token) {
619
908
  case "selector":
620
909
  // End of media/supports/document rule.
621
910
  if (!parentRule) {
911
+ parseError("Unexpected }");
912
+
913
+ var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
914
+ if (hasPreviousStyleRule) {
915
+ i = ignoreBalancedBlock(i, token.slice(i), 1);
916
+ }
917
+
622
918
  break;
623
- //parseError("Unexpected }");
624
919
  }
625
920
 
626
- // Handle rules nested in @media or @supports
627
- hasAncestors = ancestorRules.length > 0;
628
921
 
629
922
  while (ancestorRules.length > 0) {
630
923
  parentRule = ancestorRules.pop();
@@ -651,14 +944,10 @@ CSSOM.parse = function parse(token) {
651
944
  } else {
652
945
  prevScope = currentScope;
653
946
  currentScope = parentRule;
654
- currentScope.cssRules.push(prevScope);
947
+ currentScope !== prevScope && currentScope.cssRules.push(prevScope);
655
948
  break;
656
949
  }
657
950
  }
658
-
659
- if (ancestorRules.length === 0) {
660
- hasAncestors = false;
661
- }
662
951
  }
663
952
 
664
953
  if (currentScope.parentRule == null) {
@@ -672,8 +961,12 @@ CSSOM.parse = function parse(token) {
672
961
  if (nestedSelectorRule === parentRule) {
673
962
  // Check if this selector is really starting inside another selector
674
963
  var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
675
-
676
- if (nestedSelectorTokenToCurrentSelectorToken.match(/{/g)?.length === nestedSelectorTokenToCurrentSelectorToken.match(/}/g)?.length) {
964
+ var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/{/g);
965
+ var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/}/g);
966
+ var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
967
+ var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
968
+
969
+ if (openingBraceLen === closingBraceLen) {
677
970
  // If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
678
971
  nestedSelectorRule.__ends = i + 1;
679
972
  nestedSelectorRule = null;
@@ -780,4 +1073,5 @@ CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
780
1073
  CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
781
1074
  CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
782
1075
  CSSOM.CSSLayerBlockRule = require("./CSSLayerBlockRule").CSSLayerBlockRule;
1076
+ CSSOM.CSSLayerStatementRule = require("./CSSLayerStatementRule").CSSLayerStatementRule;
783
1077
  ///CommonJS
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "parser",
8
8
  "styleSheet"
9
9
  ],
10
- "version": "0.9.0",
10
+ "version": "0.9.2",
11
11
  "author": "Nikita Vasilyev <me@elv1s.ru>",
12
12
  "contributors": [
13
13
  "Acemir Sousa Mendes <acemirsm@gmail.com>"