| Index: lib/common.js |
| =================================================================== |
| --- a/lib/common.js |
| +++ b/lib/common.js |
| @@ -96,8 +96,110 @@ |
| } |
| } |
| selectors.push(selector.substring(start)); |
| return selectors; |
| } |
| exports.splitSelector = splitSelector; |
| + |
| +function findTargetSelectorIndex(selector) |
| +{ |
| + let index = 0; |
| + let whitespace = 0; |
| + let scope = []; |
| + |
| + // Start from the end of the string and go character by character, where each |
| + // character is a Unicode code point. |
| + for (let character of [...selector].reverse()) |
| + { |
| + let currentScope = scope[scope.length - 1]; |
| + |
| + if (character == "'" || character == "\"") |
| + { |
| + // If we're already within the same type of quote, close the scope; |
| + // otherwise open a new scope. |
| + if (currentScope == character) |
| + scope.pop(); |
| + else |
| + scope.push(character); |
| + } |
| + else if (character == "]" || character == ")") |
| + { |
| + // For closing brackets and parentheses, open a new scope only if we're |
| + // not within a quote. Within quotes these characters should have no |
| + // meaning. |
| + if (currentScope != "'" && currentScope != "\"") |
| + scope.push(character); |
| + } |
| + else if (character == "[") |
| + { |
| + // If we're already within a bracket, close the scope. |
| + if (currentScope == "]") |
| + scope.pop(); |
| + } |
| + else if (character == "(") |
| + { |
| + // If we're already within a parenthesis, close the scope. |
| + if (currentScope == ")") |
| + scope.pop(); |
| + } |
| + else if (!currentScope) |
| + { |
| + // At the top level (not within any scope), count the whitespace if we've |
| + // encountered it. Otherwise if we've hit one of the combinators, |
| + // terminate here; otherwise if we've hit a non-colon character, |
| + // terminate here. |
| + if (/\s/u.test(character)) |
| + { |
| + whitespace++; |
| + } |
| + else if ((character == ">" || character == "+" || character == "~") || |
| + (whitespace > 0 && character != ":")) |
| + { |
| + break; |
| + } |
| + } |
| + |
| + // Zero out the whitespace count if we've entered a scope. |
| + if (scope.length > 0) |
| + whitespace = 0; |
| + |
| + // Increment the index by the size of the character. Note that for Unicode |
| + // composite characters (like emoji) this will be more than one. |
| + index += character.length; |
| + } |
| + |
| + return selector.length - index + whitespace; |
| +} |
| + |
| +/** |
| + * Qualifies a CSS selector with a qualifier, which may be another CSS selector |
| + * or an empty string. For example, given the selector "div.bar" and the |
| + * qualifier "#foo", this function returns "div#foo.bar". |
| + * @param {string} selector The selector to qualify. |
| + * @param {string} qualifier The qualifier with which to qualify the selector. |
| + * @returns {string} The qualified selector. |
| + */ |
| +function qualifySelector(selector, qualifier) |
| +{ |
| + let qualifiedSelector = ""; |
| + |
| + for (let sub of splitSelector(selector)) |
| + { |
| + sub = sub.trim(); |
| + |
| + qualifiedSelector += ", "; |
| + |
| + let index = findTargetSelectorIndex(sub); |
| + let [, type = "", rest] = /^([a-z][a-z-]*)?(.*)/i.exec(sub.substr(index)); |
| + |
| + // Note that the first group in the regular expression is optional. If it |
| + // doesn't match (e.g. "#foo::nth-child(1)"), type will be an empty string. |
| + qualifiedSelector += sub.substr(0, index) + type + qualifier + rest; |
| + } |
| + |
| + // Remove the initial comma and space. |
| + return qualifiedSelector.substr(2); |
| +} |
| + |
| +exports.qualifySelector = qualifySelector; |