UNPKG

@acemir/cssom

Version:

CSS Object Model implementation and CSS parser

1,211 lines (1,112 loc) 39.8 kB
//.CommonJS var CSSOM = {}; ///CommonJS /** * @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) // This is similar to the original, but without nested paren limitations var looseSelectorRegExp = /^([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+)+$/; return looseSelectorRegExp.test(selector); } /** * Regular expression to match CSS pseudo-classes with arguments. * * Matches patterns like `:pseudo-class(argument)`, capturing the pseudo-class name and its argument. * * Capture groups: * 1. The pseudo-class name (letters and hyphens). * 2. The argument inside the parentheses (can contain nested parentheses, quoted strings, and other characters.). * * Global flag (`g`) is used to find all matches in the input string. * * Example matches: * - :nth-child(2n+1) * - :has(.sel:nth-child(3n)) * - :not(".foo, .bar") * @type {RegExp} */ var globalPseudoClassRegExp = /:([a-zA-Z-]+)\(((?:[^()"]+|"[^"]*"|'[^']*'|\((?:[^()"]+|"[^"]*"|'[^']*')*\))*?)\)/g; /** * Parses a CSS selector string and splits it into parts, handling nested parentheses. * * This function is useful for splitting selectors that may contain nested function-like * syntax (e.g., :not(.foo, .bar)), ensuring that commas inside parentheses do not split * the selector. * * @param {string} selector - The CSS selector string to parse. * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed. */ function parseAndSplitNestedSelectors(selector) { var depth = 0; var buffer = ""; var parts = []; var inSingleQuote = false; var inDoubleQuote = false; var i, char; for (i = 0; i < selector.length; i++) { char = selector.charAt(i); if (char === "'" && !inDoubleQuote) { inSingleQuote = !inSingleQuote; buffer += char; } else if (char === '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; buffer += char; } else if (!inSingleQuote && !inDoubleQuote) { if (char === '(') { depth++; buffer += char; } else if (char === ')') { depth--; buffer += char; if (depth === 0) { parts.push(buffer.replace(/^\s+|\s+$/g, "")); buffer = ""; } } else if (char === ',' && depth === 0) { if (buffer.replace(/^\s+|\s+$/g, "")) { parts.push(buffer.replace(/^\s+|\s+$/g, "")); } buffer = ""; } else { buffer += char; } } else { buffer += char; } } if (buffer.replace(/^\s+|\s+$/g, "")) { parts.push(buffer.replace(/^\s+|\s+$/g, "")); } return parts; } /** * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes. * * This function checks if the provided selector is valid according to the rules defined by * `basicSelectorValidator`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where), * it recursively validates each nested selector using the same validation logic. * * @param {string} selector - The CSS selector string to validate. * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`. */ // Cache to store validated selectors var validatedSelectorsCache = new Map(); // Only pseudo-classes that accept selector lists should recurse var selectorListPseudoClasses = { 'not': true, 'is': true, 'has': true, 'where': true }; function validateSelector(selector) { if (validatedSelectorsCache.has(selector)) { return validatedSelectorsCache.get(selector); } // Use a non-global regex to find all pseudo-classes with arguments var pseudoClassMatches = []; var pseudoClassRegExp = new RegExp(globalPseudoClassRegExp.source, globalPseudoClassRegExp.flags); var match; while ((match = pseudoClassRegExp.exec(selector)) !== null) { pseudoClassMatches.push(match); } for (var j = 0; j < pseudoClassMatches.length; j++) { var pseudoClass = pseudoClassMatches[j][1]; if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) { var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]); for (var i = 0; i < nestedSelectors.length; i++) { var nestedSelector = nestedSelectors[i]; if (!validatedSelectorsCache.has(nestedSelector)) { var nestedSelectorValidation = validateSelector(nestedSelector); validatedSelectorsCache.set(nestedSelector, nestedSelectorValidation); if (!nestedSelectorValidation) { validatedSelectorsCache.set(selector, false); return false; } } else if (!validatedSelectorsCache.get(nestedSelector)) { validatedSelectorsCache.set(selector, false); return false; } } } } var basicSelectorValidation = basicSelectorValidator(selector); validatedSelectorsCache.set(selector, basicSelectorValidation); return basicSelectorValidation; } /** * Checks if a given CSS selector text is valid by splitting it by commas * and validating each individual selector using the `validateSelector` function. * * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas. * @returns {boolean} Returns true if all selectors are valid, otherwise false. */ function isValidSelectorText(selectorText) { // Check for newlines inside single or double quotes using regex // This matches any quoted string (single or double) containing a newline var quotedNewlineRegExp = /(['"])(?:\\.|[^\\])*?\1/g; var match; while ((match = quotedNewlineRegExp.exec(selectorText)) !== null) { if (/\r|\n/.test(match[0].slice(1, -1))) { return false; } } // Split selectorText by commas and validate each part var selectors = parseAndSplitNestedSelectors(selectorText); for (var i = 0; i < selectors.length; i++) { var processedSelectors = selectors[i].trim(); if (!validateSelector(processedSelectors)) { return false; } } return true; } var endingIndex = token.length - 1; for (var character; (character = token.charAt(i)); i++) { if (i === endingIndex) { switch (state) { case "importRule": case "layerBlock": token += ";" } } switch (character) { case " ": case "\t": case "\r": case "\n": case "\f": if (SIGNIFICANT_WHITESPACE[state]) { buffer += character; } break; // String case '"': index = i + 1; do { index = token.indexOf('"', index) + 1; if (!index) { parseError('Unmatched "'); } } while (token[index - 2] === '\\'); if (index === 0) { break; } buffer += token.slice(i, index); i = index - 1; switch (state) { case 'before-value': state = 'value'; break; case 'importRule-begin': state = 'importRule'; if (i === endingIndex) { token += ';' } break; } break; case "'": index = i + 1; do { index = token.indexOf("'", index) + 1; if (!index) { parseError("Unmatched '"); } } while (token[index - 2] === '\\'); if (index === 0) { break; } buffer += token.slice(i, index); i = index - 1; switch (state) { case 'before-value': state = 'value'; break; case 'importRule-begin': state = 'importRule'; break; } break; // Comment case "/": if (token.charAt(i + 1) === "*") { i += 2; index = token.indexOf("*/", i); if (index === -1) { parseError("Missing */"); } else { i = index + 1; } } else { buffer += character; } if (state === "importRule-begin") { buffer += " "; state = "importRule"; } break; // At-rule case "@": if (token.indexOf("@-moz-document", i) === i) { validateAtRule("@-moz-document", function(){ state = "documentRule-begin"; documentRule = new CSSOM.CSSDocumentRule(); documentRule.__starts = i; i += "-moz-document".length; }); buffer = ""; break; } else if (token.indexOf("@media", i) === i) { validateAtRule("@media", function(){ state = "atBlock"; mediaRule = new CSSOM.CSSMediaRule(); mediaRule.__starts = i; i += "media".length; }); buffer = ""; break; } else if (token.indexOf("@container", i) === i) { validateAtRule("@container", function(){ state = "containerBlock"; containerRule = new CSSOM.CSSContainerRule(); containerRule.__starts = i; i += "container".length; }); buffer = ""; break; } else if (token.indexOf("@counter-style", i) === i) { validateAtRule("@counter-style", function(){ state = "counterStyleBlock" counterStyleRule = new CSSOM.CSSCounterStyleRule(); counterStyleRule.__starts = i; i += "counter-style".length; }, true); buffer = ""; break; } else if (token.indexOf("@layer", i) === i) { validateAtRule("@layer", function(){ state = "layerBlock" layerBlockRule = new CSSOM.CSSLayerBlockRule(); layerBlockRule.__starts = i; i += "layer".length; }); buffer = ""; break; } else if (token.indexOf("@supports", i) === i) { validateAtRule("@supports", function(){ state = "conditionBlock"; supportsRule = new CSSOM.CSSSupportsRule(); supportsRule.__starts = i; i += "supports".length; }); buffer = ""; break; } else if (token.indexOf("@host", i) === i) { validateAtRule("@host", function(){ state = "hostRule-begin"; i += "host".length; hostRule = new CSSOM.CSSHostRule(); hostRule.__starts = i; }); buffer = ""; break; } else if (token.indexOf("@starting-style", i) === i) { validateAtRule("@starting-style", function(){ state = "startingStyleRule-begin"; i += "starting-style".length; startingStyleRule = new CSSOM.CSSStartingStyleRule(); startingStyleRule.__starts = i; }); buffer = ""; break; } else if (token.indexOf("@import", i) === i) { buffer = ""; validateAtRule("@import", function(){ state = "importRule-begin"; i += "import".length; buffer += "@import"; }, true); break; } else if (token.indexOf("@font-face", i) === i) { buffer = ""; validateAtRule("@font-face", function(){ state = "fontFaceRule-begin"; i += "font-face".length; fontFaceRule = new CSSOM.CSSFontFaceRule(); fontFaceRule.__starts = i; }, parentRule && parentRule.constructor.name === "CSSStyleRule" ); break; } else { atKeyframesRegExp.lastIndex = i; var matchKeyframes = atKeyframesRegExp.exec(token); if (matchKeyframes && matchKeyframes.index === i) { state = "keyframesRule-begin"; keyframesRule = new CSSOM.CSSKeyframesRule(); keyframesRule.__starts = i; keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found i += matchKeyframes[0].length - 1; buffer = ""; break; } else if (state === "selector") { state = "atRule"; } } buffer += character; break; case "{": if (currentScope === styleSheet) { nestedSelectorRule = null; } if (state === 'before-selector') { parseError("Unexpected {"); i = ignoreBalancedBlock(i, token.slice(i)); break; } if (state === "selector" || state === "atRule") { if (!nestedSelectorRule && buffer.indexOf(";") !== -1) { var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp); if (ruleClosingMatch) { styleRule = null; buffer = ""; state = "before-selector"; i += ruleClosingMatch.index + ruleClosingMatch[0].length; break; } } if (parentRule) { styleRule.parentRule = parentRule; ancestorRules.push(parentRule); } currentScope = parentRule = styleRule; styleRule.selectorText = buffer.trim().replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) { if (newline) return " "; return match; }); styleRule.style.__starts = i; styleRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-name"; } else if (state === "atBlock") { mediaRule.media.mediaText = buffer.trim(); if (parentRule) { mediaRule.parentRule = parentRule; ancestorRules.push(parentRule); } currentScope = parentRule = mediaRule; mediaRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "containerBlock") { containerRule.containerText = buffer.trim(); if (parentRule) { containerRule.parentRule = parentRule; ancestorRules.push(parentRule); } currentScope = parentRule = containerRule; containerRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "counterStyleBlock") { // TODO: Validate counter-style name. At least that it cannot be empty nor multiple counterStyleRule.name = buffer.trim().replace(/\n/g, ""); currentScope = parentRule = counterStyleRule; counterStyleRule.parentStyleSheet = styleSheet; buffer = ""; } else if (state === "conditionBlock") { supportsRule.conditionText = buffer.trim(); if (parentRule) { supportsRule.parentRule = parentRule; ancestorRules.push(parentRule); } currentScope = parentRule = supportsRule; supportsRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "layerBlock") { layerBlockRule.name = buffer.trim(); var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(layerRuleNameRegExp) !== null; if (isValidName) { if (parentRule) { layerBlockRule.parentRule = parentRule; ancestorRules.push(parentRule); } currentScope = parentRule = layerBlockRule; layerBlockRule.parentStyleSheet = styleSheet; } buffer = ""; state = "before-selector"; } else if (state === "hostRule-begin") { if (parentRule) { ancestorRules.push(parentRule); } currentScope = parentRule = hostRule; hostRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "startingStyleRule-begin") { if (parentRule) { startingStyleRule.parentRule = parentRule; ancestorRules.push(parentRule); } currentScope = parentRule = startingStyleRule; startingStyleRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "fontFaceRule-begin") { if (parentRule) { fontFaceRule.parentRule = parentRule; } fontFaceRule.parentStyleSheet = styleSheet; styleRule = fontFaceRule; buffer = ""; state = "before-name"; } else if (state === "keyframesRule-begin") { keyframesRule.name = buffer.trim(); if (parentRule) { ancestorRules.push(parentRule); keyframesRule.parentRule = parentRule; } keyframesRule.parentStyleSheet = styleSheet; currentScope = parentRule = keyframesRule; buffer = ""; state = "keyframeRule-begin"; } else if (state === "keyframeRule-begin") { styleRule = new CSSOM.CSSKeyframeRule(); styleRule.keyText = buffer.trim(); styleRule.__starts = i; buffer = ""; state = "before-name"; } else if (state === "documentRule-begin") { // FIXME: what if this '{' is in the url text of the match function? documentRule.matcher.matcherText = buffer.trim(); if (parentRule) { ancestorRules.push(parentRule); documentRule.parentRule = parentRule; } currentScope = parentRule = documentRule; documentRule.parentStyleSheet = styleSheet; buffer = ""; state = "before-selector"; } else if (state === "before-name" || state === "name") { if (styleRule.constructor.name === "CSSNestedDeclarations") { if (styleRule.style.length) { parentRule.cssRules.push(styleRule); styleRule.parentRule = parentRule; styleRule.parentStyleSheet = styleSheet; ancestorRules.push(parentRule); } else { // If the styleRule is empty, we can assume that it's a nested selector ancestorRules.push(parentRule); } } else { currentScope = parentRule = styleRule; ancestorRules.push(parentRule); styleRule.parentStyleSheet = styleSheet; } styleRule = new CSSOM.CSSStyleRule(); var processedSelectorText = buffer.trim().replace(/(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g, function(match, _, newline) { if (newline) return " "; return match; }); // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function(sel) { return sel.indexOf('&') === -1 ? '& ' + sel : sel; }).join(', '); styleRule.style.__starts = i - buffer.length; styleRule.parentRule = parentRule; nestedSelectorRule = styleRule; buffer = ""; state = "before-name"; } break; case ":": if (state === "name") { // It can be a nested selector, let's check var openBraceBeforeMatch = token.slice(i).match(/[{;}]/); var hasOpenBraceBefore = openBraceBeforeMatch && openBraceBeforeMatch[0] === '{'; if (hasOpenBraceBefore) { // Is a selector buffer += character; } else { // Is a declaration name = buffer.trim(); buffer = ""; state = "before-value"; } } else { buffer += character; } break; case "(": if (state === 'value') { // ie css expression mode if (buffer.trim() === 'expression') { var info = (new CSSOM.CSSValueExpression(token, i)).parse(); if (info.error) { parseError(info.error); } else { buffer += info.expression; i = info.idx; } } else { state = 'value-parenthesis'; //always ensure this is reset to 1 on transition //from value to value-parenthesis valueParenthesisDepth = 1; buffer += character; } } else if (state === 'value-parenthesis') { valueParenthesisDepth++; buffer += character; } else { buffer += character; } break; case ")": if (state === 'value-parenthesis') { valueParenthesisDepth--; if (valueParenthesisDepth === 0) state = 'value'; } buffer += character; break; case "!": if (state === "value" && token.indexOf("!important", i) === i) { priority = "important"; i += "important".length; } else { buffer += character; } break; case ";": switch (state) { case "before-value": case "before-name": parseError("Unexpected ;"); buffer = ""; state = "before-name"; break; case "value": styleRule.style.setProperty(name, buffer.trim(), priority, parseError); priority = ""; buffer = ""; state = "before-name"; break; case "atRule": buffer = ""; state = "before-selector"; break; case "importRule": var isValid = styleSheet.cssRules.length === 0 || styleSheet.cssRules.some(function (rule) { return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1 }); if (isValid) { importRule = new CSSOM.CSSImportRule(); importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet; importRule.cssText = buffer + character; styleSheet.cssRules.push(importRule); } buffer = ""; state = "before-selector"; break; case "layerBlock": var nameListStr = buffer.trim().split(",").map(function (name) { return name.trim(); }); var isInvalid = parentRule !== undefined || nameListStr.some(function (name) { return name.trim().match(layerRuleNameRegExp) === null; }); if (!isInvalid) { layerStatementRule = new CSSOM.CSSLayerStatementRule(); layerStatementRule.parentStyleSheet = styleSheet; layerStatementRule.__starts = layerBlockRule.__starts; layerStatementRule.__ends = i; layerStatementRule.nameList = nameListStr; styleSheet.cssRules.push(layerStatementRule); } buffer = ""; state = "before-selector"; break; default: buffer += character; break; } break; case "}": if (state === "counterStyleBlock") { // FIXME : Implement cssText get setter that parses the real implementation counterStyleRule.cssText = "@counter-style " + counterStyleRule.name + " { " + buffer.trim().replace(/\n/g, " ").replace(/(['"])(?:\\.|[^\\])*?\1|(\s{2,})/g, function(match, quote) { return quote ? match : ' '; }) + " }"; buffer = ""; state = "before-selector"; } switch (state) { case "value": styleRule.style.setProperty(name, buffer.trim(), priority, parseError); priority = ""; /* falls through */ case "before-value": case "before-name": case "name": styleRule.__ends = i + 1; if (parentRule === styleRule) { parentRule = ancestorRules.pop() } if (parentRule) { styleRule.parentRule = parentRule; } styleRule.parentStyleSheet = styleSheet; if (currentScope === styleRule) { currentScope = parentRule || styleSheet; } if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) { if (styleRule === nestedSelectorRule) { nestedSelectorRule = null; } parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")'); } else { currentScope.cssRules.push(styleRule); } buffer = ""; if (currentScope.constructor === CSSOM.CSSKeyframesRule) { state = "keyframeRule-begin"; } else { state = "before-selector"; } if (styleRule.constructor.name === "CSSNestedDeclarations") { if (currentScope !== styleSheet) { nestedSelectorRule = currentScope; } styleRule = null; } else { styleRule = null; break; } case "keyframeRule-begin": case "before-selector": case "selector": // End of media/supports/document rule. if (!parentRule) { parseError("Unexpected }"); var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule"; if (hasPreviousStyleRule) { i = ignoreBalancedBlock(i, token.slice(i), 1); } break; } while (ancestorRules.length > 0) { parentRule = ancestorRules.pop(); if ( parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSMediaRule" || parentRule.constructor.name === "CSSSupportsRule" || parentRule.constructor.name === "CSSContainerRule" || parentRule.constructor.name === "CSSLayerBlockRule" || parentRule.constructor.name === "CSSStartingStyleRule" ) { if (nestedSelectorRule) { if (nestedSelectorRule.parentRule) { prevScope = nestedSelectorRule; currentScope = nestedSelectorRule.parentRule; if (currentScope.cssRules.findIndex(function (rule) { return rule === prevScope }) === -1) { currentScope.cssRules.push(prevScope); } nestedSelectorRule = currentScope; } } else { prevScope = currentScope; currentScope = parentRule; currentScope !== prevScope && currentScope.cssRules.push(prevScope); break; } } } if (currentScope.parentRule == null) { currentScope.__ends = i + 1; if (currentScope !== styleSheet && styleSheet.cssRules.findIndex(function (rule) { return rule === currentScope }) === -1) { styleSheet.cssRules.push(currentScope); } currentScope = styleSheet; if (nestedSelectorRule === parentRule) { // Check if this selector is really starting inside another selector var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1); var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/{/g); var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(/}/g); var openingBraceLen = openingBraceMatch && openingBraceMatch.length; var closingBraceLen = closingBraceMatch && closingBraceMatch.length; if (openingBraceLen === closingBraceLen) { // If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule nestedSelectorRule.__ends = i + 1; nestedSelectorRule = null; parentRule = null; } } else { parentRule = null; } } buffer = ""; state = "before-selector"; break; } break; default: switch (state) { case "before-selector": state = "selector"; if (styleRule && parentRule) { // Assuming it's a declaration inside Nested Selector OR a Nested Declaration // If Declaration inside Nested Selector let's keep the same styleRule if ( parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSMediaRule" || parentRule.constructor.name === "CSSSupportsRule" || parentRule.constructor.name === "CSSContainerRule" || parentRule.constructor.name === "CSSLayerBlockRule" || parentRule.constructor.name === "CSSStartingStyleRule" ) { // parentRule.parentRule = styleRule; state = "before-name"; if (styleRule !== parentRule) { styleRule = new CSSOM.CSSNestedDeclarations(); styleRule.__starts = i; } } } else if (nestedSelectorRule && parentRule && ( parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSMediaRule" || parentRule.constructor.name === "CSSSupportsRule" || parentRule.constructor.name === "CSSContainerRule" || parentRule.constructor.name === "CSSLayerBlockRule" || parentRule.constructor.name === "CSSStartingStyleRule" )) { state = "before-name"; if (parentRule.cssRules.length) { currentScope = nestedSelectorRule = parentRule; styleRule = new CSSOM.CSSNestedDeclarations(); styleRule.__starts = i; } else { if (parentRule.constructor.name === "CSSStyleRule") { styleRule = parentRule; } else { styleRule = new CSSOM.CSSStyleRule(); styleRule.__starts = i; } } } else { styleRule = new CSSOM.CSSStyleRule(); styleRule.__starts = i; } break; case "before-name": state = "name"; break; case "before-value": state = "value"; break; case "importRule-begin": state = "importRule"; break; } buffer += character; break; } } return styleSheet; }; //.CommonJS exports.parse = CSSOM.parse; // The following modules cannot be included sooner due to the mutual dependency with parse.js CSSOM.CSSStyleSheet = require("./CSSStyleSheet").CSSStyleSheet; CSSOM.CSSStyleRule = require("./CSSStyleRule").CSSStyleRule; CSSOM.CSSNestedDeclarations = require("./CSSNestedDeclarations").CSSNestedDeclarations; CSSOM.CSSImportRule = require("./CSSImportRule").CSSImportRule; CSSOM.CSSGroupingRule = require("./CSSGroupingRule").CSSGroupingRule; CSSOM.CSSMediaRule = require("./CSSMediaRule").CSSMediaRule; CSSOM.CSSCounterStyleRule = require("./CSSCounterStyleRule").CSSCounterStyleRule; CSSOM.CSSContainerRule = require("./CSSContainerRule").CSSContainerRule; CSSOM.CSSConditionRule = require("./CSSConditionRule").CSSConditionRule; CSSOM.CSSSupportsRule = require("./CSSSupportsRule").CSSSupportsRule; CSSOM.CSSFontFaceRule = require("./CSSFontFaceRule").CSSFontFaceRule; CSSOM.CSSHostRule = require("./CSSHostRule").CSSHostRule; CSSOM.CSSStartingStyleRule = require("./CSSStartingStyleRule").CSSStartingStyleRule; CSSOM.CSSStyleDeclaration = require('./CSSStyleDeclaration').CSSStyleDeclaration; CSSOM.CSSKeyframeRule = require('./CSSKeyframeRule').CSSKeyframeRule; CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule; CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression; CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule; CSSOM.CSSLayerBlockRule = require("./CSSLayerBlockRule").CSSLayerBlockRule; CSSOM.CSSLayerStatementRule = require("./CSSLayerStatementRule").CSSLayerStatementRule; ///CommonJS