@acemir/cssom 0.9.20 → 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.
@@ -1,5 +1,6 @@
1
1
  //.CommonJS
2
2
  var CSSOM = {
3
+ MediaList: require("./MediaList").MediaList,
3
4
  StyleSheet: require("./StyleSheet").StyleSheet,
4
5
  CSSRuleList: require("./CSSRuleList").CSSRuleList,
5
6
  CSSStyleRule: require("./CSSStyleRule").CSSStyleRule,
@@ -59,7 +60,10 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
59
60
  }
60
61
 
61
62
  var ruleToParse = String(rule);
62
- var parsedSheet = CSSOM.parse(ruleToParse);
63
+ var parseErrors = [];
64
+ var parsedSheet = CSSOM.parse(ruleToParse, undefined, function(err) {
65
+ parseErrors.push(err);
66
+ } );
63
67
  if (parsedSheet.cssRules.length !== 1) {
64
68
  errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
65
69
  }
@@ -127,12 +131,21 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
127
131
  'HierarchyRequestError');
128
132
  }
129
133
 
134
+ // Cannot insert if there are already non-special rules
135
+ if (firstNonImportNamespaceIndex < this.cssRules.length) {
136
+ errorUtils.throwError(this, 'DOMException',
137
+ "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
138
+ 'InvalidStateError');
139
+ }
140
+
130
141
  // Cannot insert after other types of rules
131
142
  if (index > firstNonImportNamespaceIndex) {
132
143
  errorUtils.throwError(this, 'DOMException',
133
144
  "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
134
145
  'HierarchyRequestError');
135
146
  }
147
+
148
+
136
149
  } else if (cssRule.constructor.name === 'CSSLayerStatementRule') {
137
150
  // @layer statement rules can be inserted anywhere before @import and @namespace
138
151
  // No additional restrictions beyond what's already handled
@@ -149,6 +162,10 @@ CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
149
162
  "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
150
163
  'HierarchyRequestError');
151
164
  }
165
+
166
+ if (parseErrors.length !== 0) {
167
+ errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
168
+ }
152
169
  }
153
170
 
154
171
  cssRule.__parentStyleSheet = this;
@@ -188,13 +205,20 @@ CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) {
188
205
  if (index >= this.cssRules.length) {
189
206
  errorUtils.throwIndexError(this, 'deleteRule', this.constructor.name, index, this.cssRules.length);
190
207
  }
191
- if (this.cssRules[index] && this.cssRules[index].constructor.name == "CSSNamespaceRule") {
192
- var shouldContinue = this.cssRules.every(function (rule) {
193
- return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
194
- });
195
- if (!shouldContinue) {
196
- errorUtils.throwError(this, 'DOMException', "Failed to execute 'deleteRule' on '" + this.constructor.name + "': Failed to delete rule.", "InvalidStateError");
208
+ if (this.cssRules[index]) {
209
+ if (this.cssRules[index].constructor.name == "CSSNamespaceRule") {
210
+ var shouldContinue = this.cssRules.every(function (rule) {
211
+ return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
212
+ });
213
+ if (!shouldContinue) {
214
+ errorUtils.throwError(this, 'DOMException', "Failed to execute 'deleteRule' on '" + this.constructor.name + "': Failed to delete rule.", "InvalidStateError");
215
+ }
197
216
  }
217
+ if (this.cssRules[index].constructor.name == "CSSImportRule") {
218
+ this.cssRules[index].styleSheet.__parentStyleSheet = null;
219
+ }
220
+
221
+ this.cssRules[index].__parentStyleSheet = null;
198
222
  }
199
223
  this.cssRules.splice(index, 1);
200
224
  };
package/lib/StyleSheet.js CHANGED
@@ -1,5 +1,7 @@
1
1
  //.CommonJS
2
- var CSSOM = {};
2
+ var CSSOM = {
3
+ MediaList: require("./MediaList").MediaList
4
+ };
3
5
  ///CommonJS
4
6
 
5
7
 
@@ -8,10 +10,23 @@ var CSSOM = {};
8
10
  * @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface
9
11
  */
10
12
  CSSOM.StyleSheet = function StyleSheet() {
13
+ this.__media = new CSSOM.MediaList();
11
14
  this.__parentStyleSheet = null;
12
15
  };
13
16
 
14
17
  Object.defineProperties(CSSOM.StyleSheet.prototype, {
18
+ media: {
19
+ get: function() {
20
+ return this.__media;
21
+ },
22
+ set: function(value) {
23
+ if (typeof value === "string") {
24
+ this.__media.mediaText = value;
25
+ } else {
26
+ this.__media = value;
27
+ }
28
+ }
29
+ },
15
30
  parentStyleSheet: {
16
31
  get: function() {
17
32
  return this.__parentStyleSheet;
package/lib/index.js CHANGED
@@ -33,5 +33,6 @@ exports.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
33
33
  exports.CSSScopeRule = require('./CSSScopeRule').CSSScopeRule;
34
34
  exports.CSSLayerBlockRule = require('./CSSLayerBlockRule').CSSLayerBlockRule;
35
35
  exports.CSSLayerStatementRule = require('./CSSLayerStatementRule').CSSLayerStatementRule;
36
+ exports.CSSPageRule = require('./CSSPageRule').CSSPageRule;
36
37
  exports.parse = require('./parse').parse;
37
38
  exports.clone = require('./clone').clone;
package/lib/parse.js CHANGED
@@ -51,7 +51,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
51
51
  "counterStyleBlock": true,
52
52
  'documentRule-begin': true,
53
53
  "scopeBlock": true,
54
- "layerBlock": true
54
+ "layerBlock": true,
55
+ "pageBlock": true
55
56
  };
56
57
 
57
58
  var styleSheet = new CSSOM.CSSStyleSheet();
@@ -69,7 +70,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
69
70
  var ancestorRules = [];
70
71
  var prevScope;
71
72
 
72
- var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
73
+ var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
73
74
 
74
75
  // Track defined namespace prefixes for validation
75
76
  var definedNamespacePrefixes = {};
@@ -83,8 +84,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
83
84
  var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
84
85
  var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
85
86
  var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
86
- var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
87
+ var cssCustomIdentifierRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a css custom identifier
87
88
  var startsWithCombinatorRegExp = /^\s*[>+~]/; // Checks if a selector starts with a CSS combinator (>, +, ~)
89
+ var atPageRuleSelectorRegExp = /^([^\s:]+)?((?::\w+)*)$/;
88
90
 
89
91
  /**
90
92
  * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
@@ -409,12 +411,12 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
409
411
  if (isValid && atRuleKey === "@scope") {
410
412
  var openBraceIndex = ruleSlice.indexOf('{');
411
413
  if (openBraceIndex !== -1) {
412
- // Extract the scope prelude (everything between @scope and {)
413
- var scopePrelude = ruleSlice.slice(0, openBraceIndex).trim();
414
-
415
- // Skip past '@scope' keyword and whitespace
416
- var preludeContent = scopePrelude.slice(6).trim();
417
-
414
+ // Extract the rule prelude (everything between the at-rule and {)
415
+ var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
416
+
417
+ // Skip past at-rule keyword and whitespace
418
+ var preludeContent = rulePrelude.slice("@scope".length).trim();
419
+
418
420
  if (preludeContent.length > 0) {
419
421
  // Parse the scope prelude
420
422
  var parsedScopePrelude = parseScopePrelude(preludeContent);
@@ -453,6 +455,64 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
453
455
  // Empty prelude (@scope {}) is valid
454
456
  }
455
457
  }
458
+
459
+ if (isValid && atRuleKey === "@page") {
460
+ var openBraceIndex = ruleSlice.indexOf('{');
461
+ if (openBraceIndex !== -1) {
462
+ // Extract the rule prelude (everything between the at-rule and {)
463
+ var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
464
+
465
+ // Skip past at-rule keyword and whitespace
466
+ var preludeContent = rulePrelude.slice("@page".length).trim();
467
+
468
+ if (preludeContent.length > 0) {
469
+ var trimmedValue = preludeContent.trim();
470
+
471
+ // Empty selector is valid for @page
472
+ if (trimmedValue !== '') {
473
+ // Parse @page selectorText for page name and pseudo-pages
474
+ // Valid formats:
475
+ // - (empty - no name, no pseudo-page)
476
+ // - :left, :right, :first, :blank (pseudo-page only)
477
+ // - named (named page only)
478
+ // - named:first (named page with single pseudo-page)
479
+ // - named:first:left (named page with multiple pseudo-pages)
480
+ var match = trimmedValue.match(atPageRuleSelectorRegExp);
481
+ if (match) {
482
+ var pageName = match[1] || '';
483
+ var pseudoPages = match[2] || '';
484
+
485
+ // Validate page name if present
486
+ if (pageName) {
487
+ if (!cssCustomIdentifierRegExp.test(pageName)) {
488
+ isValid = false;
489
+ }
490
+ }
491
+
492
+ // Validate pseudo-pages if present
493
+ if (pseudoPages) {
494
+ var pseudos = pseudoPages.split(':').filter(function(p) { return p; });
495
+ var validPseudos = ['left', 'right', 'first', 'blank'];
496
+ var allValid = true;
497
+ for (var j = 0; j < pseudos.length; j++) {
498
+ if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) {
499
+ allValid = false;
500
+ break;
501
+ }
502
+ }
503
+
504
+ if (!allValid) {
505
+ isValid = false;
506
+ }
507
+ }
508
+ } else {
509
+ isValid = false;
510
+ }
511
+ }
512
+
513
+ }
514
+ }
515
+ }
456
516
 
457
517
  if (!isValid) {
458
518
  // If it's invalid the browser will simply ignore the entire invalid block
@@ -532,6 +592,27 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
532
592
  return false;
533
593
  }
534
594
 
595
+ // Check for invalid pseudo-class usage with quoted strings
596
+ // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
597
+ var pseudoPattern = /::?([a-zA-Z][\w-]*)\(([^)]+)\)/g;
598
+ var pseudoMatch;
599
+ while ((pseudoMatch = pseudoPattern.exec(selector)) !== null) {
600
+ var pseudoName = pseudoMatch[1];
601
+ var pseudoContent = pseudoMatch[2];
602
+
603
+ // List of pseudo-classes that should not accept quoted strings
604
+ // :lang() - accepts language codes: en, fr-CA
605
+ // :dir() - accepts direction: ltr, rtl
606
+ // :nth-*() - accepts An+B notation: 2n+1, odd, even
607
+ var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
608
+
609
+ for (var i = 0; i < noQuotesPseudos.length; i++) {
610
+ if (pseudoName === noQuotesPseudos[i] && /['"]/.test(pseudoContent)) {
611
+ return false;
612
+ }
613
+ }
614
+ }
615
+
535
616
  // Fallback to a loose regexp for the overall selector structure (without deep paren matching)
536
617
  // This is similar to the original, but without nested paren limitations
537
618
  // Modified to support namespace selectors: *|element, prefix|element, |element
@@ -636,7 +717,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
636
717
  * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
637
718
  */
638
719
 
639
- // Cache to store validated selectors (ES5-compliant object)
720
+ // Cache to store validated selectors (previously a ES6 Map, now an ES5-compliant object)
640
721
  var validatedSelectorsCache = {};
641
722
 
642
723
  // Only pseudo-classes that accept selector lists should recurse
@@ -746,6 +827,28 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
746
827
  return definedNamespacePrefixes.hasOwnProperty(namespacePrefix);
747
828
  }
748
829
 
830
+ /**
831
+ * Processes a CSS selector text
832
+ *
833
+ * @param {string} selectorText - The CSS selector text to process
834
+ * @returns {string} The processed selector text with normalized whitespace
835
+ */
836
+ function processSelectorText(selectorText) {
837
+ // TODO: Remove invalid selectors that appears inside pseudo classes
838
+ // TODO: The same processing here needs to be reused in CSSStyleRule.selectorText setter
839
+ // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
840
+
841
+ /**
842
+ * Normalizes whitespace and preserving quoted strings.
843
+ * Replaces all newline characters (CRLF, CR, or LF) with spaces while keeping quoted
844
+ * strings (single or double quotes) intact, including any escaped characters within them.
845
+ */
846
+ return selectorText.replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
847
+ if (newline) return " ";
848
+ return match;
849
+ });
850
+ }
851
+
749
852
  /**
750
853
  * Checks if a given CSS selector text is valid by splitting it by commas
751
854
  * and validating each individual selector using the `validateSelector` function.
@@ -754,6 +857,9 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
754
857
  * @returns {boolean} Returns true if all selectors are valid, otherwise false.
755
858
  */
756
859
  function isValidSelectorText(selectorText) {
860
+ // TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter
861
+ // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
862
+
757
863
  // Check for newlines inside single or double quotes using regex
758
864
  // This matches any quoted string (single or double) containing a newline
759
865
  var quotedNewlineRegExp = /(['"])(?:\\.|[^\\])*?\1/g;
@@ -880,7 +986,8 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
880
986
  i += 2;
881
987
  index = token.indexOf("*/", i);
882
988
  if (index === -1) {
883
- parseError("Missing */");
989
+ i = token.length - 1;
990
+ buffer = "";
884
991
  } else {
885
992
  i = index + 1;
886
993
  }
@@ -953,6 +1060,15 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
953
1060
  });
954
1061
  buffer = "";
955
1062
  break;
1063
+ } else if (token.indexOf("@page", i) === i) {
1064
+ validateAtRule("@page", function(){
1065
+ state = "pageBlock"
1066
+ pageRule = new CSSOM.CSSPageRule();
1067
+ pageRule.__starts = i;
1068
+ i += "page".length;
1069
+ });
1070
+ buffer = "";
1071
+ break;
956
1072
  } else if (token.indexOf("@supports", i) === i) {
957
1073
  validateAtRule("@supports", function(){
958
1074
  state = "conditionBlock";
@@ -1050,10 +1166,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1050
1166
  }
1051
1167
 
1052
1168
  currentScope = parentRule = styleRule;
1053
- styleRule.selectorText = buffer.trim().replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
1054
- if (newline) return " ";
1055
- return match;
1056
- });
1169
+ styleRule.selectorText = processSelectorText(buffer.trim());
1057
1170
  styleRule.style.__starts = i;
1058
1171
  styleRule.__parentStyleSheet = styleSheet;
1059
1172
  buffer = "";
@@ -1071,7 +1184,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1071
1184
  buffer = "";
1072
1185
  state = "before-selector";
1073
1186
  } else if (state === "containerBlock") {
1074
- containerRule.containerText = buffer.trim();
1187
+ containerRule.__conditionText = buffer.trim();
1075
1188
 
1076
1189
  if (parentRule) {
1077
1190
  containerRule.__parentRule = parentRule;
@@ -1088,7 +1201,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1088
1201
  counterStyleRule.__parentStyleSheet = styleSheet;
1089
1202
  buffer = "";
1090
1203
  } else if (state === "conditionBlock") {
1091
- supportsRule.conditionText = buffer.trim();
1204
+ supportsRule.__conditionText = buffer.trim();
1092
1205
 
1093
1206
  if (parentRule) {
1094
1207
  supportsRule.__parentRule = parentRule;
@@ -1123,7 +1236,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1123
1236
  } else if (state === "layerBlock") {
1124
1237
  layerBlockRule.name = buffer.trim();
1125
1238
 
1126
- var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(layerRuleNameRegExp) !== null;
1239
+ var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
1127
1240
 
1128
1241
  if (isValidName) {
1129
1242
  if (parentRule) {
@@ -1136,6 +1249,19 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1136
1249
  }
1137
1250
  buffer = "";
1138
1251
  state = "before-selector";
1252
+ } else if (state === "pageBlock") {
1253
+ pageRule.selectorText = buffer.trim();
1254
+
1255
+ if (parentRule) {
1256
+ pageRule.__parentRule = parentRule;
1257
+ ancestorRules.push(parentRule);
1258
+ }
1259
+
1260
+ currentScope = parentRule = pageRule;
1261
+ pageRule.__parentStyleSheet = styleSheet;
1262
+ styleRule = pageRule;
1263
+ buffer = "";
1264
+ state = "before-name";
1139
1265
  } else if (state === "hostRule-begin") {
1140
1266
  if (parentRule) {
1141
1267
  ancestorRules.push(parentRule);
@@ -1209,10 +1335,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1209
1335
  }
1210
1336
 
1211
1337
  styleRule = new CSSOM.CSSStyleRule();
1212
- var processedSelectorText = buffer.trim().replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) {
1213
- if (newline) return " ";
1214
- return match;
1215
- });
1338
+ var processedSelectorText = processSelectorText(buffer.trim());
1216
1339
  // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
1217
1340
  if (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null) {
1218
1341
  styleRule.selectorText = processedSelectorText;
@@ -1336,7 +1459,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1336
1459
  testNamespaceRule.cssText = buffer + character;
1337
1460
 
1338
1461
  namespaceRule = testNamespaceRule;
1339
- namespaceRule.__parentStyleSheet = namespaceRule.styleSheet.__parentStyleSheet = styleSheet;
1462
+ namespaceRule.__parentStyleSheet = styleSheet;
1340
1463
  styleSheet.cssRules.push(namespaceRule);
1341
1464
 
1342
1465
  // Track the namespace prefix for validation
@@ -1355,7 +1478,7 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1355
1478
  return name.trim();
1356
1479
  });
1357
1480
  var isInvalid = parentRule !== undefined || nameListStr.some(function (name) {
1358
- return name.trim().match(layerRuleNameRegExp) === null;
1481
+ return name.trim().match(cssCustomIdentifierRegExp) === null;
1359
1482
  });
1360
1483
 
1361
1484
  if (!isInvalid) {
@@ -1582,6 +1705,10 @@ CSSOM.parse = function parse(token, opts, errorHandler) {
1582
1705
  }
1583
1706
  }
1584
1707
 
1708
+ if (buffer.trim() !== "") {
1709
+ parseError("Unexpected end of input");
1710
+ }
1711
+
1585
1712
  return styleSheet;
1586
1713
  };
1587
1714
 
@@ -1611,6 +1738,7 @@ CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
1611
1738
  CSSOM.CSSScopeRule = require('./CSSScopeRule').CSSScopeRule;
1612
1739
  CSSOM.CSSLayerBlockRule = require("./CSSLayerBlockRule").CSSLayerBlockRule;
1613
1740
  CSSOM.CSSLayerStatementRule = require("./CSSLayerStatementRule").CSSLayerStatementRule;
1741
+ CSSOM.CSSPageRule = require("./CSSPageRule").CSSPageRule;
1614
1742
  // Use cssstyle if available
1615
1743
  try {
1616
1744
  CSSOM.CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration;
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "parser",
8
8
  "styleSheet"
9
9
  ],
10
- "version": "0.9.20",
10
+ "version": "0.9.21",
11
11
  "author": "Nikita Vasilyev <me@elv1s.ru>",
12
12
  "contributors": [
13
13
  "Acemir Sousa Mendes <acemirsm@gmail.com>"