@acemir/cssom 0.9.19 → 0.9.21

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 CHANGED
@@ -326,6 +326,12 @@ Object.defineProperties(CSSOM.CSSRule.prototype, {
326
326
 
327
327
  constructor: { value: CSSOM.CSSRule },
328
328
 
329
+ cssRule: {
330
+ value: "",
331
+ configurable: true,
332
+ enumerable: true
333
+ },
334
+
329
335
  parentRule: {
330
336
  get: function() {
331
337
  return this.__parentRule
@@ -364,6 +370,28 @@ Object.defineProperties(CSSOM.CSSRule.prototype, {
364
370
 
365
371
 
366
372
 
373
+ /**
374
+ * @constructor
375
+ * @see https://drafts.csswg.org/cssom/#the-cssrulelist-interface
376
+ */
377
+ CSSOM.CSSRuleList = function CSSRuleList(){
378
+ const arr = new Array();
379
+ Object.setPrototypeOf(arr, CSSOM.CSSRuleList.prototype);
380
+ return arr;
381
+ };
382
+
383
+ CSSOM.CSSRuleList.prototype = Object.create(Array.prototype);
384
+ CSSOM.CSSRuleList.prototype.constructor = CSSOM.CSSRuleList;
385
+
386
+ CSSOM.CSSRuleList.prototype.item = function(index) {
387
+ return this[index] || null;
388
+ };
389
+
390
+
391
+
392
+
393
+
394
+
367
395
  /**
368
396
  * @constructor
369
397
  * @see https://drafts.csswg.org/css-nesting-1/
@@ -408,7 +436,7 @@ Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "cssText", {
408
436
  */
409
437
  CSSOM.CSSGroupingRule = function CSSGroupingRule() {
410
438
  CSSOM.CSSRule.call(this);
411
- this.cssRules = [];
439
+ this.cssRules = new CSSOM.CSSRuleList();
412
440
  };
413
441
 
414
442
  CSSOM.CSSGroupingRule.prototype = new CSSOM.CSSRule();
@@ -525,13 +553,17 @@ CSSOM.CSSCounterStyleRule.prototype.type = 11;
525
553
  */
526
554
  CSSOM.CSSConditionRule = function CSSConditionRule() {
527
555
  CSSOM.CSSGroupingRule.call(this);
528
- this.cssRules = [];
556
+ this.__conditionText = '';
529
557
  };
530
558
 
531
559
  CSSOM.CSSConditionRule.prototype = new CSSOM.CSSGroupingRule();
532
560
  CSSOM.CSSConditionRule.prototype.constructor = CSSOM.CSSConditionRule;
533
- CSSOM.CSSConditionRule.prototype.conditionText = ''
534
- CSSOM.CSSConditionRule.prototype.cssText = ''
561
+
562
+ Object.defineProperty(CSSOM.CSSConditionRule.prototype, "conditionText", {
563
+ get: function () {
564
+ return this.__conditionText;
565
+ }
566
+ });
535
567
 
536
568
 
537
569
 
@@ -562,7 +594,20 @@ Object.defineProperty(CSSOM.CSSStyleRule.prototype, "selectorText", {
562
594
  return this.__selectorText;
563
595
  },
564
596
  set: function(value) {
565
- this.__selectorText = value;
597
+ if (typeof value === "string") {
598
+ var trimmedValue = value.trim();
599
+
600
+ if (trimmedValue === '') {
601
+ return;
602
+ }
603
+
604
+ // TODO: Setting invalid selectorText should be ignored
605
+ // There are some validations already on lib/parse.js
606
+ // but the same validations should be applied here.
607
+ // Check if we can move these validation logic to a shared function.
608
+
609
+ this.__selectorText = trimmedValue;
610
+ }
566
611
  }
567
612
  });
568
613
 
@@ -599,9 +644,11 @@ Object.defineProperty(CSSOM.CSSStyleRule.prototype, "cssText", {
599
644
  return text;
600
645
  },
601
646
  set: function(cssText) {
602
- var rule = CSSOM.CSSStyleRule.parse(cssText);
603
- this.__style = rule.style;
604
- this.__selectorText = rule.selectorText;
647
+ if (typeof cssText === "string") {
648
+ var rule = CSSOM.CSSStyleRule.parse(cssText);
649
+ this.__style = rule.style;
650
+ this.selectorText = rule.selectorText;
651
+ }
605
652
  }
606
653
  });
607
654
 
@@ -819,7 +866,7 @@ CSSOM.MediaList.prototype = {
819
866
  */
820
867
  CSSOM.CSSMediaRule = function CSSMediaRule() {
821
868
  CSSOM.CSSConditionRule.call(this);
822
- this.media = new CSSOM.MediaList();
869
+ this.__media = new CSSOM.MediaList();
823
870
  };
824
871
 
825
872
  CSSOM.CSSMediaRule.prototype = new CSSOM.CSSConditionRule();
@@ -828,16 +875,24 @@ CSSOM.CSSMediaRule.prototype.type = 4;
828
875
 
829
876
  // https://opensource.apple.com/source/WebCore/WebCore-7611.1.21.161.3/css/CSSMediaRule.cpp
830
877
  Object.defineProperties(CSSOM.CSSMediaRule.prototype, {
831
- "conditionText": {
878
+ "media": {
832
879
  get: function() {
833
- return this.media.mediaText;
880
+ return this.__media;
834
881
  },
835
882
  set: function(value) {
836
- this.media.mediaText = value;
883
+ if (typeof value === "string") {
884
+ this.__media.mediaText = value;
885
+ } else {
886
+ this.__media = value;
887
+ }
837
888
  },
838
- configurable: true,
839
889
  enumerable: true
840
890
  },
891
+ "conditionText": {
892
+ get: function() {
893
+ return this.media.mediaText;
894
+ }
895
+ },
841
896
  "cssText": {
842
897
  get: function() {
843
898
  var cssTexts = [];
@@ -870,27 +925,35 @@ CSSOM.CSSContainerRule.prototype.constructor = CSSOM.CSSContainerRule;
870
925
  CSSOM.CSSContainerRule.prototype.type = 17;
871
926
 
872
927
  Object.defineProperties(CSSOM.CSSContainerRule.prototype, {
873
- "conditionText": {
874
- get: function() {
875
- return this.containerText;
876
- },
877
- set: function(value) {
878
- this.containerText = value;
879
- },
880
- configurable: true,
881
- enumerable: true
882
- },
883
928
  "cssText": {
884
929
  get: function() {
885
930
  var cssTexts = [];
886
931
  for (var i=0, length=this.cssRules.length; i < length; i++) {
887
932
  cssTexts.push(this.cssRules[i].cssText);
888
933
  }
889
- return "@container " + this.containerText + " {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
934
+ return "@container " + this.conditionText + " {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
890
935
  },
891
936
  configurable: true,
892
937
  enumerable: true
893
- }
938
+ },
939
+ "containerName": {
940
+ get: function() {
941
+ var parts = this.conditionText.trim().split(/\s+/);
942
+ if (parts.length > 1 && parts[0] !== '(' && !parts[0].startsWith('(')) {
943
+ return parts[0];
944
+ }
945
+ return "";
946
+ }
947
+ },
948
+ "containerQuery": {
949
+ get: function() {
950
+ var parts = this.conditionText.trim().split(/\s+/);
951
+ if (parts.length > 1 && parts[0] !== '(' && !parts[0].startsWith('(')) {
952
+ return parts.slice(1).join(' ');
953
+ }
954
+ return this.conditionText;
955
+ }
956
+ },
894
957
  });
895
958
 
896
959
 
@@ -933,21 +996,25 @@ Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "cssText", {
933
996
  */
934
997
  CSSOM.CSSImportRule = function CSSImportRule() {
935
998
  CSSOM.CSSRule.call(this);
936
- this.href = "";
937
- this.media = new CSSOM.MediaList();
938
- this.layerName = null;
939
- this.supportsText = null;
940
- this.styleSheet = new CSSOM.CSSStyleSheet();
999
+ this.__href = "";
1000
+ this.__media = new CSSOM.MediaList();
1001
+ this.__layerName = null;
1002
+ this.__supportsText = null;
1003
+ this.__styleSheet = new CSSOM.CSSStyleSheet();
941
1004
  };
942
1005
 
943
1006
  CSSOM.CSSImportRule.prototype = new CSSOM.CSSRule();
944
1007
  CSSOM.CSSImportRule.prototype.constructor = CSSOM.CSSImportRule;
945
- CSSOM.CSSImportRule.prototype.type = 3;
1008
+
1009
+ Object.defineProperty(CSSOM.CSSImportRule.prototype, "type", {
1010
+ value: 3,
1011
+ writable: false
1012
+ });
946
1013
 
947
1014
  Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
948
1015
  get: function() {
949
1016
  var mediaText = this.media.mediaText;
950
- return "@import url(\"" + this.href + "\")" + (this.layerName !== null ? " layer" + (this.layerName && "(" + this.layerName + ")") : "" ) + (this.supportsText ? " supports(" + this.supportsText + ")" : "" ) + (mediaText ? " " + mediaText : "") + ";";
1017
+ return "@import url(\"" + this.href.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + "\")" + (this.layerName !== null ? " layer" + (this.layerName && "(" + this.layerName + ")") : "" ) + (this.supportsText ? " supports(" + this.supportsText + ")" : "" ) + (mediaText ? " " + mediaText : "") + ";";
951
1018
  },
952
1019
  set: function(cssText) {
953
1020
  var i = 0;
@@ -966,8 +1033,40 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
966
1033
 
967
1034
  var layerRegExp = /layer\(([^)]*)\)/;
968
1035
  var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/;
969
- var supportsRegExp = /supports\(([^)]+)\)/;
970
1036
  var doubleOrMoreSpacesRegExp = /\s{2,}/g;
1037
+
1038
+ /**
1039
+ * Extracts the content inside supports() handling nested parentheses.
1040
+ * @param {string} text - The text to parse
1041
+ * @returns {object|null} - {content: string, endIndex: number} or null if not found
1042
+ */
1043
+ function extractSupportsContent(text) {
1044
+ var supportsIndex = text.indexOf('supports(');
1045
+ if (supportsIndex !== 0) {
1046
+ return null;
1047
+ }
1048
+
1049
+ var depth = 0;
1050
+ var start = supportsIndex + 'supports('.length;
1051
+ var i = start;
1052
+
1053
+ for (; i < text.length; i++) {
1054
+ if (text[i] === '(') {
1055
+ depth++;
1056
+ } else if (text[i] === ')') {
1057
+ if (depth === 0) {
1058
+ // Found the closing parenthesis for supports()
1059
+ return {
1060
+ content: text.slice(start, i),
1061
+ endIndex: i
1062
+ };
1063
+ }
1064
+ depth--;
1065
+ }
1066
+ }
1067
+
1068
+ return null; // Unbalanced parentheses
1069
+ }
971
1070
 
972
1071
  for (var character; (character = cssText.charAt(i)); i++) {
973
1072
 
@@ -1008,7 +1107,7 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
1008
1107
  url = url.slice(1, -1);
1009
1108
  }
1010
1109
  }
1011
- this.href = url;
1110
+ this.__href = url;
1012
1111
  i = index;
1013
1112
  state = 'media';
1014
1113
  }
@@ -1020,7 +1119,7 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
1020
1119
  if (!index) {
1021
1120
  throw i + ": '\"' not found";
1022
1121
  }
1023
- this.href = cssText.slice(i + 1, index);
1122
+ this.__href = cssText.slice(i + 1, index);
1024
1123
  i = index;
1025
1124
  state = 'media';
1026
1125
  }
@@ -1032,7 +1131,7 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
1032
1131
  if (!index) {
1033
1132
  throw i + ': "\'" not found';
1034
1133
  }
1035
- this.href = cssText.slice(i + 1, index);
1134
+ this.__href = cssText.slice(i + 1, index);
1036
1135
  i = index;
1037
1136
  state = 'media';
1038
1137
  }
@@ -1050,7 +1149,7 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
1050
1149
  var layerName = layerMatch[1].trim();
1051
1150
 
1052
1151
  if (layerName.match(layerRuleNameRegExp) !== null) {
1053
- this.layerName = layerMatch[1].trim();
1152
+ this.__layerName = layerMatch[1].trim();
1054
1153
  bufferTrimmed = bufferTrimmed.replace(layerRegExp, '')
1055
1154
  .replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space
1056
1155
  .trim();
@@ -1063,19 +1162,19 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
1063
1162
  }
1064
1163
  }
1065
1164
  } else {
1066
- this.layerName = "";
1165
+ this.__layerName = "";
1067
1166
  bufferTrimmed = bufferTrimmed.substring('layer'.length).trim()
1068
1167
  }
1069
1168
  }
1070
1169
 
1071
- var supportsMatch = bufferTrimmed.match(supportsRegExp);
1170
+ var supportsResult = extractSupportsContent(bufferTrimmed);
1072
1171
 
1073
- if (supportsMatch && supportsMatch.index === 0) {
1172
+ if (supportsResult) {
1074
1173
  // REVIEW: In the browser, an empty supports() invalidates and ignores the entire @import rule
1075
- this.supportsText = supportsMatch[1].trim();
1076
- bufferTrimmed = bufferTrimmed.replace(supportsRegExp, '')
1077
- .replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space
1078
- .trim();
1174
+ this.__supportsText = supportsResult.content.trim();
1175
+ // Remove the entire supports(...) from the buffer
1176
+ bufferTrimmed = bufferTrimmed.slice(0, 0) + bufferTrimmed.slice(supportsResult.endIndex + 1);
1177
+ bufferTrimmed = bufferTrimmed.replace(doubleOrMoreSpacesRegExp, ' ').trim();
1079
1178
  }
1080
1179
 
1081
1180
  // REVIEW: In the browser, any invalid media is replaced with 'not all'
@@ -1096,6 +1195,43 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
1096
1195
  }
1097
1196
  });
1098
1197
 
1198
+ Object.defineProperty(CSSOM.CSSImportRule.prototype, "href", {
1199
+ get: function() {
1200
+ return this.__href;
1201
+ }
1202
+ });
1203
+
1204
+ Object.defineProperty(CSSOM.CSSImportRule.prototype, "media", {
1205
+ get: function() {
1206
+ return this.__media;
1207
+ },
1208
+ set: function(value) {
1209
+ if (typeof value === "string") {
1210
+ this.__media.mediaText = value;
1211
+ } else {
1212
+ this.__media = value;
1213
+ }
1214
+ }
1215
+ });
1216
+
1217
+ Object.defineProperty(CSSOM.CSSImportRule.prototype, "layerName", {
1218
+ get: function() {
1219
+ return this.__layerName;
1220
+ }
1221
+ });
1222
+
1223
+ Object.defineProperty(CSSOM.CSSImportRule.prototype, "supportsText", {
1224
+ get: function() {
1225
+ return this.__supportsText;
1226
+ }
1227
+ });
1228
+
1229
+ Object.defineProperty(CSSOM.CSSImportRule.prototype, "styleSheet", {
1230
+ get: function() {
1231
+ return this.__styleSheet;
1232
+ }
1233
+ });
1234
+
1099
1235
 
1100
1236
 
1101
1237
 
@@ -1107,23 +1243,25 @@ Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
1107
1243
  */
1108
1244
  CSSOM.CSSNamespaceRule = function CSSNamespaceRule() {
1109
1245
  CSSOM.CSSRule.call(this);
1110
- this.prefix = "";
1111
- this.namespaceURI = "";
1112
- this.styleSheet = new CSSOM.CSSStyleSheet();
1246
+ this.__prefix = "";
1247
+ this.__namespaceURI = "";
1113
1248
  };
1114
1249
 
1115
1250
  CSSOM.CSSNamespaceRule.prototype = new CSSOM.CSSRule();
1116
1251
  CSSOM.CSSNamespaceRule.prototype.constructor = CSSOM.CSSNamespaceRule;
1117
- CSSOM.CSSNamespaceRule.prototype.type = 10;
1252
+
1253
+ Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "type", {
1254
+ value: 10,
1255
+ writable: false
1256
+ });
1118
1257
 
1119
1258
  Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "cssText", {
1120
1259
  get: function() {
1121
1260
  return "@namespace" + (this.prefix && " " + this.prefix) + " url(\"" + this.namespaceURI + "\");";
1122
1261
  },
1123
1262
  set: function(cssText) {
1124
- // Reset prefix and namespaceURI
1125
- this.prefix = "";
1126
- this.namespaceURI = "";
1263
+ var newPrefix = "";
1264
+ var newNamespaceURI = "";
1127
1265
 
1128
1266
  // Remove @namespace and trim
1129
1267
  var text = cssText.trim();
@@ -1146,26 +1284,40 @@ Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "cssText", {
1146
1284
  if (match) {
1147
1285
  // If prefix is present
1148
1286
  if (match[1]) {
1149
- this.prefix = match[1];
1287
+ newPrefix = match[1];
1150
1288
  }
1151
1289
  // If url(...) form with quotes
1152
1290
  if (typeof match[3] !== "undefined") {
1153
- this.namespaceURI = match[3];
1291
+ newNamespaceURI = match[3];
1154
1292
  }
1155
1293
  // If url(...) form without quotes
1156
1294
  else if (typeof match[4] !== "undefined") {
1157
- this.namespaceURI = match[4].trim();
1295
+ newNamespaceURI = match[4].trim();
1158
1296
  }
1159
1297
  // If quoted string form
1160
1298
  else if (typeof match[6] !== "undefined") {
1161
- this.namespaceURI = match[6];
1299
+ newNamespaceURI = match[6];
1162
1300
  }
1301
+
1302
+ this.__prefix = newPrefix;
1303
+ this.__namespaceURI = newNamespaceURI;
1163
1304
  } else {
1164
1305
  throw new DOMException("Invalid @namespace rule", "InvalidStateError");
1165
1306
  }
1166
1307
  }
1167
1308
  });
1168
1309
 
1310
+ Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "prefix", {
1311
+ get: function() {
1312
+ return this.__prefix;
1313
+ }
1314
+ });
1315
+
1316
+ Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "namespaceURI", {
1317
+ get: function() {
1318
+ return this.__namespaceURI;
1319
+ }
1320
+ });
1169
1321
 
1170
1322
 
1171
1323
 
@@ -1219,7 +1371,7 @@ Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", {
1219
1371
  */
1220
1372
  CSSOM.CSSHostRule = function CSSHostRule() {
1221
1373
  CSSOM.CSSRule.call(this);
1222
- this.cssRules = [];
1374
+ this.cssRules = new CSSOM.CSSRuleList();
1223
1375
  };
1224
1376
 
1225
1377
  CSSOM.CSSHostRule.prototype = new CSSOM.CSSRule();
@@ -1249,11 +1401,10 @@ Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", {
1249
1401
  * @see http://www.w3.org/TR/shadow-dom/#host-at-rule
1250
1402
  */
1251
1403
  CSSOM.CSSStartingStyleRule = function CSSStartingStyleRule() {
1252
- CSSOM.CSSRule.call(this);
1253
- this.cssRules = [];
1404
+ CSSOM.CSSGroupingRule.call(this);
1254
1405
  };
1255
1406
 
1256
- CSSOM.CSSStartingStyleRule.prototype = new CSSOM.CSSRule();
1407
+ CSSOM.CSSStartingStyleRule.prototype = new CSSOM.CSSGroupingRule();
1257
1408
  CSSOM.CSSStartingStyleRule.prototype.constructor = CSSOM.CSSStartingStyleRule;
1258
1409
  CSSOM.CSSStartingStyleRule.prototype.type = 1002;
1259
1410
  //FIXME
@@ -1280,10 +1431,23 @@ Object.defineProperty(CSSOM.CSSStartingStyleRule.prototype, "cssText", {
1280
1431
  * @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface
1281
1432
  */
1282
1433
  CSSOM.StyleSheet = function StyleSheet() {
1434
+ this.__media = new CSSOM.MediaList();
1283
1435
  this.__parentStyleSheet = null;
1284
1436
  };
1285
1437
 
1286
1438
  Object.defineProperties(CSSOM.StyleSheet.prototype, {
1439
+ media: {
1440
+ get: function() {
1441
+ return this.__media;
1442
+ },
1443
+ set: function(value) {
1444
+ if (typeof value === "string") {
1445
+ this.__media.mediaText = value;
1446
+ } else {
1447
+ this.__media = value;
1448
+ }
1449
+ }
1450
+ },
1287
1451
  parentStyleSheet: {
1288
1452
  get: function() {
1289
1453
  return this.__parentStyleSheet;
@@ -1301,13 +1465,18 @@ Object.defineProperties(CSSOM.StyleSheet.prototype, {
1301
1465
  */
1302
1466
  CSSOM.CSSStyleSheet = function CSSStyleSheet() {
1303
1467
  CSSOM.StyleSheet.call(this);
1304
- this.cssRules = [];
1468
+ this.cssRules = new CSSOM.CSSRuleList();
1305
1469
  };
1306
1470
 
1307
1471
 
1308
1472
  CSSOM.CSSStyleSheet.prototype = new CSSOM.StyleSheet();
1309
1473
  CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet;
1310
1474
 
1475
+ Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "rules", {
1476
+ get: function() {
1477
+ return this.cssRules;
1478
+ }
1479
+ });
1311
1480
 
1312
1481
  /**
1313
1482
  * Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade.
@@ -1341,7 +1510,10 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
1341
1510
  }
1342
1511
 
1343
1512
  var ruleToParse = String(rule);
1344
- var parsedSheet = CSSOM.parse(ruleToParse);
1513
+ var parseErrors = [];
1514
+ var parsedSheet = CSSOM.parse(ruleToParse, undefined, function(err) {
1515
+ parseErrors.push(err);
1516
+ } );
1345
1517
  if (parsedSheet.cssRules.length !== 1) {
1346
1518
  errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
1347
1519
  }
@@ -1409,12 +1581,21 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
1409
1581
  'HierarchyRequestError');
1410
1582
  }
1411
1583
 
1584
+ // Cannot insert if there are already non-special rules
1585
+ if (firstNonImportNamespaceIndex < this.cssRules.length) {
1586
+ errorUtils.throwError(this, 'DOMException',
1587
+ "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
1588
+ 'InvalidStateError');
1589
+ }
1590
+
1412
1591
  // Cannot insert after other types of rules
1413
1592
  if (index > firstNonImportNamespaceIndex) {
1414
1593
  errorUtils.throwError(this, 'DOMException',
1415
1594
  "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
1416
1595
  'HierarchyRequestError');
1417
1596
  }
1597
+
1598
+
1418
1599
  } else if (cssRule.constructor.name === 'CSSLayerStatementRule') {
1419
1600
  // @layer statement rules can be inserted anywhere before @import and @namespace
1420
1601
  // No additional restrictions beyond what's already handled
@@ -1431,6 +1612,10 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
1431
1612
  "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
1432
1613
  'HierarchyRequestError');
1433
1614
  }
1615
+
1616
+ if (parseErrors.length !== 0) {
1617
+ errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
1618
+ }
1434
1619
  }
1435
1620
 
1436
1621
  cssRule.__parentStyleSheet = this;
@@ -1438,6 +1623,13 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
1438
1623
  return index;
1439
1624
  };
1440
1625
 
1626
+ CSSOM.CSSStyleSheet.prototype.addRule = function(selector, styleBlock, index) {
1627
+ if (index === void 0) {
1628
+ index = this.cssRules.length;
1629
+ }
1630
+ this.insertRule(selector + "{" + styleBlock + "}", index);
1631
+ return -1;
1632
+ };
1441
1633
 
1442
1634
  /**
1443
1635
  * Used to delete a rule from the style sheet.
@@ -1463,17 +1655,27 @@ CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) {
1463
1655
  if (index >= this.cssRules.length) {
1464
1656
  errorUtils.throwIndexError(this, 'deleteRule', this.constructor.name, index, this.cssRules.length);
1465
1657
  }
1466
- if (this.cssRules[index] && this.cssRules[index].constructor.name == "CSSNamespaceRule") {
1467
- var shouldContinue = this.cssRules.every(function (rule) {
1468
- return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
1469
- });
1470
- if (!shouldContinue) {
1471
- errorUtils.throwError(this, 'DOMException', "Failed to execute 'deleteRule' on '" + this.constructor.name + "': Failed to delete rule.", "InvalidStateError");
1658
+ if (this.cssRules[index]) {
1659
+ if (this.cssRules[index].constructor.name == "CSSNamespaceRule") {
1660
+ var shouldContinue = this.cssRules.every(function (rule) {
1661
+ return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
1662
+ });
1663
+ if (!shouldContinue) {
1664
+ errorUtils.throwError(this, 'DOMException', "Failed to execute 'deleteRule' on '" + this.constructor.name + "': Failed to delete rule.", "InvalidStateError");
1665
+ }
1472
1666
  }
1667
+ if (this.cssRules[index].constructor.name == "CSSImportRule") {
1668
+ this.cssRules[index].styleSheet.__parentStyleSheet = null;
1669
+ }
1670
+
1671
+ this.cssRules[index].__parentStyleSheet = null;
1473
1672
  }
1474
1673
  this.cssRules.splice(index, 1);
1475
1674
  };
1476
1675
 
1676
+ CSSOM.CSSStyleSheet.prototype.removeRule = function(index) {
1677
+ this.deleteRule(index);
1678
+ };
1477
1679
 
1478
1680
  /**
1479
1681
  * NON-STANDARD
@@ -1500,7 +1702,7 @@ CSSOM.CSSStyleSheet.prototype.toString = function() {
1500
1702
  CSSOM.CSSKeyframesRule = function CSSKeyframesRule() {
1501
1703
  CSSOM.CSSRule.call(this);
1502
1704
  this.name = '';
1503
- this.cssRules = [];
1705
+ this.cssRules = new CSSOM.CSSRuleList();
1504
1706
 
1505
1707
  // Set up initial indexed access
1506
1708
  this._setupIndexedAccess();
@@ -1826,7 +2028,7 @@ CSSOM.MatcherList.prototype = {
1826
2028
  CSSOM.CSSDocumentRule = function CSSDocumentRule() {
1827
2029
  CSSOM.CSSRule.call(this);
1828
2030
  this.matcher = new CSSOM.MatcherList();
1829
- this.cssRules = [];
2031
+ this.cssRules = new CSSOM.CSSRuleList();
1830
2032
  };
1831
2033
 
1832
2034
  CSSOM.CSSDocumentRule.prototype = new CSSOM.CSSRule();
@@ -2227,6 +2429,51 @@ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
2227
2429
 
2228
2430
 
2229
2431
 
2432
+ /**
2433
+ * @constructor
2434
+ * @see https://drafts.csswg.org/css-cascade-6/#cssscoperule
2435
+ */
2436
+ CSSOM.CSSScopeRule = function CSSScopeRule() {
2437
+ CSSOM.CSSGroupingRule.call(this);
2438
+ this.__start = null;
2439
+ this.__end = null;
2440
+ };
2441
+
2442
+ CSSOM.CSSScopeRule.prototype = new CSSOM.CSSGroupingRule();
2443
+ CSSOM.CSSScopeRule.prototype.constructor = CSSOM.CSSScopeRule;
2444
+
2445
+
2446
+ Object.defineProperties(CSSOM.CSSScopeRule.prototype, {
2447
+ type: {
2448
+ value: 0,
2449
+ writable: false,
2450
+ },
2451
+ cssText: {
2452
+ get: function () {
2453
+ var cssTexts = [];
2454
+ for (var i = 0, length = this.cssRules.length; i < length; i++) {
2455
+ cssTexts.push(this.cssRules[i].cssText);
2456
+ }
2457
+ return "@scope " + (this.start ? "(" + this.start + ") " : "") + (this.end ? "to (" + this.end + ") " : "") + "{" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
2458
+ },
2459
+ configurable: true,
2460
+ enumerable: true,
2461
+ },
2462
+ start: {
2463
+ get: function () {
2464
+ return this.__start;
2465
+ }
2466
+ },
2467
+ end: {
2468
+ get: function () {
2469
+ return this.__end;
2470
+ }
2471
+ }
2472
+ });
2473
+
2474
+
2475
+
2476
+
2230
2477
  /**
2231
2478
  * @constructor
2232
2479
  * @see https://drafts.csswg.org/css-cascade-5/#csslayerblockrule
@@ -2234,7 +2481,6 @@ CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
2234
2481
  CSSOM.CSSLayerBlockRule = function CSSLayerBlockRule() {
2235
2482
  CSSOM.CSSGroupingRule.call(this);
2236
2483
  this.name = "";
2237
- this.cssRules = [];
2238
2484
  };
2239
2485
 
2240
2486
  CSSOM.CSSLayerBlockRule.prototype = new CSSOM.CSSGroupingRule();
@@ -2286,63 +2532,325 @@ Object.defineProperties(CSSOM.CSSLayerStatementRule.prototype, {
2286
2532
 
2287
2533
 
2288
2534
  /**
2289
- * Parses a CSS string and returns a CSSOM.CSSStyleSheet object representing the parsed stylesheet.
2290
- *
2291
- * @param {string} token - The CSS string to parse.
2292
- * @param {object} [opts] - Optional parsing options.
2293
- * @param {object} [opts.globalObject] - An optional global object to attach to the stylesheet. Useful on jsdom webplatform tests.
2294
- * @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`.
2295
- * @returns {CSSOM.CSSStyleSheet} The parsed CSSStyleSheet object.
2535
+ * @constructor
2536
+ * @see https://drafts.csswg.org/cssom/#the-csspagerule-interface
2296
2537
  */
2297
- CSSOM.parse = function parse(token, opts, errorHandler) {
2298
- errorHandler = errorHandler === true ? (console && console.error) : errorHandler;
2538
+ CSSOM.CSSPageRule = function CSSPageRule() {
2539
+ CSSOM.CSSGroupingRule.call(this);
2540
+ this.__style = new CSSOM.CSSStyleDeclaration();
2541
+ this.__style.parentRule = this;
2542
+ };
2299
2543
 
2300
- var i = 0;
2544
+ CSSOM.CSSPageRule.prototype = new CSSOM.CSSGroupingRule();
2545
+ CSSOM.CSSPageRule.prototype.constructor = CSSOM.CSSPageRule;
2301
2546
 
2302
- /**
2303
- "before-selector" or
2304
- "selector" or
2305
- "atRule" or
2306
- "atBlock" or
2307
- "conditionBlock" or
2308
- "before-name" or
2309
- "name" or
2310
- "before-value" or
2311
- "value"
2312
- */
2313
- var state = "before-selector";
2547
+ Object.defineProperty(CSSOM.CSSPageRule.prototype, "type", {
2548
+ value: 6,
2549
+ writable: false
2550
+ });
2551
+
2552
+ Object.defineProperty(CSSOM.CSSPageRule.prototype, "selectorText", {
2553
+ get: function() {
2554
+ return this.__selectorText;
2555
+ },
2556
+ set: function(value) {
2557
+ if (typeof value === "string") {
2558
+ var trimmedValue = value.trim();
2559
+
2560
+ // Empty selector is valid for @page
2561
+ if (trimmedValue === '') {
2562
+ this.__selectorText = '';
2563
+ return;
2564
+ }
2565
+
2566
+ // Parse @page selectorText for page name and pseudo-pages
2567
+ // Valid formats:
2568
+ // - (empty - no name, no pseudo-page)
2569
+ // - :left, :right, :first, :blank (pseudo-page only)
2570
+ // - named (named page only)
2571
+ // - named:first (named page with single pseudo-page)
2572
+ // - named:first:left (named page with multiple pseudo-pages)
2573
+ var atPageRuleSelectorRegExp = /^([^\s:]+)?((?::\w+)*)$/;
2574
+ var match = trimmedValue.match(atPageRuleSelectorRegExp);
2575
+ if (match) {
2576
+ var pageName = match[1] || '';
2577
+ var pseudoPages = match[2] || '';
2578
+
2579
+ // Validate page name if present
2580
+ if (pageName) {
2581
+ var cssCustomIdentifierRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a css custom identifier
2582
+ // Page name can be an identifier or a string
2583
+ if (!cssCustomIdentifierRegExp.test(pageName)) {
2584
+ return;
2585
+ }
2586
+ }
2587
+
2588
+ // Validate pseudo-pages if present
2589
+ if (pseudoPages) {
2590
+ var pseudos = pseudoPages.split(':').filter(function(p) { return p; });
2591
+ var validPseudos = ['left', 'right', 'first', 'blank'];
2592
+ var allValid = true;
2593
+ for (var j = 0; j < pseudos.length; j++) {
2594
+ if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) {
2595
+ allValid = false;
2596
+ break;
2597
+ }
2598
+ }
2599
+
2600
+ if (!allValid) {
2601
+ return; // Invalid pseudo-page, do nothing
2602
+ }
2603
+ }
2604
+
2605
+ this.__selectorText = pageName + pseudoPages.toLowerCase();
2606
+ }
2607
+ }
2608
+ }
2609
+ });
2610
+
2611
+ Object.defineProperty(CSSOM.CSSPageRule.prototype, "style", {
2612
+ get: function() {
2613
+ return this.__style;
2614
+ },
2615
+ set: function(value) {
2616
+ if (typeof value === "string") {
2617
+ this.__style.cssText = value;
2618
+ } else {
2619
+ this.__style = value;
2620
+ }
2621
+ }
2622
+ });
2623
+
2624
+ Object.defineProperty(CSSOM.CSSPageRule.prototype, "cssText", {
2625
+ get: function() {
2626
+ var values = ""
2627
+ if (this.cssRules.length) {
2628
+ var valuesArr = [" {"];
2629
+ this.style.cssText && valuesArr.push(this.style.cssText);
2630
+ valuesArr.push(this.cssRules.map(function(rule){ return rule.cssText }).join("\n "));
2631
+ values = valuesArr.join("\n ") + "\n}"
2632
+ } else {
2633
+ values = " {" + (this.style.cssText ? " " + this.style.cssText : "") + " }";
2634
+ }
2635
+ return "@page" + (this.selectorText ? " " + this.selectorText : "") + values;
2636
+ },
2637
+ set: function(cssText) {
2638
+ if (typeof value === "string") {
2639
+ var rule = CSSOM.CSSPageRule.parse(cssText);
2640
+ this.__style = rule.style;
2641
+ this.selectorText = rule.selectorText;
2642
+ }
2643
+ }
2644
+ });
2314
2645
 
2646
+ /**
2647
+ * NON-STANDARD
2648
+ * lightweight version of parse.js.
2649
+ * @param {string} ruleText
2650
+ * @return CSSPageRule
2651
+ */
2652
+ CSSOM.CSSPageRule.parse = function(ruleText) {
2653
+ var i = 0;
2654
+ var state = "selector";
2315
2655
  var index;
2656
+ var j = i;
2316
2657
  var buffer = "";
2317
- var valueParenthesisDepth = 0;
2318
2658
 
2319
2659
  var SIGNIFICANT_WHITESPACE = {
2320
- "name": true,
2321
- "before-name": true,
2322
2660
  "selector": true,
2323
- "value": true,
2324
- "value-parenthesis": true,
2325
- "atRule": true,
2326
- "importRule-begin": true,
2327
- "importRule": true,
2328
- "namespaceRule-begin": true,
2329
- "namespaceRule": true,
2330
- "atBlock": true,
2331
- "containerBlock": true,
2332
- "conditionBlock": true,
2333
- "counterStyleBlock": true,
2334
- 'documentRule-begin': true,
2335
- "layerBlock": true
2661
+ "value": true
2336
2662
  };
2337
2663
 
2338
- var styleSheet = new CSSOM.CSSStyleSheet();
2664
+ var pageRule = new CSSOM.CSSPageRule();
2665
+ var name, priority="";
2339
2666
 
2340
- if (opts && opts.globalObject) {
2341
- styleSheet.__globalObject = opts.globalObject;
2342
- }
2667
+ for (var character; (character = ruleText.charAt(i)); i++) {
2343
2668
 
2344
- // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
2345
- var currentScope = styleSheet;
2669
+ switch (character) {
2670
+
2671
+ case " ":
2672
+ case "\t":
2673
+ case "\r":
2674
+ case "\n":
2675
+ case "\f":
2676
+ if (SIGNIFICANT_WHITESPACE[state]) {
2677
+ // Squash 2 or more white-spaces in the row into 1
2678
+ switch (ruleText.charAt(i - 1)) {
2679
+ case " ":
2680
+ case "\t":
2681
+ case "\r":
2682
+ case "\n":
2683
+ case "\f":
2684
+ break;
2685
+ default:
2686
+ buffer += " ";
2687
+ break;
2688
+ }
2689
+ }
2690
+ break;
2691
+
2692
+ // String
2693
+ case '"':
2694
+ j = i + 1;
2695
+ index = ruleText.indexOf('"', j) + 1;
2696
+ if (!index) {
2697
+ throw '" is missing';
2698
+ }
2699
+ buffer += ruleText.slice(i, index);
2700
+ i = index - 1;
2701
+ break;
2702
+
2703
+ case "'":
2704
+ j = i + 1;
2705
+ index = ruleText.indexOf("'", j) + 1;
2706
+ if (!index) {
2707
+ throw "' is missing";
2708
+ }
2709
+ buffer += ruleText.slice(i, index);
2710
+ i = index - 1;
2711
+ break;
2712
+
2713
+ // Comment
2714
+ case "/":
2715
+ if (ruleText.charAt(i + 1) === "*") {
2716
+ i += 2;
2717
+ index = ruleText.indexOf("*/", i);
2718
+ if (index === -1) {
2719
+ throw new SyntaxError("Missing */");
2720
+ } else {
2721
+ i = index + 1;
2722
+ }
2723
+ } else {
2724
+ buffer += character;
2725
+ }
2726
+ break;
2727
+
2728
+ case "{":
2729
+ if (state === "selector") {
2730
+ pageRule.selectorText = buffer.trim();
2731
+ buffer = "";
2732
+ state = "name";
2733
+ }
2734
+ break;
2735
+
2736
+ case ":":
2737
+ if (state === "name") {
2738
+ name = buffer.trim();
2739
+ buffer = "";
2740
+ state = "value";
2741
+ } else {
2742
+ buffer += character;
2743
+ }
2744
+ break;
2745
+
2746
+ case "!":
2747
+ if (state === "value" && ruleText.indexOf("!important", i) === i) {
2748
+ priority = "important";
2749
+ i += "important".length;
2750
+ } else {
2751
+ buffer += character;
2752
+ }
2753
+ break;
2754
+
2755
+ case ";":
2756
+ if (state === "value") {
2757
+ pageRule.style.setProperty(name, buffer.trim(), priority);
2758
+ priority = "";
2759
+ buffer = "";
2760
+ state = "name";
2761
+ } else {
2762
+ buffer += character;
2763
+ }
2764
+ break;
2765
+
2766
+ case "}":
2767
+ if (state === "value") {
2768
+ pageRule.style.setProperty(name, buffer.trim(), priority);
2769
+ priority = "";
2770
+ buffer = "";
2771
+ } else if (state === "name") {
2772
+ break;
2773
+ } else {
2774
+ buffer += character;
2775
+ }
2776
+ state = "selector";
2777
+ break;
2778
+
2779
+ default:
2780
+ buffer += character;
2781
+ break;
2782
+
2783
+ }
2784
+ }
2785
+
2786
+ return pageRule;
2787
+
2788
+ };
2789
+
2790
+
2791
+
2792
+
2793
+
2794
+ /**
2795
+ * Parses a CSS string and returns a CSSOM.CSSStyleSheet object representing the parsed stylesheet.
2796
+ *
2797
+ * @param {string} token - The CSS string to parse.
2798
+ * @param {object} [opts] - Optional parsing options.
2799
+ * @param {object} [opts.globalObject] - An optional global object to attach to the stylesheet. Useful on jsdom webplatform tests.
2800
+ * @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`.
2801
+ * @returns {CSSOM.CSSStyleSheet} The parsed CSSStyleSheet object.
2802
+ */
2803
+ CSSOM.parse = function parse(token, opts, errorHandler) {
2804
+ errorHandler = errorHandler === true ? (console && console.error) : errorHandler;
2805
+
2806
+ var i = 0;
2807
+
2808
+ /**
2809
+ "before-selector" or
2810
+ "selector" or
2811
+ "atRule" or
2812
+ "atBlock" or
2813
+ "conditionBlock" or
2814
+ "before-name" or
2815
+ "name" or
2816
+ "before-value" or
2817
+ "value"
2818
+ */
2819
+ var state = "before-selector";
2820
+
2821
+ var index;
2822
+ var buffer = "";
2823
+ var valueParenthesisDepth = 0;
2824
+
2825
+ var SIGNIFICANT_WHITESPACE = {
2826
+ "name": true,
2827
+ "before-name": true,
2828
+ "selector": true,
2829
+ "value": true,
2830
+ "value-parenthesis": true,
2831
+ "atRule": true,
2832
+ "importRule-begin": true,
2833
+ "importRule": true,
2834
+ "namespaceRule-begin": true,
2835
+ "namespaceRule": true,
2836
+ "atBlock": true,
2837
+ "containerBlock": true,
2838
+ "conditionBlock": true,
2839
+ "counterStyleBlock": true,
2840
+ 'documentRule-begin': true,
2841
+ "scopeBlock": true,
2842
+ "layerBlock": true,
2843
+ "pageBlock": true
2844
+ };
2845
+
2846
+ var styleSheet = new CSSOM.CSSStyleSheet();
2847
+
2848
+ if (opts && opts.globalObject) {
2849
+ styleSheet.__globalObject = opts.globalObject;
2850
+ }
2851
+
2852
+ // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
2853
+ var currentScope = styleSheet;
2346
2854
 
2347
2855
  // @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
2348
2856
  var parentRule;
@@ -2350,7 +2858,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2350
2858
  var ancestorRules = [];
2351
2859
  var prevScope;
2352
2860
 
2353
- var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
2861
+ var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
2354
2862
 
2355
2863
  // Track defined namespace prefixes for validation
2356
2864
  var definedNamespacePrefixes = {};
@@ -2360,11 +2868,13 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2360
2868
  // var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
2361
2869
  var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
2362
2870
  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
2363
- var forwardRuleValidationRegExp = /(?:\(|\s|\/\*)/; // Match that the rule is followed by any whitespace, a opening comment or a condition opening parenthesis
2871
+ var forwardRuleValidationRegExp = /(?:\s|\/\*|\{|\()/; // Match that the rule is followed by any whitespace, a opening comment, a condition opening parenthesis or a opening brace
2364
2872
  var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
2365
2873
  var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
2366
2874
  var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
2367
- var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
2875
+ var cssCustomIdentifierRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a css custom identifier
2876
+ var startsWithCombinatorRegExp = /^\s*[>+~]/; // Checks if a selector starts with a CSS combinator (>, +, ~)
2877
+ var atPageRuleSelectorRegExp = /^([^\s:]+)?((?::\w+)*)$/;
2368
2878
 
2369
2879
  /**
2370
2880
  * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
@@ -2456,24 +2966,214 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2456
2966
  return i;
2457
2967
  }
2458
2968
 
2459
- var parseError = function(message) {
2460
- var lines = token.substring(0, i).split('\n');
2461
- var lineCount = lines.length;
2462
- var charCount = lines.pop().length + 1;
2463
- var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
2464
- error.line = lineCount;
2465
- /* jshint sub : true */
2466
- error['char'] = charCount;
2467
- error.styleSheet = styleSheet;
2468
- // Print the error but continue parsing the sheet
2469
- try {
2470
- throw error;
2471
- } catch(e) {
2472
- errorHandler && errorHandler(e);
2969
+ /**
2970
+ * Parses the scope prelude and extracts start and end selectors.
2971
+ * @param {string} preludeContent - The scope prelude content (without @scope keyword)
2972
+ * @returns {object} Object with startSelector and endSelector properties
2973
+ */
2974
+ function parseScopePrelude(preludeContent) {
2975
+ var parts = preludeContent.split(/\s*\)\s*to\s+\(/);
2976
+
2977
+ // Restore the parentheses that were consumed by the split
2978
+ if (parts.length === 2) {
2979
+ parts[0] = parts[0] + ')';
2980
+ parts[1] = '(' + parts[1];
2473
2981
  }
2982
+
2983
+ var hasStart = parts[0] &&
2984
+ parts[0].charAt(0) === '(' &&
2985
+ parts[0].charAt(parts[0].length - 1) === ')';
2986
+ var hasEnd = parts[1] &&
2987
+ parts[1].charAt(0) === '(' &&
2988
+ parts[1].charAt(parts[1].length - 1) === ')';
2989
+
2990
+ // Handle case: @scope to (<end>)
2991
+ var hasOnlyEnd = !hasStart &&
2992
+ !hasEnd &&
2993
+ parts[0].indexOf('to (') === 0 &&
2994
+ parts[0].charAt(parts[0].length - 1) === ')';
2995
+
2996
+ var startSelector = '';
2997
+ var endSelector = '';
2998
+
2999
+ if (hasStart) {
3000
+ startSelector = parts[0].slice(1, -1).trim();
3001
+ }
3002
+ if (hasEnd) {
3003
+ endSelector = parts[1].slice(1, -1).trim();
3004
+ }
3005
+ if (hasOnlyEnd) {
3006
+ endSelector = parts[0].slice(4, -1).trim();
3007
+ }
3008
+
3009
+ return {
3010
+ startSelector: startSelector,
3011
+ endSelector: endSelector,
3012
+ hasStart: hasStart,
3013
+ hasEnd: hasEnd,
3014
+ hasOnlyEnd: hasOnlyEnd
3015
+ };
2474
3016
  };
2475
3017
 
2476
- var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
3018
+ /**
3019
+ * Checks if a selector contains pseudo-elements.
3020
+ * @param {string} selector - The CSS selector to check
3021
+ * @returns {boolean} True if the selector contains pseudo-elements
3022
+ */
3023
+ function hasPseudoElement(selector) {
3024
+ // Match only double-colon (::) pseudo-elements
3025
+ // Also match legacy single-colon pseudo-elements: :before, :after, :first-line, :first-letter
3026
+ // These must NOT be followed by alphanumeric characters (to avoid matching :before-x or similar)
3027
+ var pseudoElementRegex = /::[a-zA-Z][\w-]*|:(before|after|first-line|first-letter)(?![a-zA-Z0-9_-])/;
3028
+ return pseudoElementRegex.test(selector);
3029
+ };
3030
+
3031
+ /**
3032
+ * Validates balanced parentheses, brackets, and quotes in a selector.
3033
+ *
3034
+ * @param {string} selector - The CSS selector to validate
3035
+ * @param {boolean} trackAttributes - Whether to track attribute selector context
3036
+ * @param {boolean} useStack - Whether to use a stack for parentheses (needed for nested validation)
3037
+ * @returns {boolean} True if the syntax is valid (all brackets, parentheses, and quotes are balanced)
3038
+ */
3039
+ function validateBalancedSyntax(selector, trackAttributes, useStack) {
3040
+ var parenDepth = 0;
3041
+ var bracketDepth = 0;
3042
+ var inSingleQuote = false;
3043
+ var inDoubleQuote = false;
3044
+ var inAttr = false;
3045
+ var stack = useStack ? [] : null;
3046
+
3047
+ for (var i = 0; i < selector.length; i++) {
3048
+ var char = selector[i];
3049
+ var prevChar = i > 0 ? selector[i - 1] : '';
3050
+
3051
+ if (inSingleQuote) {
3052
+ if (char === "'" && prevChar !== "\\") {
3053
+ inSingleQuote = false;
3054
+ }
3055
+ } else if (inDoubleQuote) {
3056
+ if (char === '"' && prevChar !== "\\") {
3057
+ inDoubleQuote = false;
3058
+ }
3059
+ } else if (trackAttributes && inAttr) {
3060
+ if (char === "]") {
3061
+ inAttr = false;
3062
+ } else if (char === "'") {
3063
+ inSingleQuote = true;
3064
+ } else if (char === '"') {
3065
+ inDoubleQuote = true;
3066
+ }
3067
+ } else {
3068
+ if (trackAttributes && char === "[") {
3069
+ inAttr = true;
3070
+ } else if (char === "'") {
3071
+ inSingleQuote = true;
3072
+ } else if (char === '"') {
3073
+ inDoubleQuote = true;
3074
+ } else if (char === '(') {
3075
+ if (useStack) {
3076
+ stack.push("(");
3077
+ } else {
3078
+ parenDepth++;
3079
+ }
3080
+ } else if (char === ')') {
3081
+ if (useStack) {
3082
+ if (!stack.length || stack.pop() !== "(") {
3083
+ return false;
3084
+ }
3085
+ } else {
3086
+ parenDepth--;
3087
+ if (parenDepth < 0) {
3088
+ return false;
3089
+ }
3090
+ }
3091
+ } else if (char === '[') {
3092
+ bracketDepth++;
3093
+ } else if (char === ']') {
3094
+ bracketDepth--;
3095
+ if (bracketDepth < 0) {
3096
+ return false;
3097
+ }
3098
+ }
3099
+ }
3100
+ }
3101
+
3102
+ // Check if everything is balanced
3103
+ if (useStack) {
3104
+ return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
3105
+ } else {
3106
+ return parenDepth === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote;
3107
+ }
3108
+ };
3109
+
3110
+ /**
3111
+ * Checks for basic syntax errors in selectors (mismatched parentheses, brackets, quotes).
3112
+ * @param {string} selector - The CSS selector to check
3113
+ * @returns {boolean} True if there are syntax errors
3114
+ */
3115
+ function hasBasicSyntaxError(selector) {
3116
+ return !validateBalancedSyntax(selector, false, false);
3117
+ };
3118
+
3119
+ /**
3120
+ * Checks for invalid combinator patterns in selectors.
3121
+ * @param {string} selector - The CSS selector to check
3122
+ * @returns {boolean} True if the selector contains invalid combinators
3123
+ */
3124
+ function hasInvalidCombinators(selector) {
3125
+ // Check for invalid combinator patterns:
3126
+ // - <> (not a valid combinator)
3127
+ // - >> (deep descendant combinator, deprecated and invalid)
3128
+ // - Multiple consecutive combinators like >>, >~, etc.
3129
+ if (/<>/.test(selector)) return true;
3130
+ if (/>>/.test(selector)) return true;
3131
+ // Check for other invalid consecutive combinator patterns
3132
+ if (/[>+~]\s*[>+~]/.test(selector)) return true;
3133
+ return false;
3134
+ };
3135
+
3136
+ /**
3137
+ * Checks for invalid pseudo-like syntax (function calls without proper pseudo prefix).
3138
+ * @param {string} selector - The CSS selector to check
3139
+ * @returns {boolean} True if the selector contains invalid pseudo-like syntax
3140
+ */
3141
+ function hasInvalidPseudoSyntax(selector) {
3142
+ // Check for specific known pseudo-elements used without : or :: prefix
3143
+ // Examples: slotted(div), part(name), cue(selector)
3144
+ // These are ONLY valid as ::slotted(), ::part(), ::cue()
3145
+ var invalidPatterns = [
3146
+ /(?:^|[\s>+~,\[])slotted\s*\(/i,
3147
+ /(?:^|[\s>+~,\[])part\s*\(/i,
3148
+ /(?:^|[\s>+~,\[])cue\s*\(/i,
3149
+ /(?:^|[\s>+~,\[])cue-region\s*\(/i
3150
+ ];
3151
+
3152
+ for (var i = 0; i < invalidPatterns.length; i++) {
3153
+ if (invalidPatterns[i].test(selector)) {
3154
+ return true;
3155
+ }
3156
+ }
3157
+ return false;
3158
+ };
3159
+
3160
+ /**
3161
+ * Checks for invalid nesting selector (&) usage.
3162
+ * The & selector cannot be directly followed by a type selector without a delimiter.
3163
+ * Valid: &.class, &#id, &[attr], &:hover, &::before, & div, &>div
3164
+ * Invalid: &div, &span
3165
+ * @param {string} selector - The CSS selector to check
3166
+ * @returns {boolean} True if the selector contains invalid & usage
3167
+ */
3168
+ function hasInvalidNestingSelector(selector) {
3169
+ // Check for & followed directly by a letter (type selector) without any delimiter
3170
+ // This regex matches & followed by a letter (start of type selector) that's not preceded by an escape
3171
+ // We need to exclude valid cases like &.class, &#id, &[attr], &:pseudo, &::pseudo, & (with space), &>
3172
+ var invalidNestingPattern = /&(?![.\#\[:>\+~\s])[a-zA-Z]/;
3173
+ return invalidNestingPattern.test(selector);
3174
+ };
3175
+
3176
+ function validateAtRule(atRuleKey, validCallback, cannotBeNested) {
2477
3177
  var isValid = false;
2478
3178
  var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
2479
3179
  var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
@@ -2494,6 +3194,114 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2494
3194
  isValid = true;
2495
3195
  }
2496
3196
  }
3197
+
3198
+ // Additional validation for @scope rule
3199
+ if (isValid && atRuleKey === "@scope") {
3200
+ var openBraceIndex = ruleSlice.indexOf('{');
3201
+ if (openBraceIndex !== -1) {
3202
+ // Extract the rule prelude (everything between the at-rule and {)
3203
+ var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
3204
+
3205
+ // Skip past at-rule keyword and whitespace
3206
+ var preludeContent = rulePrelude.slice("@scope".length).trim();
3207
+
3208
+ if (preludeContent.length > 0) {
3209
+ // Parse the scope prelude
3210
+ var parsedScopePrelude = parseScopePrelude(preludeContent);
3211
+ var startSelector = parsedScopePrelude.startSelector;
3212
+ var endSelector = parsedScopePrelude.endSelector;
3213
+ var hasStart = parsedScopePrelude.hasStart;
3214
+ var hasEnd = parsedScopePrelude.hasEnd;
3215
+ var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
3216
+
3217
+ // Validation rules for @scope:
3218
+ // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
3219
+ if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
3220
+ isValid = false;
3221
+ }
3222
+ // 2. Pseudo-elements are invalid in scope selectors
3223
+ else if ((startSelector && hasPseudoElement(startSelector)) || (endSelector && hasPseudoElement(endSelector))) {
3224
+ isValid = false;
3225
+ }
3226
+ // 3. Basic syntax errors (mismatched parens, brackets, quotes)
3227
+ else if ((startSelector && hasBasicSyntaxError(startSelector)) || (endSelector && hasBasicSyntaxError(endSelector))) {
3228
+ isValid = false;
3229
+ }
3230
+ // 4. Invalid combinator patterns
3231
+ else if ((startSelector && hasInvalidCombinators(startSelector)) || (endSelector && hasInvalidCombinators(endSelector))) {
3232
+ isValid = false;
3233
+ }
3234
+ // 5. Invalid pseudo-like syntax (function without : or :: prefix)
3235
+ else if ((startSelector && hasInvalidPseudoSyntax(startSelector)) || (endSelector && hasInvalidPseudoSyntax(endSelector))) {
3236
+ isValid = false;
3237
+ }
3238
+ // 6. Invalid structure (no proper parentheses found when prelude is not empty)
3239
+ else if (!hasStart && !hasOnlyEnd) {
3240
+ isValid = false;
3241
+ }
3242
+ }
3243
+ // Empty prelude (@scope {}) is valid
3244
+ }
3245
+ }
3246
+
3247
+ if (isValid && atRuleKey === "@page") {
3248
+ var openBraceIndex = ruleSlice.indexOf('{');
3249
+ if (openBraceIndex !== -1) {
3250
+ // Extract the rule prelude (everything between the at-rule and {)
3251
+ var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
3252
+
3253
+ // Skip past at-rule keyword and whitespace
3254
+ var preludeContent = rulePrelude.slice("@page".length).trim();
3255
+
3256
+ if (preludeContent.length > 0) {
3257
+ var trimmedValue = preludeContent.trim();
3258
+
3259
+ // Empty selector is valid for @page
3260
+ if (trimmedValue !== '') {
3261
+ // Parse @page selectorText for page name and pseudo-pages
3262
+ // Valid formats:
3263
+ // - (empty - no name, no pseudo-page)
3264
+ // - :left, :right, :first, :blank (pseudo-page only)
3265
+ // - named (named page only)
3266
+ // - named:first (named page with single pseudo-page)
3267
+ // - named:first:left (named page with multiple pseudo-pages)
3268
+ var match = trimmedValue.match(atPageRuleSelectorRegExp);
3269
+ if (match) {
3270
+ var pageName = match[1] || '';
3271
+ var pseudoPages = match[2] || '';
3272
+
3273
+ // Validate page name if present
3274
+ if (pageName) {
3275
+ if (!cssCustomIdentifierRegExp.test(pageName)) {
3276
+ isValid = false;
3277
+ }
3278
+ }
3279
+
3280
+ // Validate pseudo-pages if present
3281
+ if (pseudoPages) {
3282
+ var pseudos = pseudoPages.split(':').filter(function(p) { return p; });
3283
+ var validPseudos = ['left', 'right', 'first', 'blank'];
3284
+ var allValid = true;
3285
+ for (var j = 0; j < pseudos.length; j++) {
3286
+ if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) {
3287
+ allValid = false;
3288
+ break;
3289
+ }
3290
+ }
3291
+
3292
+ if (!allValid) {
3293
+ isValid = false;
3294
+ }
3295
+ }
3296
+ } else {
3297
+ isValid = false;
3298
+ }
3299
+ }
3300
+
3301
+ }
3302
+ }
3303
+ }
3304
+
2497
3305
  if (!isValid) {
2498
3306
  // If it's invalid the browser will simply ignore the entire invalid block
2499
3307
  // Use regex to find the closing brace of the invalid rule
@@ -2552,55 +3360,47 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2552
3360
  * @returns {boolean}
2553
3361
  */
2554
3362
  function basicSelectorValidator(selector) {
2555
- var length = selector.length;
2556
- var i = 0;
2557
- var stack = [];
2558
- var inAttr = false;
2559
- var inSingleQuote = false;
2560
- var inDoubleQuote = false;
3363
+ // Validate balanced syntax with attribute tracking and stack-based parentheses matching
3364
+ if (!validateBalancedSyntax(selector, true, true)) {
3365
+ return false;
3366
+ }
2561
3367
 
2562
- while (i < length) {
2563
- var char = selector[i];
3368
+ // Check for invalid combinator patterns
3369
+ if (hasInvalidCombinators(selector)) {
3370
+ return false;
3371
+ }
2564
3372
 
2565
- if (inSingleQuote) {
2566
- if (char === "'" && selector[i - 1] !== "\\") {
2567
- inSingleQuote = false;
2568
- }
2569
- } else if (inDoubleQuote) {
2570
- if (char === '"' && selector[i - 1] !== "\\") {
2571
- inDoubleQuote = false;
2572
- }
2573
- } else if (inAttr) {
2574
- if (char === "]") {
2575
- inAttr = false;
2576
- } else if (char === "'") {
2577
- inSingleQuote = true;
2578
- } else if (char === '"') {
2579
- inDoubleQuote = true;
2580
- }
2581
- } else {
2582
- if (char === "[") {
2583
- inAttr = true;
2584
- } else if (char === "'") {
2585
- inSingleQuote = true;
2586
- } else if (char === '"') {
2587
- inDoubleQuote = true;
2588
- } else if (char === "(") {
2589
- stack.push("(");
2590
- } else if (char === ")") {
2591
- if (!stack.length || stack.pop() !== "(") {
2592
- return false;
2593
- }
2594
- }
2595
- }
2596
- i++;
3373
+ // Check for invalid pseudo-like syntax
3374
+ if (hasInvalidPseudoSyntax(selector)) {
3375
+ return false;
2597
3376
  }
2598
3377
 
2599
- // If any stack or quote/attr context remains, it's invalid
2600
- if (stack.length || inAttr || inSingleQuote || inDoubleQuote) {
3378
+ // Check for invalid nesting selector (&) usage
3379
+ if (hasInvalidNestingSelector(selector)) {
2601
3380
  return false;
2602
3381
  }
2603
3382
 
3383
+ // Check for invalid pseudo-class usage with quoted strings
3384
+ // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
3385
+ var pseudoPattern = /::?([a-zA-Z][\w-]*)\(([^)]+)\)/g;
3386
+ var pseudoMatch;
3387
+ while ((pseudoMatch = pseudoPattern.exec(selector)) !== null) {
3388
+ var pseudoName = pseudoMatch[1];
3389
+ var pseudoContent = pseudoMatch[2];
3390
+
3391
+ // List of pseudo-classes that should not accept quoted strings
3392
+ // :lang() - accepts language codes: en, fr-CA
3393
+ // :dir() - accepts direction: ltr, rtl
3394
+ // :nth-*() - accepts An+B notation: 2n+1, odd, even
3395
+ var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
3396
+
3397
+ for (var i = 0; i < noQuotesPseudos.length; i++) {
3398
+ if (pseudoName === noQuotesPseudos[i] && /['"]/.test(pseudoContent)) {
3399
+ return false;
3400
+ }
3401
+ }
3402
+ }
3403
+
2604
3404
  // Fallback to a loose regexp for the overall selector structure (without deep paren matching)
2605
3405
  // This is similar to the original, but without nested paren limitations
2606
3406
  // Modified to support namespace selectors: *|element, prefix|element, |element
@@ -2608,7 +3408,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2608
3408
  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+)+$/;
2609
3409
  return looseSelectorRegExp.test(selector);
2610
3410
  }
2611
-
3411
+
2612
3412
  /**
2613
3413
  * Regular expression to match CSS pseudo-classes with arguments.
2614
3414
  *
@@ -2639,48 +3439,56 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2639
3439
  * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
2640
3440
  */
2641
3441
  function parseAndSplitNestedSelectors(selector) {
2642
- var depth = 0;
2643
- var buffer = "";
2644
- var parts = [];
2645
- var inSingleQuote = false;
2646
- var inDoubleQuote = false;
3442
+ var depth = 0; // Track parenthesis nesting depth
3443
+ var buffer = ""; // Accumulate characters for current selector part
3444
+ var parts = []; // Array of split selector parts
3445
+ var inSingleQuote = false; // Track if we're inside single quotes
3446
+ var inDoubleQuote = false; // Track if we're inside double quotes
2647
3447
  var i, char;
2648
3448
 
2649
3449
  for (i = 0; i < selector.length; i++) {
2650
3450
  char = selector.charAt(i);
2651
3451
 
3452
+ // Handle single quote strings
2652
3453
  if (char === "'" && !inDoubleQuote) {
2653
3454
  inSingleQuote = !inSingleQuote;
2654
3455
  buffer += char;
2655
- } else if (char === '"' && !inSingleQuote) {
3456
+ }
3457
+ // Handle double quote strings
3458
+ else if (char === '"' && !inSingleQuote) {
2656
3459
  inDoubleQuote = !inDoubleQuote;
2657
3460
  buffer += char;
2658
- } else if (!inSingleQuote && !inDoubleQuote) {
3461
+ }
3462
+ // Process characters outside of quoted strings
3463
+ else if (!inSingleQuote && !inDoubleQuote) {
2659
3464
  if (char === '(') {
3465
+ // Entering a nested level (e.g., :is(...))
2660
3466
  depth++;
2661
3467
  buffer += char;
2662
3468
  } else if (char === ')') {
3469
+ // Exiting a nested level
2663
3470
  depth--;
2664
3471
  buffer += char;
2665
- if (depth === 0) {
2666
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
2667
- buffer = "";
2668
- }
2669
3472
  } else if (char === ',' && depth === 0) {
2670
- if (buffer.replace(/^\s+|\s+$/g, "")) {
2671
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
3473
+ // Found a top-level comma separator - split here
3474
+ if (buffer.trim()) {
3475
+ parts.push(buffer.trim());
2672
3476
  }
2673
3477
  buffer = "";
2674
3478
  } else {
3479
+ // Regular character - add to buffer
2675
3480
  buffer += char;
2676
3481
  }
2677
- } else {
3482
+ }
3483
+ // Characters inside quoted strings - add to buffer
3484
+ else {
2678
3485
  buffer += char;
2679
3486
  }
2680
3487
  }
2681
3488
 
2682
- if (buffer.replace(/^\s+|\s+$/g, "")) {
2683
- parts.push(buffer.replace(/^\s+|\s+$/g, ""));
3489
+ // Add any remaining content in buffer as the last part
3490
+ if (buffer.trim()) {
3491
+ parts.push(buffer.trim());
2684
3492
  }
2685
3493
 
2686
3494
  return parts;
@@ -2697,8 +3505,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2697
3505
  * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
2698
3506
  */
2699
3507
 
2700
- // Cache to store validated selectors
2701
- var validatedSelectorsCache = new Map();
3508
+ // Cache to store validated selectors (previously a ES6 Map, now an ES5-compliant object)
3509
+ var validatedSelectorsCache = {};
2702
3510
 
2703
3511
  // Only pseudo-classes that accept selector lists should recurse
2704
3512
  var selectorListPseudoClasses = {
@@ -2709,8 +3517,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2709
3517
  };
2710
3518
 
2711
3519
  function validateSelector(selector) {
2712
- if (validatedSelectorsCache.has(selector)) {
2713
- return validatedSelectorsCache.get(selector);
3520
+ if (validatedSelectorsCache.hasOwnProperty(selector)) {
3521
+ return validatedSelectorsCache[selector];
2714
3522
  }
2715
3523
 
2716
3524
  // Use a non-global regex to find all pseudo-classes with arguments
@@ -2727,15 +3535,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2727
3535
  var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
2728
3536
  for (var i = 0; i < nestedSelectors.length; i++) {
2729
3537
  var nestedSelector = nestedSelectors[i];
2730
- if (!validatedSelectorsCache.has(nestedSelector)) {
3538
+ if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
2731
3539
  var nestedSelectorValidation = validateSelector(nestedSelector);
2732
- validatedSelectorsCache.set(nestedSelector, nestedSelectorValidation);
3540
+ validatedSelectorsCache[nestedSelector] = nestedSelectorValidation;
2733
3541
  if (!nestedSelectorValidation) {
2734
- validatedSelectorsCache.set(selector, false);
3542
+ validatedSelectorsCache[selector] = false;
2735
3543
  return false;
2736
3544
  }
2737
- } else if (!validatedSelectorsCache.get(nestedSelector)) {
2738
- validatedSelectorsCache.set(selector, false);
3545
+ } else if (!validatedSelectorsCache[nestedSelector]) {
3546
+ validatedSelectorsCache[selector] = false;
2739
3547
  return false;
2740
3548
  }
2741
3549
  }
@@ -2743,7 +3551,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2743
3551
  }
2744
3552
 
2745
3553
  var basicSelectorValidation = basicSelectorValidator(selector);
2746
- validatedSelectorsCache.set(selector, basicSelectorValidation);
3554
+ validatedSelectorsCache[selector] = basicSelectorValidation;
2747
3555
 
2748
3556
  return basicSelectorValidation;
2749
3557
  }
@@ -2807,6 +3615,28 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2807
3615
  return definedNamespacePrefixes.hasOwnProperty(namespacePrefix);
2808
3616
  }
2809
3617
 
3618
+ /**
3619
+ * Processes a CSS selector text
3620
+ *
3621
+ * @param {string} selectorText - The CSS selector text to process
3622
+ * @returns {string} The processed selector text with normalized whitespace
3623
+ */
3624
+ function processSelectorText(selectorText) {
3625
+ // TODO: Remove invalid selectors that appears inside pseudo classes
3626
+ // TODO: The same processing here needs to be reused in CSSStyleRule.selectorText setter
3627
+ // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
3628
+
3629
+ /**
3630
+ * Normalizes whitespace and preserving quoted strings.
3631
+ * Replaces all newline characters (CRLF, CR, or LF) with spaces while keeping quoted
3632
+ * strings (single or double quotes) intact, including any escaped characters within them.
3633
+ */
3634
+ return selectorText.replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
3635
+ if (newline) return " ";
3636
+ return match;
3637
+ });
3638
+ }
3639
+
2810
3640
  /**
2811
3641
  * Checks if a given CSS selector text is valid by splitting it by commas
2812
3642
  * and validating each individual selector using the `validateSelector` function.
@@ -2815,6 +3645,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2815
3645
  * @returns {boolean} Returns true if all selectors are valid, otherwise false.
2816
3646
  */
2817
3647
  function isValidSelectorText(selectorText) {
3648
+ // TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter
3649
+ // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
3650
+
2818
3651
  // Check for newlines inside single or double quotes using regex
2819
3652
  // This matches any quoted string (single or double) containing a newline
2820
3653
  var quotedNewlineRegExp = /(['"])(?:\\.|[^\\])*?\1/g;
@@ -2835,6 +3668,23 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2835
3668
  return true;
2836
3669
  }
2837
3670
 
3671
+ function parseError(message) {
3672
+ var lines = token.substring(0, i).split('\n');
3673
+ var lineCount = lines.length;
3674
+ var charCount = lines.pop().length + 1;
3675
+ var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
3676
+ error.line = lineCount;
3677
+ /* jshint sub : true */
3678
+ error['char'] = charCount;
3679
+ error.styleSheet = styleSheet;
3680
+ // Print the error but continue parsing the sheet
3681
+ try {
3682
+ throw error;
3683
+ } catch(e) {
3684
+ errorHandler && errorHandler(e);
3685
+ }
3686
+ };
3687
+
2838
3688
  var endingIndex = token.length - 1;
2839
3689
 
2840
3690
  for (var character; (character = token.charAt(i)); i++) {
@@ -2924,7 +3774,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2924
3774
  i += 2;
2925
3775
  index = token.indexOf("*/", i);
2926
3776
  if (index === -1) {
2927
- parseError("Missing */");
3777
+ i = token.length - 1;
3778
+ buffer = "";
2928
3779
  } else {
2929
3780
  i = index + 1;
2930
3781
  }
@@ -2979,6 +3830,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2979
3830
  }, true);
2980
3831
  buffer = "";
2981
3832
  break;
3833
+ } else if (token.indexOf("@scope", i) === i) {
3834
+ validateAtRule("@scope", function(){
3835
+ state = "scopeBlock";
3836
+ scopeRule = new CSSOM.CSSScopeRule();
3837
+ scopeRule.__starts = i;
3838
+ i += "scope".length;
3839
+ });
3840
+ buffer = "";
3841
+ break;
2982
3842
  } else if (token.indexOf("@layer", i) === i) {
2983
3843
  validateAtRule("@layer", function(){
2984
3844
  state = "layerBlock"
@@ -2988,6 +3848,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
2988
3848
  });
2989
3849
  buffer = "";
2990
3850
  break;
3851
+ } else if (token.indexOf("@page", i) === i) {
3852
+ validateAtRule("@page", function(){
3853
+ state = "pageBlock"
3854
+ pageRule = new CSSOM.CSSPageRule();
3855
+ pageRule.__starts = i;
3856
+ i += "page".length;
3857
+ });
3858
+ buffer = "";
3859
+ break;
2991
3860
  } else if (token.indexOf("@supports", i) === i) {
2992
3861
  validateAtRule("@supports", function(){
2993
3862
  state = "conditionBlock";
@@ -3085,10 +3954,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3085
3954
  }
3086
3955
 
3087
3956
  currentScope = parentRule = styleRule;
3088
- styleRule.selectorText = buffer.trim().replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
3089
- if (newline) return " ";
3090
- return match;
3091
- });
3957
+ styleRule.selectorText = processSelectorText(buffer.trim());
3092
3958
  styleRule.style.__starts = i;
3093
3959
  styleRule.__parentStyleSheet = styleSheet;
3094
3960
  buffer = "";
@@ -3106,7 +3972,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3106
3972
  buffer = "";
3107
3973
  state = "before-selector";
3108
3974
  } else if (state === "containerBlock") {
3109
- containerRule.containerText = buffer.trim();
3975
+ containerRule.__conditionText = buffer.trim();
3110
3976
 
3111
3977
  if (parentRule) {
3112
3978
  containerRule.__parentRule = parentRule;
@@ -3123,7 +3989,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3123
3989
  counterStyleRule.__parentStyleSheet = styleSheet;
3124
3990
  buffer = "";
3125
3991
  } else if (state === "conditionBlock") {
3126
- supportsRule.conditionText = buffer.trim();
3992
+ supportsRule.__conditionText = buffer.trim();
3127
3993
 
3128
3994
  if (parentRule) {
3129
3995
  supportsRule.__parentRule = parentRule;
@@ -3134,10 +4000,31 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3134
4000
  supportsRule.__parentStyleSheet = styleSheet;
3135
4001
  buffer = "";
3136
4002
  state = "before-selector";
4003
+ } else if (state === "scopeBlock") {
4004
+ var parsedScopePrelude = parseScopePrelude(buffer.trim());
4005
+
4006
+ if (parsedScopePrelude.hasStart) {
4007
+ scopeRule.__start = parsedScopePrelude.startSelector;
4008
+ }
4009
+ if (parsedScopePrelude.hasEnd) {
4010
+ scopeRule.__end = parsedScopePrelude.endSelector;
4011
+ }
4012
+ if (parsedScopePrelude.hasOnlyEnd) {
4013
+ scopeRule.__end = parsedScopePrelude.endSelector;
4014
+ }
4015
+
4016
+ if (parentRule) {
4017
+ scopeRule.__parentRule = parentRule;
4018
+ ancestorRules.push(parentRule);
4019
+ }
4020
+ currentScope = parentRule = scopeRule;
4021
+ scopeRule.__parentStyleSheet = styleSheet;
4022
+ buffer = "";
4023
+ state = "before-selector";
3137
4024
  } else if (state === "layerBlock") {
3138
4025
  layerBlockRule.name = buffer.trim();
3139
4026
 
3140
- var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(layerRuleNameRegExp) !== null;
4027
+ var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
3141
4028
 
3142
4029
  if (isValidName) {
3143
4030
  if (parentRule) {
@@ -3150,6 +4037,19 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3150
4037
  }
3151
4038
  buffer = "";
3152
4039
  state = "before-selector";
4040
+ } else if (state === "pageBlock") {
4041
+ pageRule.selectorText = buffer.trim();
4042
+
4043
+ if (parentRule) {
4044
+ pageRule.__parentRule = parentRule;
4045
+ ancestorRules.push(parentRule);
4046
+ }
4047
+
4048
+ currentScope = parentRule = pageRule;
4049
+ pageRule.__parentStyleSheet = styleSheet;
4050
+ styleRule = pageRule;
4051
+ buffer = "";
4052
+ state = "before-name";
3153
4053
  } else if (state === "hostRule-begin") {
3154
4054
  if (parentRule) {
3155
4055
  ancestorRules.push(parentRule);
@@ -3223,17 +4123,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3223
4123
  }
3224
4124
 
3225
4125
  styleRule = new CSSOM.CSSStyleRule();
3226
- var processedSelectorText = buffer.trim().replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
3227
- if (newline) return " ";
3228
- return match;
3229
- });
4126
+ var processedSelectorText = processSelectorText(buffer.trim());
3230
4127
  // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
3231
4128
  if (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null) {
3232
4129
  styleRule.selectorText = processedSelectorText;
3233
4130
  } else {
3234
4131
  styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) {
3235
- return sel.indexOf('&') === -1 ? '& ' + sel : sel;
3236
- }).join(', ');
4132
+ // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
4133
+ return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
4134
+ }).join(', ');
3237
4135
  }
3238
4136
  styleRule.style.__starts = i - buffer.length;
3239
4137
  styleRule.__parentRule = parentRule;
@@ -3349,7 +4247,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3349
4247
  testNamespaceRule.cssText = buffer + character;
3350
4248
 
3351
4249
  namespaceRule = testNamespaceRule;
3352
- namespaceRule.__parentStyleSheet = namespaceRule.styleSheet.__parentStyleSheet = styleSheet;
4250
+ namespaceRule.__parentStyleSheet = styleSheet;
3353
4251
  styleSheet.cssRules.push(namespaceRule);
3354
4252
 
3355
4253
  // Track the namespace prefix for validation
@@ -3368,7 +4266,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3368
4266
  return name.trim();
3369
4267
  });
3370
4268
  var isInvalid = parentRule !== undefined || nameListStr.some(function (name) {
3371
- return name.trim().match(layerRuleNameRegExp) === null;
4269
+ return name.trim().match(cssCustomIdentifierRegExp) === null;
3372
4270
  });
3373
4271
 
3374
4272
  if (!isInvalid) {
@@ -3469,6 +4367,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3469
4367
  || parentRule.constructor.name === "CSSMediaRule"
3470
4368
  || parentRule.constructor.name === "CSSSupportsRule"
3471
4369
  || parentRule.constructor.name === "CSSContainerRule"
4370
+ || parentRule.constructor.name === "CSSScopeRule"
3472
4371
  || parentRule.constructor.name === "CSSLayerBlockRule"
3473
4372
  || parentRule.constructor.name === "CSSStartingStyleRule"
3474
4373
  ) {
@@ -3538,6 +4437,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3538
4437
  || parentRule.constructor.name === "CSSMediaRule"
3539
4438
  || parentRule.constructor.name === "CSSSupportsRule"
3540
4439
  || parentRule.constructor.name === "CSSContainerRule"
4440
+ || parentRule.constructor.name === "CSSScopeRule"
3541
4441
  || parentRule.constructor.name === "CSSLayerBlockRule"
3542
4442
  || parentRule.constructor.name === "CSSStartingStyleRule"
3543
4443
  ) {
@@ -3593,6 +4493,10 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
3593
4493
  }
3594
4494
  }
3595
4495
 
4496
+ if (buffer.trim() !== "") {
4497
+ parseError("Unexpected end of input");
4498
+ }
4499
+
3596
4500
  return styleSheet;
3597
4501
  };
3598
4502