eslint-plugin-vue
Version:
Official ESLint plugin for Vue.js
424 lines (421 loc) • 14.8 kB
JavaScript
;
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.js');
const require_index = require('./index.js');
//#region lib/utils/selector.js
var require_selector = /* @__PURE__ */ require_rolldown_runtime.__commonJSMin(((exports, module) => {
const parser = require("postcss-selector-parser");
const { default: nthCheck } = require("nth-check");
const { getAttribute, isVElement } = require_index.default;
/**
* @typedef {object} VElementSelector
* @property {(element: VElement)=>boolean} test
*/
module.exports = { parseSelector };
/**
* Parses CSS selectors and returns an object with a function that tests VElement.
* @param {string} selector CSS selector
* @param {RuleContext} context - The rule context.
* @returns {VElementSelector}
*/
function parseSelector(selector, context) {
let astSelector;
try {
astSelector = parser().astSync(selector);
} catch {
context.report({
loc: {
line: 0,
column: 0
},
message: `Cannot parse selector: ${selector}.`
});
return { test: () => false };
}
try {
const test = selectorsToVElementMatcher(astSelector.nodes);
return { test(element) {
return test(element, null);
} };
} catch (error) {
if (error instanceof SelectorError) {
context.report({
loc: {
line: 0,
column: 0
},
message: error.message
});
return { test: () => false };
}
throw error;
}
}
var SelectorError = class extends Error {};
/**
* @typedef {(element: VElement, subject: VElement | null )=>boolean} VElementMatcher
* @typedef {Exclude<parser.Selector['nodes'][number], {type:'comment'|'root'}>} ChildNode
*/
/**
* Convert nodes to VElementMatcher
* @param {parser.Selector[]} selectorNodes
* @returns {VElementMatcher}
*/
function selectorsToVElementMatcher(selectorNodes) {
const selectors = selectorNodes.map((n) => selectorToVElementMatcher(cleanSelectorChildren(n)));
return (element, subject) => selectors.some((sel) => sel(element, subject));
}
/**
* @param {parser.Node|null} node
* @returns {node is parser.Combinator}
*/
function isDescendantCombinator(node) {
return Boolean(node && node.type === "combinator" && !node.value.trim());
}
/**
* Clean and get the selector child nodes.
* @param {parser.Selector} selector
* @returns {ChildNode[]}
*/
function cleanSelectorChildren(selector) {
/** @type {ChildNode[]} */
const nodes = [];
/** @type {ChildNode|null} */
let last = null;
for (const node of selector.nodes) {
if (node.type === "root") throw new SelectorError("Unexpected state type=root");
if (node.type === "comment") continue;
if ((last == null || last.type === "combinator") && isDescendantCombinator(node)) continue;
if (isDescendantCombinator(last) && node.type === "combinator") nodes.pop();
nodes.push(node);
last = node;
}
if (isDescendantCombinator(last)) nodes.pop();
return nodes;
}
/**
* Convert Selector child nodes to VElementMatcher
* @param {ChildNode[]} selectorChildren
* @returns {VElementMatcher}
*/
function selectorToVElementMatcher(selectorChildren) {
const nodes = [...selectorChildren];
let node = nodes.shift();
/**
* @type {VElementMatcher | null}
*/
let result = null;
while (node) {
if (node.type === "combinator") {
const combinator = node.value;
node = nodes.shift();
if (!node) throw new SelectorError(`Expected selector after '${combinator}'.`);
if (node.type === "combinator") throw new SelectorError(`Unexpected combinator '${node.value}'.`);
const right = nodeToVElementMatcher(node);
result = combination(result || ((element, subject) => element === subject), combinator, right);
} else {
const sel = nodeToVElementMatcher(node);
result = result ? compound(result, sel) : sel;
}
node = nodes.shift();
}
if (!result) throw new SelectorError(`Unexpected empty selector.`);
return result;
}
/**
* @param {VElementMatcher} left
* @param {string} combinator
* @param {VElementMatcher} right
* @returns {VElementMatcher}
*/
function combination(left, combinator, right) {
switch (combinator.trim()) {
case "": return (element, subject) => {
if (right(element, null)) {
let parent = element.parent;
while (parent.type === "VElement") {
if (left(parent, subject)) return true;
parent = parent.parent;
}
}
return false;
};
case ">": return (element, subject) => {
if (right(element, null)) {
const parent = element.parent;
if (parent.type === "VElement") return left(parent, subject);
}
return false;
};
case "+": return (element, subject) => {
if (right(element, null)) {
const before = getBeforeElement(element);
if (before) return left(before, subject);
}
return false;
};
case "~": return (element, subject) => {
if (right(element, null)) {
for (const before of getBeforeElements(element)) if (left(before, subject)) return true;
}
return false;
};
default: throw new SelectorError(`Unknown combinator: ${combinator}.`);
}
}
/**
* Convert node to VElementMatcher
* @param {Exclude<parser.Node, {type:'combinator'|'comment'|'root'|'selector'}>} selector
* @returns {VElementMatcher}
*/
function nodeToVElementMatcher(selector) {
switch (selector.type) {
case "attribute": return attributeNodeToVElementMatcher(selector);
case "class": return classNameNodeToVElementMatcher(selector);
case "id": return identifierNodeToVElementMatcher(selector);
case "tag": return tagNodeToVElementMatcher(selector);
case "universal": return universalNodeToVElementMatcher(selector);
case "pseudo": return pseudoNodeToVElementMatcher(selector);
case "nesting": throw new SelectorError("Unsupported nesting selector.");
case "string": throw new SelectorError(`Unknown selector: ${selector.value}.`);
default: throw new SelectorError(`Unknown selector: ${selector.value}.`);
}
}
/**
* Convert Attribute node to VElementMatcher
* @param {parser.Attribute} selector
* @returns {VElementMatcher}
*/
function attributeNodeToVElementMatcher(selector) {
const key = selector.attribute;
if (!selector.operator) return (element) => getAttributeValue(element, key) != null;
const value = selector.value || "";
switch (selector.operator) {
case "=": return buildVElementMatcher(value, (attr, val) => attr === val);
case "~=": return buildVElementMatcher(value, (attr, val) => attr.split(/\s+/gu).includes(val));
case "|=": return buildVElementMatcher(value, (attr, val) => attr === val || attr.startsWith(`${val}-`));
case "^=": return buildVElementMatcher(value, (attr, val) => attr.startsWith(val));
case "$=": return buildVElementMatcher(value, (attr, val) => attr.endsWith(val));
case "*=": return buildVElementMatcher(value, (attr, val) => attr.includes(val));
default: throw new SelectorError(`Unsupported operator: ${selector.operator}.`);
}
/**
* @param {string} selectorValue
* @param {(attrValue:string, selectorValue: string)=>boolean} test
* @returns {VElementMatcher}
*/
function buildVElementMatcher(selectorValue, test) {
const val = selector.insensitive ? selectorValue.toLowerCase() : selectorValue;
return (element) => {
const attrValue = getAttributeValue(element, key);
if (attrValue == null) return false;
return test(selector.insensitive ? attrValue.toLowerCase() : attrValue, val);
};
}
}
/**
* Convert ClassName node to VElementMatcher
* @param {parser.ClassName} selector
* @returns {VElementMatcher}
*/
function classNameNodeToVElementMatcher(selector) {
const className = selector.value;
return (element) => {
const attrValue = getAttributeValue(element, "class");
if (attrValue == null) return false;
return attrValue.split(/\s+/gu).includes(className);
};
}
/**
* Convert Identifier node to VElementMatcher
* @param {parser.Identifier} selector
* @returns {VElementMatcher}
*/
function identifierNodeToVElementMatcher(selector) {
const id = selector.value;
return (element) => {
const attrValue = getAttributeValue(element, "id");
if (attrValue == null) return false;
return attrValue === id;
};
}
/**
* Convert Tag node to VElementMatcher
* @param {parser.Tag} selector
* @returns {VElementMatcher}
*/
function tagNodeToVElementMatcher(selector) {
const name = selector.value;
return (element) => element.rawName === name;
}
/**
* Convert Universal node to VElementMatcher
* @param {parser.Universal} _selector
* @returns {VElementMatcher}
*/
function universalNodeToVElementMatcher(_selector) {
return () => true;
}
/**
* Convert Pseudo node to VElementMatcher
* @param {parser.Pseudo} selector
* @returns {VElementMatcher}
*/
function pseudoNodeToVElementMatcher(selector) {
const pseudo = selector.value;
switch (pseudo) {
case ":not": {
const selectors = selectorsToVElementMatcher(selector.nodes);
return (element, subject) => !selectors(element, subject);
}
case ":is":
case ":where": return selectorsToVElementMatcher(selector.nodes);
case ":has": return pseudoHasSelectorsToVElementMatcher(selector.nodes);
case ":empty": return (element) => element.children.every((child) => child.type === "VText" && !child.value.trim());
case ":nth-child": return buildPseudoNthVElementMatcher(parseNth(selector));
case ":nth-last-child": {
const nth = parseNth(selector);
return buildPseudoNthVElementMatcher((index, length) => nth(length - index - 1));
}
case ":first-child": return buildPseudoNthVElementMatcher((index) => index === 0);
case ":last-child": return buildPseudoNthVElementMatcher((index, length) => index === length - 1);
case ":only-child": return buildPseudoNthVElementMatcher((index, length) => index === 0 && length === 1);
case ":nth-of-type": return buildPseudoNthOfTypeVElementMatcher(parseNth(selector));
case ":nth-last-of-type": {
const nth = parseNth(selector);
return buildPseudoNthOfTypeVElementMatcher((index, length) => nth(length - index - 1));
}
case ":first-of-type": return buildPseudoNthOfTypeVElementMatcher((index) => index === 0);
case ":last-of-type": return buildPseudoNthOfTypeVElementMatcher((index, length) => index === length - 1);
case ":only-of-type": return buildPseudoNthOfTypeVElementMatcher((index, length) => index === 0 && length === 1);
default: throw new SelectorError(`Unsupported pseudo selector: ${pseudo}.`);
}
}
/**
* Convert :has() selector nodes to VElementMatcher
* @param {parser.Selector[]} selectorNodes
* @returns {VElementMatcher}
*/
function pseudoHasSelectorsToVElementMatcher(selectorNodes) {
const selectors = selectorNodes.map((n) => pseudoHasSelectorToVElementMatcher(n));
return (element, subject) => selectors.some((sel) => sel(element, subject));
}
/**
* Convert :has() selector node to VElementMatcher
* @param {parser.Selector} selector
* @returns {VElementMatcher}
*/
function pseudoHasSelectorToVElementMatcher(selector) {
const nodes = cleanSelectorChildren(selector);
const selectors = selectorToVElementMatcher(nodes);
const firstNode = nodes[0];
if (firstNode.type === "combinator" && (firstNode.value === "+" || firstNode.value === "~")) return buildVElementMatcher(selectors, (element) => getAfterElements(element));
return buildVElementMatcher(selectors, (element) => element.children.filter(isVElement));
}
/**
* @param {VElementMatcher} selectors
* @param {(element: VElement) => VElement[]} getStartElements
* @returns {VElementMatcher}
*/
function buildVElementMatcher(selectors, getStartElements) {
return (element) => {
const elements = [...getStartElements(element)];
/** @type {VElement|undefined} */
let curr;
while (curr = elements.shift()) {
const el = curr;
if (selectors(el, element)) return true;
elements.push(...el.children.filter(isVElement));
}
return false;
};
}
/**
* Parse <nth>
* @param {parser.Pseudo} pseudoNode
* @returns {(index: number)=>boolean}
*/
function parseNth(pseudoNode) {
const argumentsText = pseudoNode.toString().slice(pseudoNode.value.length).toLowerCase();
const openParenIndex = argumentsText.indexOf("(");
const closeParenIndex = argumentsText.lastIndexOf(")");
if (openParenIndex === -1 || closeParenIndex === -1) throw new SelectorError(`Cannot parse An+B micro syntax (:nth-xxx() argument): ${argumentsText}.`);
const argument = argumentsText.slice(openParenIndex + 1, closeParenIndex).trim();
try {
return nthCheck(argument);
} catch {
throw new SelectorError(`Cannot parse An+B micro syntax (:nth-xxx() argument): '${argument}'.`);
}
}
/**
* Build VElementMatcher for :nth-xxx()
* @param {(index: number, length: number)=>boolean} testIndex
* @returns {VElementMatcher}
*/
function buildPseudoNthVElementMatcher(testIndex) {
return (element) => {
const elements = element.parent.children.filter(isVElement);
return testIndex(elements.indexOf(element), elements.length);
};
}
/**
* Build VElementMatcher for :nth-xxx-of-type()
* @param {(index: number, length: number)=>boolean} testIndex
* @returns {VElementMatcher}
*/
function buildPseudoNthOfTypeVElementMatcher(testIndex) {
return (element) => {
const elements = element.parent.children.filter(
/** @returns {e is VElement} */
(e) => isVElement(e) && e.rawName === element.rawName
);
return testIndex(elements.indexOf(element), elements.length);
};
}
/**
* @param {VElement} element
*/
function getBeforeElement(element) {
return getBeforeElements(element).pop() || null;
}
/**
* @param {VElement} element
*/
function getBeforeElements(element) {
const parent = element.parent;
const index = parent.children.indexOf(element);
return parent.children.slice(0, index).filter(isVElement);
}
/**
* @param {VElement} element
*/
function getAfterElements(element) {
const parent = element.parent;
const index = parent.children.indexOf(element);
return parent.children.slice(index + 1).filter(isVElement);
}
/**
* @param {VElementMatcher} a
* @param {VElementMatcher} b
* @returns {VElementMatcher}
*/
function compound(a, b) {
return (element, subject) => a(element, subject) && b(element, subject);
}
/**
* Get attribute value from given element.
* @param {VElement} element The element node.
* @param {string} attribute The attribute name.
*/
function getAttributeValue(element, attribute) {
const attr = getAttribute(element, attribute);
if (attr) return attr.value && attr.value.value || "";
return null;
}
}));
//#endregion
Object.defineProperty(exports, 'default', {
enumerable: true,
get: function () {
return require_selector();
}
});