You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
4.0 KiB
146 lines
4.0 KiB
// @ts-nocheck |
|
|
|
'use strict'; |
|
|
|
const _ = require('lodash'); |
|
const findAtRuleContext = require('../../utils/findAtRuleContext'); |
|
const isCustomPropertySet = require('../../utils/isCustomPropertySet'); |
|
const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule'); |
|
const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector'); |
|
const keywordSets = require('../../reference/keywordSets'); |
|
const nodeContextLookup = require('../../utils/nodeContextLookup'); |
|
const optionsMatches = require('../../utils/optionsMatches'); |
|
const parseSelector = require('../../utils/parseSelector'); |
|
const report = require('../../utils/report'); |
|
const resolvedNestedSelector = require('postcss-resolve-nested-selector'); |
|
const ruleMessages = require('../../utils/ruleMessages'); |
|
const specificity = require('specificity'); |
|
const validateOptions = require('../../utils/validateOptions'); |
|
|
|
const ruleName = 'no-descending-specificity'; |
|
|
|
const messages = ruleMessages(ruleName, { |
|
rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"`, |
|
}); |
|
|
|
function rule(on, options) { |
|
return (root, result) => { |
|
const validOptions = validateOptions( |
|
result, |
|
ruleName, |
|
{ |
|
actual: on, |
|
}, |
|
{ |
|
optional: true, |
|
actual: options, |
|
possible: { |
|
ignore: ['selectors-within-list'], |
|
}, |
|
}, |
|
); |
|
|
|
if (!validOptions) { |
|
return; |
|
} |
|
|
|
const selectorContextLookup = nodeContextLookup(); |
|
|
|
root.walkRules((ruleNode) => { |
|
// Ignore custom property set `--foo: {};` |
|
if (isCustomPropertySet(ruleNode)) { |
|
return; |
|
} |
|
|
|
// Ignore nested property `foo: {};` |
|
if (!isStandardSyntaxRule(ruleNode)) { |
|
return; |
|
} |
|
|
|
// Ignores selectors within list of selectors |
|
if ( |
|
optionsMatches(options, 'ignore', 'selectors-within-list') && |
|
ruleNode.selectors.length > 1 |
|
) { |
|
return; |
|
} |
|
|
|
const comparisonContext = selectorContextLookup.getContext( |
|
ruleNode, |
|
findAtRuleContext(ruleNode), |
|
); |
|
|
|
ruleNode.selectors.forEach((selector) => { |
|
const trimSelector = selector.trim(); |
|
|
|
// Ignore `.selector, { }` |
|
if (trimSelector === '') { |
|
return; |
|
} |
|
|
|
// The edge-case of duplicate selectors will act acceptably |
|
const index = ruleNode.selector.indexOf(trimSelector); |
|
|
|
// Resolve any nested selectors before checking |
|
resolvedNestedSelector(selector, ruleNode).forEach((resolvedSelector) => { |
|
parseSelector(resolvedSelector, result, ruleNode, (s) => { |
|
if (!isStandardSyntaxSelector(resolvedSelector)) { |
|
return; |
|
} |
|
|
|
checkSelector(s, ruleNode, index, comparisonContext); |
|
}); |
|
}); |
|
}); |
|
}); |
|
|
|
function checkSelector(selectorNode, ruleNode, sourceIndex, comparisonContext) { |
|
const selector = selectorNode.toString(); |
|
const referenceSelectorNode = lastCompoundSelectorWithoutPseudoClasses(selectorNode); |
|
const selectorSpecificity = specificity.calculate(selector)[0].specificityArray; |
|
const entry = { selector, specificity: selectorSpecificity }; |
|
|
|
if (!comparisonContext.has(referenceSelectorNode)) { |
|
comparisonContext.set(referenceSelectorNode, [entry]); |
|
|
|
return; |
|
} |
|
|
|
const priorComparableSelectors = comparisonContext.get(referenceSelectorNode); |
|
|
|
priorComparableSelectors.forEach((priorEntry) => { |
|
if (specificity.compare(selectorSpecificity, priorEntry.specificity) === -1) { |
|
report({ |
|
ruleName, |
|
result, |
|
node: ruleNode, |
|
message: messages.rejected(selector, priorEntry.selector), |
|
index: sourceIndex, |
|
}); |
|
} |
|
}); |
|
|
|
priorComparableSelectors.push(entry); |
|
} |
|
}; |
|
} |
|
|
|
function lastCompoundSelectorWithoutPseudoClasses(selectorNode) { |
|
const nodesAfterLastCombinator = _.last( |
|
selectorNode.nodes[0].split((node) => { |
|
return node.type === 'combinator'; |
|
}), |
|
); |
|
|
|
const nodesWithoutPseudoClasses = nodesAfterLastCombinator |
|
.filter((node) => { |
|
return node.type !== 'pseudo' || keywordSets.pseudoElements.has(node.value.replace(/:/g, '')); |
|
}) |
|
.join(''); |
|
|
|
return nodesWithoutPseudoClasses.toString(); |
|
} |
|
|
|
rule.ruleName = ruleName; |
|
rule.messages = messages; |
|
module.exports = rule;
|
|
|