Index: lib/common.js |
=================================================================== |
--- a/lib/common.js |
+++ b/lib/common.js |
@@ -96,8 +96,102 @@ |
} |
} |
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; |
+} |
+ |
+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; |