/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
/** @fileoverview Calculator for specificity of CSS selectors. */
goog.module('goog.html.CssSpecificity');
goog.module.declareLegacyNamespace();
var userAgent = goog.require('goog.userAgent');
var userAgentProduct = goog.require('goog.userAgent.product');
/**
* Cached mapping from selectors to specificities.
* @type {!Object<string, !Array<number>>}
*/
var specificityCache = {};
/**
* Calculates the specificity of CSS selectors, using a global cache if
* supported.
* @see http://www.w3.org/TR/css3-selectors/#specificity
* @see https://specificity.keegan.st/
* @param {string} selector The CSS selector.
* @return {!Array<number>} The CSS specificity.
* @supported IE9+, other browsers.
*/
function getSpecificity(selector) {
if (userAgentProduct.IE && !userAgent.isVersionOrHigher(9)) {
// IE8 has buggy regex support.
return [0, 0, 0, 0];
}
var specificity = specificityCache.hasOwnProperty(selector) ?
specificityCache[selector] :
null;
if (specificity) {
return specificity;
}
if (Object.keys(specificityCache).length > (1 << 16)) {
// Limit the size of cache to (1 << 16) == 65536. Normally HTML pages don't
// have such numbers of selectors.
specificityCache = {};
}
specificity = calculateSpecificity(selector);
specificityCache[selector] = specificity;
return specificity;
}
/**
* Find matches for a regular expression in the selector and increase count.
* @param {string} selector The selector to match the regex with.
* @param {!Array<number>} specificity The current specificity.
* @param {!RegExp} regex The regular expression.
* @param {number} typeIndex Index of type count.
* @return {string}
*/
function replaceWithEmptyText(selector, specificity, regex, typeIndex) {
return selector.replace(regex, function(match) {
specificity[typeIndex] += 1;
// Replace this simple selector with whitespace so it won't be counted
// in further simple selectors.
return Array(match.length + 1).join(' ');
});
}
/**
* Replace escaped characters with plain text, using the "A" character.
* @see https://www.w3.org/TR/CSS21/syndata.html#characters
* @param {string} selector
* @param {!RegExp} regex
* @return {string}
*/
function replaceWithPlainText(selector, regex) {
return selector.replace(regex, function(match) {
return Array(match.length + 1).join('A');
});
}
/**
* Calculates the specificity of CSS selectors
* @see http://www.w3.org/TR/css3-selectors/#specificity
* @see https://github.com/keeganstreet/specificity
* @see https://specificity.keegan.st/
* @param {string} selector
* @return {!Array<number>} The CSS specificity.
*/
function calculateSpecificity(selector) {
var specificity = [0, 0, 0, 0];
// Cannot use RegExp literals for all regular expressions, IE does not accept
// the syntax.
// Matches a backslash followed by six hexadecimal digits followed by an
// optional single whitespace character.
var escapeHexadecimalRegex = new RegExp('\\\\[0-9A-Fa-f]{6}\\s?', 'g');
// Matches a backslash followed by fewer than six hexadecimal digits
// followed by a mandatory single whitespace character.
var escapeHexadecimalRegex2 = new RegExp('\\\\[0-9A-Fa-f]{1,5}\\s', 'g');
// Matches a backslash followed by any character
var escapeSpecialCharacter = /\\./g;
selector = replaceWithPlainText(selector, escapeHexadecimalRegex);
selector = replaceWithPlainText(selector, escapeHexadecimalRegex2);
selector = replaceWithPlainText(selector, escapeSpecialCharacter);
// Remove the negation pseudo-class (:not) but leave its argument because
// specificity is calculated on its argument.
var pseudoClassWithNotRegex = new RegExp(':not\\(([^\\)]*)\\)', 'g');
selector = selector.replace(pseudoClassWithNotRegex, ' $1 ');
// Remove anything after a left brace in case a user has pasted in a rule,
// not just a selector.
var rulesRegex = new RegExp('{[^]*', 'gm');
selector = selector.replace(rulesRegex, '');
// The following regular expressions assume that selectors matching the
// preceding regular expressions have been removed.
// SPECIFICITY 2: Counts attribute selectors.
var attributeRegex = new RegExp('(\\[[^\\]]+\\])', 'g');
selector = replaceWithEmptyText(selector, specificity, attributeRegex, 2);
// SPECIFICITY 1: Counts ID selectors.
var idRegex = new RegExp('(#[^\\#\\s\\+>~\\.\\[:]+)', 'g');
selector = replaceWithEmptyText(selector, specificity, idRegex, 1);
// SPECIFICITY 2: Counts class selectors.
var classRegex = new RegExp('(\\.[^\\s\\+>~\\.\\[:]+)', 'g');
selector = replaceWithEmptyText(selector, specificity, classRegex, 2);
// SPECIFICITY 3: Counts pseudo-element selectors.
var pseudoElementRegex =
/(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi;
selector = replaceWithEmptyText(selector, specificity, pseudoElementRegex, 3);
// SPECIFICITY 2: Counts pseudo-class selectors.
// A regex for pseudo classes with brackets. For example:
// :nth-child()
// :nth-last-child()
// :nth-of-type()
// :nth-last-type()
// :lang()
var pseudoClassWithBracketsRegex = /(:[\w-]+\([^\)]*\))/gi;
selector = replaceWithEmptyText(
selector, specificity, pseudoClassWithBracketsRegex, 2);
// A regex for other pseudo classes, which don't have brackets.
var pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g;
selector = replaceWithEmptyText(selector, specificity, pseudoClassRegex, 2);
// Remove universal selector and separator characters.
selector = selector.replace(/[\*\s\+>~]/g, ' ');
// Remove any stray dots or hashes which aren't attached to words.
// These may be present if the user is live-editing this selector.
selector = selector.replace(/[#\.]/g, ' ');
// SPECIFICITY 3: The only things left should be element selectors.
var elementRegex = /([^\s\+>~\.\[:]+)/g;
selector = replaceWithEmptyText(selector, specificity, elementRegex, 3);
return specificity;
}
exports = {
getSpecificity: getSpecificity
};