@acemir/cssom
Version:
CSS Object Model implementation and CSS parser
1,888 lines (1,620 loc) • 78.8 kB
JavaScript
var CSSOM = {};
// NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator
/**
* Regular expression to detect invalid characters in the value portion of a CSS style declaration.
*
* This regex matches a colon (:) that is not inside parentheses and not inside single or double quotes.
* It is used to ensure that the value part of a CSS property does not contain unexpected colons,
* which would indicate a malformed declaration (e.g., "color: foo:bar;" is invalid).
*
* The negative lookahead `(?![^(]*\))` ensures that the colon is not followed by a closing
* parenthesis without encountering an opening parenthesis, effectively ignoring colons inside
* function-like values (e.g., `url(data:image/png;base64,...)`).
*
* The lookahead `(?=(?:[^'"]|'[^']*'|"[^"]*")*$)` ensures that the colon is not inside single or double quotes,
* allowing colons within quoted strings (e.g., `content: ":";` or `background: url("foo:bar.png");`).
*
* Example:
* "color: red;" // valid, does not match
* "background: url(data:image/png;base64,...);" // valid, does not match
* "content: ':';" // valid, does not match
* "color: foo:bar;" // invalid, matches
*/
var basicStylePropertyValueValidationRegExp = /:(?![^(]*\))(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/;
/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration
*/
CSSOM.CSSStyleDeclaration = function CSSStyleDeclaration(){
this.length = 0;
this.parentRule = null;
// NON-STANDARD
this._importants = {};
};
CSSOM.CSSStyleDeclaration.prototype = {
constructor: CSSOM.CSSStyleDeclaration,
/**
*
* @param {string} name
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-getPropertyValue
* @return {string} the value of the property if it has been explicitly set for this declaration block.
* Returns the empty string if the property has not been set.
*/
getPropertyValue: function(name) {
return this[name] || "";
},
/**
*
* @param {string} name
* @param {string} value
* @param {string} [priority=null] "important" or null
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty
*/
setProperty: function(name, value, priority, parseErrorHandler)
{
// NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator
if (basicStylePropertyValueValidationRegExp.test(value)) {
parseErrorHandler && parseErrorHandler('Invalid CSSStyleDeclaration property (name = "' + name + '", value = "' + value + '")');
} else if (this[name]) {
// Property already exist. Overwrite it.
var index = Array.prototype.indexOf.call(this, name);
if (index < 0) {
this[this.length] = name;
this.length++;
}
// If the priority value of the incoming property is "important",
// or the value of the existing property is not "important",
// then remove the existing property and rewrite it.
if (priority || !this._importants[name]) {
this.removeProperty(name);
this[this.length] = name;
this.length++;
this[name] = value + '';
this._importants[name] = priority;
}
} else {
// New property.
this[this.length] = name;
this.length++;
this[name] = value + '';
this._importants[name] = priority;
}
},
/**
*
* @param {string} name
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-removeProperty
* @return {string} the value of the property if it has been explicitly set for this declaration block.
* Returns the empty string if the property has not been set or the property name does not correspond to a known CSS property.
*/
removeProperty: function(name) {
if (!(name in this)) {
return "";
}
var index = Array.prototype.indexOf.call(this, name);
if (index < 0) {
return "";
}
var prevValue = this[name];
this[name] = "";
// That's what WebKit and Opera do
Array.prototype.splice.call(this, index, 1);
// That's what Firefox does
//this[index] = ""
return prevValue;
},
getPropertyCSSValue: function() {
//FIXME
},
/**
*
* @param {String} name
*/
getPropertyPriority: function(name) {
return this._importants[name] || "";
},
/**
* element.style.overflow = "auto"
* element.style.getPropertyShorthand("overflow-x")
* -> "overflow"
*/
getPropertyShorthand: function() {
//FIXME
},
isPropertyImplicit: function() {
//FIXME
},
// Doesn't work in IE < 9
get cssText(){
var properties = [];
for (var i=0, length=this.length; i < length; ++i) {
var name = this[i];
var value = this.getPropertyValue(name);
var priority = this.getPropertyPriority(name);
if (priority) {
priority = " !" + priority;
}
properties[i] = name + ": " + value + priority + ";";
}
return properties.join(" ");
},
set cssText(text){
var i, name;
for (i = this.length; i--;) {
name = this[i];
this[name] = "";
}
Array.prototype.splice.call(this, 0, this.length);
this._importants = {};
var dummyRule = CSSOM.parse('#bogus{' + text + '}').cssRules[0].style;
var length = dummyRule.length;
for (i = 0; i < length; ++i) {
name = dummyRule[i];
this.setProperty(dummyRule[i], dummyRule.getPropertyValue(name), dummyRule.getPropertyPriority(name));
}
}
};
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#the-cssrule-interface
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSRule
*/
CSSOM.CSSRule = function CSSRule() {
this.parentRule = null;
this.parentStyleSheet = null;
};
CSSOM.CSSRule.UNKNOWN_RULE = 0; // obsolete
CSSOM.CSSRule.STYLE_RULE = 1;
CSSOM.CSSRule.CHARSET_RULE = 2; // obsolete
CSSOM.CSSRule.IMPORT_RULE = 3;
CSSOM.CSSRule.MEDIA_RULE = 4;
CSSOM.CSSRule.FONT_FACE_RULE = 5;
CSSOM.CSSRule.PAGE_RULE = 6;
CSSOM.CSSRule.KEYFRAMES_RULE = 7;
CSSOM.CSSRule.KEYFRAME_RULE = 8;
CSSOM.CSSRule.MARGIN_RULE = 9;
CSSOM.CSSRule.NAMESPACE_RULE = 10;
CSSOM.CSSRule.COUNTER_STYLE_RULE = 11;
CSSOM.CSSRule.SUPPORTS_RULE = 12;
CSSOM.CSSRule.DOCUMENT_RULE = 13;
CSSOM.CSSRule.FONT_FEATURE_VALUES_RULE = 14;
CSSOM.CSSRule.VIEWPORT_RULE = 15;
CSSOM.CSSRule.REGION_STYLE_RULE = 16;
CSSOM.CSSRule.CONTAINER_RULE = 17;
CSSOM.CSSRule.LAYER_BLOCK_RULE = 18;
CSSOM.CSSRule.STARTING_STYLE_RULE = 1002;
CSSOM.CSSRule.prototype = {
constructor: CSSOM.CSSRule,
//FIXME
};
exports.CSSRule = CSSOM.CSSRule;
///CommonJS
/**
* @constructor
* @see https://drafts.csswg.org/css-nesting-1/
*/
CSSOM.CSSNestedDeclarations = function CSSNestedDeclarations() {
CSSOM.CSSRule.call(this);
this.style = new CSSOM.CSSStyleDeclaration();
this.style.parentRule = this;
};
CSSOM.CSSNestedDeclarations.prototype = new CSSOM.CSSRule();
CSSOM.CSSNestedDeclarations.prototype.constructor = CSSOM.CSSNestedDeclarations;
CSSOM.CSSNestedDeclarations.prototype.type = 0;
Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "cssText", {
get: function () {
return this.style.cssText;
},
configurable: true,
enumerable: true,
});
/**
* @constructor
* @see https://drafts.csswg.org/cssom/#the-cssgroupingrule-interface
*/
CSSOM.CSSGroupingRule = function CSSGroupingRule() {
CSSOM.CSSRule.call(this);
this.cssRules = [];
};
CSSOM.CSSGroupingRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSGroupingRule.prototype.constructor = CSSOM.CSSGroupingRule;
/**
* Used to insert a new CSS rule to a list of CSS rules.
*
* @example
* cssGroupingRule.cssText
* -> "body{margin:0;}"
* cssGroupingRule.insertRule("img{border:none;}", 1)
* -> 1
* cssGroupingRule.cssText
* -> "body{margin:0;}img{border:none;}"
*
* @param {string} rule
* @param {number} [index]
* @see https://www.w3.org/TR/cssom-1/#dom-cssgroupingrule-insertrule
* @return {number} The index within the grouping rule's collection of the newly inserted rule.
*/
CSSOM.CSSGroupingRule.prototype.insertRule = function insertRule(rule, index) {
if (index < 0 || index > this.cssRules.length) {
throw new RangeError("INDEX_SIZE_ERR");
}
var cssRule = CSSOM.parse(rule).cssRules[0];
cssRule.parentRule = this;
this.cssRules.splice(index, 0, cssRule);
return index;
};
/**
* Used to delete a rule from the grouping rule.
*
* cssGroupingRule.cssText
* -> "img{border:none;}body{margin:0;}"
* cssGroupingRule.deleteRule(0)
* cssGroupingRule.cssText
* -> "body{margin:0;}"
*
* @param {number} index within the grouping rule's rule list of the rule to remove.
* @see https://www.w3.org/TR/cssom-1/#dom-cssgroupingrule-deleterule
*/
CSSOM.CSSGroupingRule.prototype.deleteRule = function deleteRule(index) {
if (index < 0 || index >= this.cssRules.length) {
throw new RangeError("INDEX_SIZE_ERR");
}
this.cssRules.splice(index, 1)[0].parentRule = null;
};
/**
* @constructor
* @see https://drafts.csswg.org/css-counter-styles/#the-csscounterstylerule-interface
*/
CSSOM.CSSCounterStyleRule = function CSSCounterStyleRule() {
CSSOM.CSSRule.call(this);
this.name = "";
};
CSSOM.CSSCounterStyleRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSCounterStyleRule.prototype.constructor = CSSOM.CSSCounterStyleRule;
CSSOM.CSSCounterStyleRule.prototype.type = 11;
/**
* @constructor
* @see https://www.w3.org/TR/css-conditional-3/#the-cssconditionrule-interface
*/
CSSOM.CSSConditionRule = function CSSConditionRule() {
CSSOM.CSSGroupingRule.call(this);
this.cssRules = [];
};
CSSOM.CSSConditionRule.prototype = new CSSOM.CSSGroupingRule();
CSSOM.CSSConditionRule.prototype.constructor = CSSOM.CSSConditionRule;
CSSOM.CSSConditionRule.prototype.conditionText = ''
CSSOM.CSSConditionRule.prototype.cssText = ''
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#cssstylerule
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleRule
*/
CSSOM.CSSStyleRule = function CSSStyleRule() {
CSSOM.CSSGroupingRule.call(this);
this.selectorText = "";
this.style = new CSSOM.CSSStyleDeclaration();
this.style.parentRule = this;
};
CSSOM.CSSStyleRule.prototype = new CSSOM.CSSGroupingRule();
CSSOM.CSSStyleRule.prototype.constructor = CSSOM.CSSStyleRule;
CSSOM.CSSStyleRule.prototype.type = 1;
Object.defineProperty(CSSOM.CSSStyleRule.prototype, "cssText", {
get: function() {
var text;
if (this.selectorText) {
var values = ""
if (this.cssRules.length) {
var valuesArr = [" {"];
this.style.cssText && valuesArr.push(this.style.cssText);
valuesArr.push(this.cssRules.map(function(rule){ return rule.cssText }).join("\n "));
values = valuesArr.join("\n ") + "\n}"
} else {
values = " {" + this.style.cssText + "}";
}
text = this.selectorText + values;
} else {
text = "";
}
return text;
},
set: function(cssText) {
var rule = CSSOM.CSSStyleRule.parse(cssText);
this.style = rule.style;
this.selectorText = rule.selectorText;
}
});
/**
* NON-STANDARD
* lightweight version of parse.js.
* @param {string} ruleText
* @return CSSStyleRule
*/
CSSOM.CSSStyleRule.parse = function(ruleText) {
var i = 0;
var state = "selector";
var index;
var j = i;
var buffer = "";
var SIGNIFICANT_WHITESPACE = {
"selector": true,
"value": true
};
var styleRule = new CSSOM.CSSStyleRule();
var name, priority="";
for (var character; (character = ruleText.charAt(i)); i++) {
switch (character) {
case " ":
case "\t":
case "\r":
case "\n":
case "\f":
if (SIGNIFICANT_WHITESPACE[state]) {
// Squash 2 or more white-spaces in the row into 1
switch (ruleText.charAt(i - 1)) {
case " ":
case "\t":
case "\r":
case "\n":
case "\f":
break;
default:
buffer += " ";
break;
}
}
break;
// String
case '"':
j = i + 1;
index = ruleText.indexOf('"', j) + 1;
if (!index) {
throw '" is missing';
}
buffer += ruleText.slice(i, index);
i = index - 1;
break;
case "'":
j = i + 1;
index = ruleText.indexOf("'", j) + 1;
if (!index) {
throw "' is missing";
}
buffer += ruleText.slice(i, index);
i = index - 1;
break;
// Comment
case "/":
if (ruleText.charAt(i + 1) === "*") {
i += 2;
index = ruleText.indexOf("*/", i);
if (index === -1) {
throw new SyntaxError("Missing */");
} else {
i = index + 1;
}
} else {
buffer += character;
}
break;
case "{":
if (state === "selector") {
styleRule.selectorText = buffer.trim();
buffer = "";
state = "name";
}
break;
case ":":
if (state === "name") {
name = buffer.trim();
buffer = "";
state = "value";
} else {
buffer += character;
}
break;
case "!":
if (state === "value" && ruleText.indexOf("!important", i) === i) {
priority = "important";
i += "important".length;
} else {
buffer += character;
}
break;
case ";":
if (state === "value") {
styleRule.style.setProperty(name, buffer.trim(), priority);
priority = "";
buffer = "";
state = "name";
} else {
buffer += character;
}
break;
case "}":
if (state === "value") {
styleRule.style.setProperty(name, buffer.trim(), priority);
priority = "";
buffer = "";
} else if (state === "name") {
break;
} else {
buffer += character;
}
state = "selector";
break;
default:
buffer += character;
break;
}
}
return styleRule;
};
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#the-medialist-interface
*/
CSSOM.MediaList = function MediaList(){
this.length = 0;
};
CSSOM.MediaList.prototype = {
constructor: CSSOM.MediaList,
/**
* @return {string}
*/
get mediaText() {
return Array.prototype.join.call(this, ", ");
},
/**
* @param {string} value
*/
set mediaText(value) {
var values = value.split(",");
var length = this.length = values.length;
for (var i=0; i<length; i++) {
this[i] = values[i].trim();
}
},
/**
* @param {string} medium
*/
appendMedium: function(medium) {
if (Array.prototype.indexOf.call(this, medium) === -1) {
this[this.length] = medium;
this.length++;
}
},
/**
* @param {string} medium
*/
deleteMedium: function(medium) {
var index = Array.prototype.indexOf.call(this, medium);
if (index !== -1) {
Array.prototype.splice.call(this, index, 1);
}
}
};
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#cssmediarule
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSMediaRule
*/
CSSOM.CSSMediaRule = function CSSMediaRule() {
CSSOM.CSSConditionRule.call(this);
this.media = new CSSOM.MediaList();
};
CSSOM.CSSMediaRule.prototype = new CSSOM.CSSConditionRule();
CSSOM.CSSMediaRule.prototype.constructor = CSSOM.CSSMediaRule;
CSSOM.CSSMediaRule.prototype.type = 4;
// https://opensource.apple.com/source/WebCore/WebCore-7611.1.21.161.3/css/CSSMediaRule.cpp
Object.defineProperties(CSSOM.CSSMediaRule.prototype, {
"conditionText": {
get: function() {
return this.media.mediaText;
},
set: function(value) {
this.media.mediaText = value;
},
configurable: true,
enumerable: true
},
"cssText": {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@media " + this.media.mediaText + " {" + cssTexts.join("") + "}";
},
configurable: true,
enumerable: true
}
});
/**
* @constructor
* @see https://drafts.csswg.org/css-contain-3/
* @see https://www.w3.org/TR/css-contain-3/
*/
CSSOM.CSSContainerRule = function CSSContainerRule() {
CSSOM.CSSConditionRule.call(this);
};
CSSOM.CSSContainerRule.prototype = new CSSOM.CSSConditionRule();
CSSOM.CSSContainerRule.prototype.constructor = CSSOM.CSSContainerRule;
CSSOM.CSSContainerRule.prototype.type = 17;
Object.defineProperties(CSSOM.CSSContainerRule.prototype, {
"conditionText": {
get: function() {
return this.containerText;
},
set: function(value) {
this.containerText = value;
},
configurable: true,
enumerable: true
},
"cssText": {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@container " + this.containerText + " {" + cssTexts.join("") + "}";
},
configurable: true,
enumerable: true
}
});
/**
* @constructor
* @see https://drafts.csswg.org/css-conditional-3/#the-csssupportsrule-interface
*/
CSSOM.CSSSupportsRule = function CSSSupportsRule() {
CSSOM.CSSConditionRule.call(this);
};
CSSOM.CSSSupportsRule.prototype = new CSSOM.CSSConditionRule();
CSSOM.CSSSupportsRule.prototype.constructor = CSSOM.CSSSupportsRule;
CSSOM.CSSSupportsRule.prototype.type = 12;
Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i = 0, length = this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@supports " + this.conditionText + " {" + cssTexts.join("") + "}";
}
});
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#cssimportrule
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSImportRule
*/
CSSOM.CSSImportRule = function CSSImportRule() {
CSSOM.CSSRule.call(this);
this.href = "";
this.media = new CSSOM.MediaList();
this.layerName = null;
this.supportsText = null;
this.styleSheet = new CSSOM.CSSStyleSheet();
};
CSSOM.CSSImportRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSImportRule.prototype.constructor = CSSOM.CSSImportRule;
CSSOM.CSSImportRule.prototype.type = 3;
Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
get: function() {
var mediaText = this.media.mediaText;
return "@import url(" + this.href + ")" + (this.layerName !== null ? " layer" + (this.layerName && "(" + this.layerName + ")") : "" ) + (this.supportsText ? " supports(" + this.supportsText + ")" : "" ) + (mediaText ? " " + mediaText : "") + ";";
},
set: function(cssText) {
var i = 0;
/**
* @import url(partial.css) screen, handheld;
* || |
* after-import media
* |
* url
*/
var state = '';
var buffer = '';
var index;
var layerRegExp = /layer\(([^)]*)\)/;
var layerRuleNameRegExp = /^(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)$/;
var supportsRegExp = /supports\(([^)]+)\)/;
var doubleOrMoreSpacesRegExp = /\s{2,}/g;
for (var character; (character = cssText.charAt(i)); i++) {
switch (character) {
case ' ':
case '\t':
case '\r':
case '\n':
case '\f':
if (state === 'after-import') {
state = 'url';
} else {
buffer += character;
}
break;
case '@':
if (!state && cssText.indexOf('@import', i) === i) {
state = 'after-import';
i += 'import'.length;
buffer = '';
}
break;
case 'u':
if (state === 'media') {
buffer += character;
}
if (state === 'url' && cssText.indexOf('url(', i) === i) {
index = cssText.indexOf(')', i + 1);
if (index === -1) {
throw i + ': ")" not found';
}
i += 'url('.length;
var url = cssText.slice(i, index);
if (url[0] === url[url.length - 1]) {
if (url[0] === '"' || url[0] === "'") {
url = url.slice(1, -1);
}
}
this.href = url;
i = index;
state = 'media';
}
break;
case '"':
if (state === 'after-import' || state === 'url') {
index = cssText.indexOf('"', i + 1);
if (!index) {
throw i + ": '\"' not found";
}
this.href = cssText.slice(i + 1, index);
i = index;
state = 'media';
}
break;
case "'":
if (state === 'after-import' || state === 'url') {
index = cssText.indexOf("'", i + 1);
if (!index) {
throw i + ': "\'" not found';
}
this.href = cssText.slice(i + 1, index);
i = index;
state = 'media';
}
break;
case ';':
if (state === 'media') {
if (buffer) {
var bufferTrimmed = buffer.trim();
if (bufferTrimmed.indexOf('layer') === 0) {
var layerMatch = bufferTrimmed.match(layerRegExp);
if (layerMatch) {
var layerName = layerMatch[1].trim();
bufferTrimmed = bufferTrimmed.replace(layerRegExp, '')
.replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space
.trim();
if (layerName.match(layerRuleNameRegExp) !== null) {
this.layerName = layerMatch[1].trim();
} else {
// REVIEW: In the browser, an empty layer() is not processed as a unamed layer
// and treats the rest of the string as mediaText, ignoring the parse of supports()
if (bufferTrimmed) {
this.media.mediaText = bufferTrimmed;
return;
}
}
} else {
this.layerName = "";
bufferTrimmed = bufferTrimmed.substring('layer'.length).trim()
}
}
var supportsMatch = bufferTrimmed.match(supportsRegExp);
if (supportsMatch && supportsMatch.index === 0) {
// REVIEW: In the browser, an empty supports() invalidates and ignores the entire @import rule
this.supportsText = supportsMatch[1].trim();
bufferTrimmed = bufferTrimmed.replace(supportsRegExp, '')
.replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space
.trim();
}
// REVIEW: In the browser, any invalid media is replaced with 'not all'
if (bufferTrimmed) {
this.media.mediaText = bufferTrimmed;
}
}
}
break;
default:
if (state === 'media') {
buffer += character;
}
break;
}
}
}
});
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#css-font-face-rule
*/
CSSOM.CSSFontFaceRule = function CSSFontFaceRule() {
CSSOM.CSSRule.call(this);
this.style = new CSSOM.CSSStyleDeclaration();
this.style.parentRule = this;
};
CSSOM.CSSFontFaceRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSFontFaceRule.prototype.constructor = CSSOM.CSSFontFaceRule;
CSSOM.CSSFontFaceRule.prototype.type = 5;
//FIXME
//CSSOM.CSSFontFaceRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSFontFaceRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
// http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSFontFaceRule.cpp
Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", {
get: function() {
return "@font-face {" + this.style.cssText + "}";
}
});
/**
* @constructor
* @see http://www.w3.org/TR/shadow-dom/#host-at-rule
*/
CSSOM.CSSHostRule = function CSSHostRule() {
CSSOM.CSSRule.call(this);
this.cssRules = [];
};
CSSOM.CSSHostRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSHostRule.prototype.constructor = CSSOM.CSSHostRule;
CSSOM.CSSHostRule.prototype.type = 1001;
//FIXME
//CSSOM.CSSHostRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSHostRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@host {" + cssTexts.join("") + "}";
}
});
/**
* @constructor
* @see http://www.w3.org/TR/shadow-dom/#host-at-rule
*/
CSSOM.CSSStartingStyleRule = function CSSStartingStyleRule() {
CSSOM.CSSRule.call(this);
this.cssRules = [];
};
CSSOM.CSSStartingStyleRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSStartingStyleRule.prototype.constructor = CSSOM.CSSStartingStyleRule;
CSSOM.CSSStartingStyleRule.prototype.type = 1002;
//FIXME
//CSSOM.CSSStartingStyleRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSStartingStyleRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
Object.defineProperty(CSSOM.CSSStartingStyleRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@starting-style {" + cssTexts.join("") + "}";
}
});
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface
*/
CSSOM.StyleSheet = function StyleSheet() {
this.parentStyleSheet = null;
};
/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet
*/
CSSOM.CSSStyleSheet = function CSSStyleSheet() {
CSSOM.StyleSheet.call(this);
this.cssRules = [];
};
CSSOM.CSSStyleSheet.prototype = new CSSOM.StyleSheet();
CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet;
/**
* Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade.
*
* sheet = new Sheet("body {margin: 0}")
* sheet.toString()
* -> "body{margin:0;}"
* sheet.insertRule("img {border: none}", 0)
* -> 0
* sheet.toString()
* -> "img{border:none;}body{margin:0;}"
*
* @param {string} rule
* @param {number} [index=0]
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-insertRule
* @return {number} The index within the style sheet's rule collection of the newly inserted rule.
*/
CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
if (index === void 0) {
index = 0;
}
if (index < 0 || index > this.cssRules.length) {
throw new RangeError("INDEX_SIZE_ERR");
}
var cssRule = CSSOM.parse(rule).cssRules[0];
cssRule.parentStyleSheet = this;
this.cssRules.splice(index, 0, cssRule);
return index;
};
/**
* Used to delete a rule from the style sheet.
*
* sheet = new Sheet("img{border:none} body{margin:0}")
* sheet.toString()
* -> "img{border:none;}body{margin:0;}"
* sheet.deleteRule(0)
* sheet.toString()
* -> "body{margin:0;}"
*
* @param {number} index within the style sheet's rule list of the rule to remove.
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-deleteRule
*/
CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) {
if (index < 0 || index >= this.cssRules.length) {
throw new RangeError("INDEX_SIZE_ERR");
}
this.cssRules.splice(index, 1);
};
/**
* NON-STANDARD
* @return {string} serialize stylesheet
*/
CSSOM.CSSStyleSheet.prototype.toString = function() {
var result = "";
var rules = this.cssRules;
for (var i=0; i<rules.length; i++) {
result += rules[i].cssText + "\n";
}
return result;
};
/**
* @constructor
* @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframesRule
*/
CSSOM.CSSKeyframesRule = function CSSKeyframesRule() {
CSSOM.CSSRule.call(this);
this.name = '';
this.cssRules = [];
};
CSSOM.CSSKeyframesRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSKeyframesRule.prototype.constructor = CSSOM.CSSKeyframesRule;
CSSOM.CSSKeyframesRule.prototype.type = 7;
//FIXME
//CSSOM.CSSKeyframesRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSKeyframesRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
// http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframesRule.cpp
Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(" " + this.cssRules[i].cssText);
}
return "@" + (this._vendorPrefix || '') + "keyframes " + this.name + " { \n" + cssTexts.join("\n") + "\n}";
}
});
/**
* @constructor
* @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframeRule
*/
CSSOM.CSSKeyframeRule = function CSSKeyframeRule() {
CSSOM.CSSRule.call(this);
this.keyText = '';
this.style = new CSSOM.CSSStyleDeclaration();
this.style.parentRule = this;
};
CSSOM.CSSKeyframeRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSKeyframeRule.prototype.constructor = CSSOM.CSSKeyframeRule;
CSSOM.CSSKeyframeRule.prototype.type = 8;
//FIXME
//CSSOM.CSSKeyframeRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSKeyframeRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
// http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframeRule.cpp
Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "cssText", {
get: function() {
return this.keyText + " {" + this.style.cssText + "} ";
}
});
/**
* @constructor
* @see https://developer.mozilla.org/en/CSS/@-moz-document
*/
CSSOM.MatcherList = function MatcherList(){
this.length = 0;
};
CSSOM.MatcherList.prototype = {
constructor: CSSOM.MatcherList,
/**
* @return {string}
*/
get matcherText() {
return Array.prototype.join.call(this, ", ");
},
/**
* @param {string} value
*/
set matcherText(value) {
// just a temporary solution, actually it may be wrong by just split the value with ',', because a url can include ','.
var values = value.split(",");
var length = this.length = values.length;
for (var i=0; i<length; i++) {
this[i] = values[i].trim();
}
},
/**
* @param {string} matcher
*/
appendMatcher: function(matcher) {
if (Array.prototype.indexOf.call(this, matcher) === -1) {
this[this.length] = matcher;
this.length++;
}
},
/**
* @param {string} matcher
*/
deleteMatcher: function(matcher) {
var index = Array.prototype.indexOf.call(this, matcher);
if (index !== -1) {
Array.prototype.splice.call(this, index, 1);
}
}
};
/**
* @constructor
* @see https://developer.mozilla.org/en/CSS/@-moz-document
*/
CSSOM.CSSDocumentRule = function CSSDocumentRule() {
CSSOM.CSSRule.call(this);
this.matcher = new CSSOM.MatcherList();
this.cssRules = [];
};
CSSOM.CSSDocumentRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSDocumentRule.prototype.constructor = CSSOM.CSSDocumentRule;
CSSOM.CSSDocumentRule.prototype.type = 10;
//FIXME
//CSSOM.CSSDocumentRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSDocumentRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
Object.defineProperty(CSSOM.CSSDocumentRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@-moz-document " + this.matcher.matcherText + " {" + cssTexts.join("") + "}";
}
});
/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
*
* TODO: add if needed
*/
CSSOM.CSSValue = function CSSValue() {
};
CSSOM.CSSValue.prototype = {
constructor: CSSOM.CSSValue,
// @see: http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
set cssText(text) {
var name = this._getConstructorName();
throw new Error('DOMException: property "cssText" of "' + name + '" is readonly and can not be replaced with "' + text + '"!');
},
get cssText() {
var name = this._getConstructorName();
throw new Error('getter "cssText" of "' + name + '" is not implemented!');
},
_getConstructorName: function() {
var s = this.constructor.toString(),
c = s.match(/function\s([^\(]+)/),
name = c[1];
return name;
}
};
/**
* @constructor
* @see http://msdn.microsoft.com/en-us/library/ms537634(v=vs.85).aspx
*
*/
CSSOM.CSSValueExpression = function CSSValueExpression(token, idx) {
this._token = token;
this._idx = idx;
};
CSSOM.CSSValueExpression.prototype = new CSSOM.CSSValue();
CSSOM.CSSValueExpression.prototype.constructor = CSSOM.CSSValueExpression;
/**
* parse css expression() value
*
* @return {Object}
* - error:
* or
* - idx:
* - expression:
*
* Example:
*
* .selector {
* zoom: expression(documentElement.clientWidth > 1000 ? '1000px' : 'auto');
* }
*/
CSSOM.CSSValueExpression.prototype.parse = function() {
var token = this._token,
idx = this._idx;
var character = '',
expression = '',
error = '',
info,
paren = [];
for (; ; ++idx) {
character = token.charAt(idx);
// end of token
if (character === '') {
error = 'css expression error: unfinished expression!';
break;
}
switch(character) {
case '(':
paren.push(character);
expression += character;
break;
case ')':
paren.pop(character);
expression += character;
break;
case '/':
if ((info = this._parseJSComment(token, idx))) { // comment?
if (info.error) {
error = 'css expression error: unfinished comment in expression!';
} else {
idx = info.idx;
// ignore the comment
}
} else if ((info = this._parseJSRexExp(token, idx))) { // regexp
idx = info.idx;
expression += info.text;
} else { // other
expression += character;
}
break;
case "'":
case '"':
info = this._parseJSString(token, idx, character);
if (info) { // string
idx = info.idx;
expression += info.text;
} else {
expression += character;
}
break;
default:
expression += character;
break;
}
if (error) {
break;
}
// end of expression
if (paren.length === 0) {
break;
}
}
var ret;
if (error) {
ret = {
error: error
};
} else {
ret = {
idx: idx,
expression: expression
};
}
return ret;
};
/**
*
* @return {Object|false}
* - idx:
* - text:
* or
* - error:
* or
* false
*
*/
CSSOM.CSSValueExpression.prototype._parseJSComment = function(token, idx) {
var nextChar = token.charAt(idx + 1),
text;
if (nextChar === '/' || nextChar === '*') {
var startIdx = idx,
endIdx,
commentEndChar;
if (nextChar === '/') { // line comment
commentEndChar = '\n';
} else if (nextChar === '*') { // block comment
commentEndChar = '*/';
}
endIdx = token.indexOf(commentEndChar, startIdx + 1 + 1);
if (endIdx !== -1) {
endIdx = endIdx + commentEndChar.length - 1;
text = token.substring(idx, endIdx + 1);
return {
idx: endIdx,
text: text
};
} else {
var error = 'css expression error: unfinished comment in expression!';
return {
error: error
};
}
} else {
return false;
}
};
/**
*
* @return {Object|false}
* - idx:
* - text:
* or
* false
*
*/
CSSOM.CSSValueExpression.prototype._parseJSString = function(token, idx, sep) {
var endIdx = this._findMatchedIdx(token, idx, sep),
text;
if (endIdx === -1) {
return false;
} else {
text = token.substring(idx, endIdx + sep.length);
return {
idx: endIdx,
text: text
};
}
};
/**
* parse regexp in css expression
*
* @return {Object|false}
* - idx:
* - regExp:
* or
* false
*/
/*
all legal RegExp
/a/
(/a/)
[/a/]
[12, /a/]
!/a/
+/a/
-/a/
* /a/
/ /a/
%/a/
===/a/
!==/a/
==/a/
!=/a/
>/a/
>=/a/
</a/
<=/a/
&/a/
|/a/
^/a/
~/a/
<</a/
>>/a/
>>>/a/
&&/a/
||/a/
?/a/
=/a/
,/a/
delete /a/
in /a/
instanceof /a/
new /a/
typeof /a/
void /a/
*/
CSSOM.CSSValueExpression.prototype._parseJSRexExp = function(token, idx) {
var before = token.substring(0, idx).replace(/\s+$/, ""),
legalRegx = [
/^$/,
/\($/,
/\[$/,
/\!$/,
/\+$/,
/\-$/,
/\*$/,
/\/\s+/,
/\%$/,
/\=$/,
/\>$/,
/<$/,
/\&$/,
/\|$/,
/\^$/,
/\~$/,
/\?$/,
/\,$/,
/delete$/,
/in$/,
/instanceof$/,
/new$/,
/typeof$/,
/void$/
];
var isLegal = legalRegx.some(function(reg) {
return reg.test(before);
});
if (!isLegal) {
return false;
} else {
var sep = '/';
// same logic as string
return this._parseJSString(token, idx, sep);
}
};
/**
*
* find next sep(same line) index in `token`
*
* @return {Number}
*
*/
CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
var startIdx = idx,
endIdx;
var NOT_FOUND = -1;
while(true) {
endIdx = token.indexOf(sep, startIdx + 1);
if (endIdx === -1) { // not found
endIdx = NOT_FOUND;
break;
} else {
var text = token.substring(idx + 1, endIdx),
matched = text.match(/\\+$/);
if (!matched || matched[0] % 2 === 0) { // not escaped
break;
} else {
startIdx = endIdx;
}
}
}
// boundary must be in the same line(js sting or regexp)
var nextNewLineIdx = token.indexOf('\n', idx + 1);
if (nextNewLineIdx < endIdx) {
endIdx = NOT_FOUND;
}
return endIdx;
};
/**
* @constructor
* @see https://drafts.csswg.org/css-cascade-5/#csslayerblockrule
*/
CSSOM.CSSLayerBlockRule = function CSSLayerBlockRule() {
CSSOM.CSSGroupingRule.call(this);
this.name = "";
this.cssRules = [];
};
CSSOM.CSSLayerBlockRule.prototype = new CSSOM.CSSGroupingRule();
CSSOM.CSSLayerBlockRule.prototype.constructor = CSSOM.CSSLayerBlockRule;
CSSOM.CSSLayerBlockRule.prototype.type = 18;
Object.defineProperties(CSSOM.CSSLayerBlockRule.prototype, {
cssText: {
get: function () {
var cssTexts = [];
for (var i = 0, length = this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@layer " + this.name + (this.name && " ") + "{" + cssTexts.join("") + "}";
},
configurable: true,
enumerable: true,
},
});
/**
* @constructor
* @see https://drafts.csswg.org/css-cascade-5/#csslayerstatementrule
*/
CSSOM.CSSLayerStatementRule = function CSSLayerStatementRule() {
CSSOM.CSSRule.call(this);
this.nameList = [];
};
CSSOM.CSSLayerStatementRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSLayerStatementRule.prototype.constructor = CSSOM.CSSLayerStatementRule;
CSSOM.CSSLayerStatementRule.prototype.type = 0;
Object.defineProperties(CSSOM.CSSLayerStatementRule.prototype, {
cssText: {
get: function () {
return "@layer " + this.nameList.join(", ") + ";";
},
configurable: true,
enumerable: true,
},
});
/**
* @param {string} token
*/
CSSOM.parse = function parse(token, errorHandler) {
errorHandler = errorHandler === undefined ? (console && console.error) : errorHandler;
var i = 0;
/**
"before-selector" or
"selector" or
"atRule" or
"atBlock" or
"conditionBlock" or
"before-name" or
"name" or
"before-value" or
"value"
*/
var state = "before-selector";
var index;
var buffer = "";
var valueParenthesisDepth = 0;
var SIGNIFICANT_WHITESPACE = {
"name": true,
"before-name": true,
"selector": true,
"value": true,
"value-parenthesis": true,
"atRule": true,
"importRule-begin": true,
"importRule": true,
"atBlock": true,
"containerBlock": true,
"conditionBlock": true,
"counterStyleBlock": true,
'documentRule-begin': true,
"layerBlock": true
};
var styleSheet = new CSSOM.CSSStyleSheet();
// @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
var currentScope = styleSheet;
// @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
var parentRule;
var ancestorRules = [];
var prevScope;
var name, priority="", styleRule, mediaRule, containerRule, counterStyleRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, layerBlockRule, layerStatementRule, nestedSelectorRule;
var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; // Match @keyframes and vendor-prefixed @keyframes
// Regex above is not ES5 compliant
// var atRulesStatemenRegExp = /(?<!{.*)[;}]\s*/; // Match a statement by verifying it finds a semicolon or closing brace not followed by another semicolon or closing brace
var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
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
var forwardRuleValidationRegExp = /(?:\(|\s|\/\*)/; // Match that the rule is followed by any whitespace, a opening comment or a condition opening parenthesis
var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
var layerRuleNameRegExp = /^(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)$/; // Validates a single @layer name
/**
* Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
* that is not inside a brace block within the given string. Mimics the behavior of a
* regular expression match for such terminators, including any trailing whitespace.
* @param {string} str - The string to search for at-rule statement terminators.
* @returns {object | null} {0: string, index: number} or null if no match is found.
*/
function atRulesStatemenRegExpES5Alternative(ruleSlice) {
for (var i = 0; i < ruleSlice.length; i++) {
var char = ruleSlice[i];
if (char === ';' || char === '}') {
// Simulate negative lookbehind: check if there is a { before this position
var sliceBefore = ruleSlice.substring(0, i);
var openBraceIndex = sliceBefore.indexOf('{');
if (openBraceIndex === -1) {
// No { found before, so we treat it as a valid match
var match = char;
var j = i + 1;
while (j < ruleSlice.length && /\s/.test(ruleSlice[j])) {
match += ruleSlice[j];
j++;
}
var matchObj = [match];
matchObj.index = i;
matchObj.input = ruleSlice;
return matchObj;
}
}
}
return null;
}
/**
* Finds the first balanced block (including nested braces) in the string, starting from fromIndex.
* Returns an object similar to RegExp.prototype.match output.
* @param {string} str - The string to search.
* @param {number} [fromIndex=0] - The index to start searching from.
* @returns {object|null} - { 0: matchedString, index: startIndex, input: str } or null if not found.
*/
function matchBalancedBlock(str, fromIndex) {
fromIndex = fromIndex || 0;
var openIndex = str.indexOf('{', fromIndex);
if (openIndex === -1) return null;
var depth = 0;
for (var i = openIndex; i < str.length; i++) {
if (str[i] === '{') {
depth++;
} else if (str[i] === '}') {
depth--;
if (depth === 0) {
var matchedString = str.slice(openIndex, i + 1);
return {
0: matchedString,
index: openIndex,
input: str
};
}
}
}
return null;
}
/**
* Advances the index `i` to skip over a balanced block of curly braces in the given string.
* This is typically used to ignore the contents of a CSS rule block.
*
* @param {number} i - The current index in the string to start searching from.
* @param {string} str - The string containing the CSS code.
* @param {number} fromIndex - The index in the string where the balanced block search should begin.
* @returns {number} The updated index after skipping the balanced block.
*/
function ignoreBalancedBlock(i, str, fromIndex) {
var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
if (ruleClosingMatch) {
var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
i+= ignoreRange;
if (token.charAt(i) === '}') {
i -= 1;
}
} else {
i += str.length;
}
return i;
}
var parseError = function(message) {
var lines = token.substring(0, i).split('\n');
var lineCount = lines.length;
var charCount = lines.pop().length + 1;
var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
error.line = lineCount;
/* jshint sub : true */
error['char'] = charCount;
error.styleSheet = styleSheet;
// Print the error but continue parsing the sheet
try {
throw error;
} catch(e) {
errorHandler && errorHandler(e);
}
};
var validateAtRule = function(atRuleKey, validCallback, cannotBeNested) {
var isValid = false;
var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
var ruleRegExp = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
var ruleSlice = token.slice(i);
// Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
var shouldPerformCheck = cannotBeNested && currentScope !== styleSheet ? false : true;
// First, check if there is no invalid characters just after the at-rule
if (shouldPerformCheck && ruleSlice.search(ruleRegExp) === 0) {
// Find the closest allowed character before the at-rule (a opening or closing brace, a semicolon or a comment ending)
var beforeSlice = token.slice(0, i);
var regexBefore = new RegExp(beforeRulePortionRegExp.source, beforeRulePortionRegExp.flags);
var matches = beforeSlice.match(regexBefore);
var lastI = matches ? beforeSlice.lastIndexOf(matches[matches.length - 1]) : 0;
var toCheckSlice = token.slice(lastI, i);
// Check if we don't have any invalid in the portion before the `at-rule` and the closest allowed character
var checkedSlice = toCheckSlice.search(beforeRuleValidationRegExp);
if (checkedSlice === 0) {
isValid = true;
}
}
if (!isValid) {
// If it's invalid the browser will simply ignore the entire invalid block
// Use regex to find the closing brace of the invalid rule
// Regex used above is not ES5 compliant. Using alternative.
// var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
// If it's a statement inside a nested rule, ignore only the statement
if (ruleStatementMatch && currentScope !== styleSheet) {
var ignoreEnd = ruleStatementMatch[0].indexOf(";");
i += ruleStatementMatch.index + ignoreEnd;
return;
}
// Check if there's a semicolon before the invalid at-rule and the first opening brace
if (atRuleKey === "@layer") {
var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";" ) {
// Ignore the rule block until the semicolon
i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
state = "before-selector";
return;
}
}
// Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
i = ignoreBalancedBlock(i, ruleSlice);
state = "before-selector";
} else {
validCallback.call(this);
}
}
/**
* Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes.
* This function replaces the previous basicSelectorRegExp.
*
* This function matches:
* - Type selectors (e.g., `div`, `span`)
* - Universal selector (`*`)
* - ID selectors (e.g., `#header`, `#a\ b`, `#åèiöú`)
* - Class selectors (e.g., `.container`, `.a\ b`, `.åèiöú`)
* - Attribute selectors (e.g., `[type="text"]`)
* - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`)
* - Pseudo-classes with nested parentheses, including cases where parentheses are nested inside arguments,
* such as `:has(.sel:nth-child(3n))`
* - The parent selector (`&`)
* - Combinators (`>`, `+`, `~`) with optional whitespace
* - Whitespace (descendant combinator)
*
* Unicode and escape sequences are allowed in identifiers.
*
* @param {string} selector
* @returns {boolean}
*/
function basicSelectorValidator(selector) {
var length = selector.length;
var i = 0;
var stack = [];
var inAttr = false;
var inSingleQuote = false;
var inDoubleQuote = false;
while (i < length) {
var char = selector[i];
if (inSingleQuote) {
if (char === "'" && selector[i - 1] !== "\\") {
inSingleQuote = false;
}
} else if (inDoubleQuote) {
if (char === '"' && selector[i - 1] !== "\\") {
inDoubleQuote = false;
}
} else if (inAttr) {
if (char === "]") {
inAttr = false;
} else if (char === "'") {
inSingleQuote = true;
} else if (char === '"') {
inDoubleQuote = true;
}
} else {
if (char === "[") {
inAttr = true;
} else if (char === "'") {
inSingleQuote = true;
} else if (char === '"') {
inDoubleQuote = true;
} else if (char === "(") {
stack.push("(");
} else if (char === ")") {
if (!stack.length || stack.pop() !== "(") {
return false;
}
}
}
i++;
}
// If any stack or quote/attr context remains, it's invalid
if (stack.length || inAttr || inSingleQuote || inDoubleQuote) {
return false;
}
// Fallback to a loose regexp for the overall selector structure (without deep paren matching)
//