mirror of
https://github.com/thangisme/notes.git
synced 2024-11-01 10:17:42 -04:00
99 lines
3.3 KiB
JavaScript
99 lines
3.3 KiB
JavaScript
|
"use strict"
|
||
|
|
||
|
const specificity = require("specificity")
|
||
|
const findAtRuleContext = require("../../utils/findAtRuleContext")
|
||
|
const isCustomPropertySet = require("../../utils/isCustomPropertySet")
|
||
|
const nodeContextLookup = require("../../utils/nodeContextLookup")
|
||
|
const parseSelector = require("../../utils/parseSelector")
|
||
|
const report = require("../../utils/report")
|
||
|
const ruleMessages = require("../../utils/ruleMessages")
|
||
|
const validateOptions = require("../../utils/validateOptions")
|
||
|
const _ = require("lodash")
|
||
|
const keywordSets = require("../../reference/keywordSets")
|
||
|
const resolvedNestedSelector = require("postcss-resolve-nested-selector")
|
||
|
|
||
|
const ruleName = "no-descending-specificity"
|
||
|
|
||
|
const messages = ruleMessages(ruleName, {
|
||
|
rejected: (b, a) => `Expected selector "${b}" to come before selector "${a}"`,
|
||
|
})
|
||
|
|
||
|
const rule = function (actual) {
|
||
|
return (root, result) => {
|
||
|
const validOptions = validateOptions(result, ruleName, { actual })
|
||
|
if (!validOptions) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const selectorContextLookup = nodeContextLookup()
|
||
|
|
||
|
root.walkRules(rule => {
|
||
|
// Ignore custom property set `--foo: {};`
|
||
|
if (isCustomPropertySet(rule)) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const comparisonContext = selectorContextLookup.getContext(rule, findAtRuleContext(rule))
|
||
|
|
||
|
rule.selectors.forEach(selector => {
|
||
|
const trimSelector = selector.trim()
|
||
|
// Ignore `.selector, { }`
|
||
|
if (trimSelector === "") {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// The edge-case of duplicate selectors will act acceptably
|
||
|
const index = rule.selector.indexOf(trimSelector)
|
||
|
// Resolve any nested selectors before checking
|
||
|
resolvedNestedSelector(selector, rule).forEach(resolvedSelector => {
|
||
|
parseSelector(resolvedSelector, result, rule, s => checkSelector(s, rule, index, comparisonContext))
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
function checkSelector(selectorNode, rule, 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: rule,
|
||
|
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
|