214 lines
5.0 KiB
JavaScript
214 lines
5.0 KiB
JavaScript
'use strict';
|
|
|
|
const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
|
|
const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
|
|
const parseSelector = require('../../utils/parseSelector');
|
|
const report = require('../../utils/report');
|
|
const ruleMessages = require('../../utils/ruleMessages');
|
|
const validateOptions = require('../../utils/validateOptions');
|
|
const {
|
|
isPseudoClass,
|
|
isAttribute,
|
|
isClassName,
|
|
isUniversal,
|
|
isIdentifier,
|
|
isTag,
|
|
} = require('postcss-selector-parser');
|
|
const { assert } = require('../../utils/validateTypes');
|
|
|
|
const ruleName = 'selector-not-notation';
|
|
const messages = ruleMessages(ruleName, {
|
|
expected: (type) => `Expected ${type} :not() pseudo-class notation`,
|
|
});
|
|
const meta = {
|
|
url: 'https://stylelint.io/user-guide/rules/selector-not-notation',
|
|
fixable: true,
|
|
};
|
|
|
|
/** @typedef {import('postcss-selector-parser').Node} Node */
|
|
/** @typedef {import('postcss-selector-parser').Selector} Selector */
|
|
/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */
|
|
|
|
/**
|
|
* @param {Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
const isSimpleSelector = (node) =>
|
|
isPseudoClass(node) ||
|
|
isAttribute(node) ||
|
|
isClassName(node) ||
|
|
isUniversal(node) ||
|
|
isIdentifier(node) ||
|
|
isTag(node);
|
|
|
|
/**
|
|
* @param {Node} node
|
|
* @returns {node is Pseudo}
|
|
*/
|
|
const isNot = (node) =>
|
|
isPseudoClass(node) && node.value !== undefined && node.value.toLowerCase() === ':not';
|
|
|
|
/**
|
|
* @param {Selector[]} list
|
|
* @returns {boolean}
|
|
*/
|
|
const isSimple = (list) => {
|
|
if (list.length > 1) return false;
|
|
|
|
assert(list[0], 'list is never empty');
|
|
const [first, second] = list[0].nodes;
|
|
|
|
if (!first) return true;
|
|
|
|
if (second) return false;
|
|
|
|
return isSimpleSelector(first) && !isNot(first);
|
|
};
|
|
|
|
/** @type {import('stylelint').Rule} */
|
|
const rule = (primary, _, context) => {
|
|
return (root, result) => {
|
|
const validOptions = validateOptions(result, ruleName, {
|
|
actual: primary,
|
|
possible: ['simple', 'complex'],
|
|
});
|
|
|
|
if (!validOptions) return;
|
|
|
|
root.walkRules((ruleNode) => {
|
|
if (!isStandardSyntaxRule(ruleNode)) return;
|
|
|
|
const selector = ruleNode.selector;
|
|
|
|
if (!selector.includes(':not(')) return;
|
|
|
|
if (!isStandardSyntaxSelector(selector)) return;
|
|
|
|
const fixedSelector = parseSelector(selector, result, ruleNode, (container) => {
|
|
container.walkPseudos((pseudo) => {
|
|
if (!isNot(pseudo)) return;
|
|
|
|
if (primary === 'complex') {
|
|
const prev = pseudo.prev();
|
|
const hasConsecutiveNot = prev && isNot(prev);
|
|
|
|
if (!hasConsecutiveNot) return;
|
|
|
|
if (context.fix) return fixComplex(prev);
|
|
} else {
|
|
const selectors = pseudo.nodes;
|
|
|
|
if (isSimple(selectors)) return;
|
|
|
|
const mustFix =
|
|
context.fix &&
|
|
selectors.length > 1 &&
|
|
selectors[1] &&
|
|
(selectors[1].nodes.length === 0 ||
|
|
selectors.every(({ nodes }) => nodes.length === 1));
|
|
|
|
if (mustFix) return fixSimple(pseudo);
|
|
}
|
|
|
|
assert(pseudo.source && pseudo.source.end);
|
|
|
|
report({
|
|
message: messages.expected,
|
|
messageArgs: [primary],
|
|
node: ruleNode,
|
|
index: pseudo.sourceIndex,
|
|
endIndex: pseudo.source.end.column,
|
|
result,
|
|
ruleName,
|
|
});
|
|
});
|
|
});
|
|
|
|
if (context.fix && fixedSelector) {
|
|
ruleNode.selector = fixedSelector;
|
|
}
|
|
});
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @param {Pseudo} not
|
|
* @returns {Node}
|
|
*/
|
|
const getLastConsecutiveNot = ({ parent, sourceIndex }) => {
|
|
assert(parent);
|
|
|
|
const nodes = parent.nodes;
|
|
const index = nodes.findIndex((node) => node.sourceIndex >= sourceIndex && !isNot(node));
|
|
const node = index === -1 ? nodes[nodes.length - 1] : nodes[index - 1];
|
|
|
|
assert(node);
|
|
|
|
return node;
|
|
};
|
|
|
|
/**
|
|
* @param {Pseudo} not
|
|
*/
|
|
function fixSimple(not) {
|
|
const simpleSelectors = not.nodes
|
|
.filter(({ nodes }) => nodes[0] && isSimpleSelector(nodes[0]))
|
|
.map((s) => {
|
|
assert(s.nodes[0]);
|
|
s.nodes[0].rawSpaceBefore = '';
|
|
s.nodes[0].rawSpaceAfter = '';
|
|
|
|
return s;
|
|
});
|
|
const firstSelector = simpleSelectors.shift();
|
|
|
|
assert(firstSelector);
|
|
assert(not.parent);
|
|
|
|
not.empty();
|
|
not.nodes.push(firstSelector);
|
|
|
|
for (const s of simpleSelectors) {
|
|
const last = getLastConsecutiveNot(not);
|
|
|
|
not.parent.insertAfter(last, last.clone({ nodes: [s] }));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Pseudo} previousNot
|
|
*/
|
|
function fixComplex(previousNot) {
|
|
const indentAndTrimRight = (/** @type {Selector[]} */ selectors) => {
|
|
for (const s of selectors) {
|
|
assert(s.nodes[0]);
|
|
s.nodes[0].rawSpaceBefore = ' ';
|
|
s.nodes[0].rawSpaceAfter = '';
|
|
}
|
|
};
|
|
const [head, ...tail] = previousNot.nodes;
|
|
let node = previousNot.next();
|
|
|
|
if (head == null || head.nodes.length === 0) return;
|
|
|
|
assert(head.nodes[0]);
|
|
head.nodes[0].rawSpaceBefore = '';
|
|
head.nodes[0].rawSpaceAfter = '';
|
|
indentAndTrimRight(tail);
|
|
|
|
while (isNot(node)) {
|
|
const selectors = node.nodes;
|
|
const prev = node;
|
|
|
|
indentAndTrimRight(selectors);
|
|
previousNot.nodes = previousNot.nodes.concat(selectors);
|
|
node = node.next();
|
|
prev.remove();
|
|
}
|
|
}
|
|
|
|
rule.ruleName = ruleName;
|
|
rule.messages = messages;
|
|
rule.meta = meta;
|
|
module.exports = rule;
|