286 lines
12 KiB
JavaScript
286 lines
12 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
function _export(target, all) {
|
|
for(var name in all)Object.defineProperty(target, name, {
|
|
enumerable: true,
|
|
get: all[name]
|
|
});
|
|
}
|
|
_export(exports, {
|
|
selectorFunctions: ()=>selectorFunctions,
|
|
formatVariantSelector: ()=>formatVariantSelector,
|
|
finalizeSelector: ()=>finalizeSelector
|
|
});
|
|
const _postcssSelectorParser = /*#__PURE__*/ _interopRequireDefault(require("postcss-selector-parser"));
|
|
const _unesc = /*#__PURE__*/ _interopRequireDefault(require("postcss-selector-parser/dist/util/unesc"));
|
|
const _escapeClassName = /*#__PURE__*/ _interopRequireDefault(require("../util/escapeClassName"));
|
|
const _prefixSelector = /*#__PURE__*/ _interopRequireDefault(require("../util/prefixSelector"));
|
|
function _interopRequireDefault(obj) {
|
|
return obj && obj.__esModule ? obj : {
|
|
default: obj
|
|
};
|
|
}
|
|
var ref;
|
|
let MERGE = ":merge";
|
|
let PARENT = "&";
|
|
let selectorFunctions = new Set([
|
|
MERGE
|
|
]);
|
|
function formatVariantSelector(current, ...others) {
|
|
for (let other of others){
|
|
let incomingValue = resolveFunctionArgument(other, MERGE);
|
|
if (incomingValue !== null) {
|
|
let existingValue = resolveFunctionArgument(current, MERGE, incomingValue);
|
|
if (existingValue !== null) {
|
|
let existingTarget = `${MERGE}(${incomingValue})`;
|
|
let splitIdx = other.indexOf(existingTarget);
|
|
let addition = other.slice(splitIdx + existingTarget.length).split(" ")[0];
|
|
current = current.replace(existingTarget, existingTarget + addition);
|
|
continue;
|
|
}
|
|
}
|
|
current = other.replace(PARENT, current);
|
|
}
|
|
return current;
|
|
}
|
|
/**
|
|
* Given any node in a selector this gets the "simple" selector it's a part of
|
|
* A simple selector is just a list of nodes without any combinators
|
|
* Technically :is(), :not(), :has(), etc… can have combinators but those are nested
|
|
* inside the relevant node and won't be picked up so they're fine to ignore
|
|
*
|
|
* @param {import('postcss-selector-parser').Node} node
|
|
* @returns {import('postcss-selector-parser').Node[]}
|
|
**/ function simpleSelectorForNode(node) {
|
|
/** @type {import('postcss-selector-parser').Node[]} */ let nodes = [];
|
|
// Walk backwards until we hit a combinator node (or the start)
|
|
while(node.prev() && node.prev().type !== "combinator"){
|
|
node = node.prev();
|
|
}
|
|
// Now record all non-combinator nodes until we hit one (or the end)
|
|
while(node && node.type !== "combinator"){
|
|
nodes.push(node);
|
|
node = node.next();
|
|
}
|
|
return nodes;
|
|
}
|
|
/**
|
|
* Resorts the nodes in a selector to ensure they're in the correct order
|
|
* Tags go before classes, and pseudo classes go after classes
|
|
*
|
|
* @param {import('postcss-selector-parser').Selector} sel
|
|
* @returns {import('postcss-selector-parser').Selector}
|
|
**/ function resortSelector(sel) {
|
|
sel.sort((a, b)=>{
|
|
if (a.type === "tag" && b.type === "class") {
|
|
return -1;
|
|
} else if (a.type === "class" && b.type === "tag") {
|
|
return 1;
|
|
} else if (a.type === "class" && b.type === "pseudo" && b.value !== ":merge") {
|
|
return -1;
|
|
} else if (a.type === "pseudo" && a.value !== ":merge" && b.type === "class") {
|
|
return 1;
|
|
}
|
|
return sel.index(a) - sel.index(b);
|
|
});
|
|
return sel;
|
|
}
|
|
function eliminateIrrelevantSelectors(sel, base) {
|
|
let hasClassesMatchingCandidate = false;
|
|
sel.walk((child)=>{
|
|
if (child.type === "class" && child.value === base) {
|
|
hasClassesMatchingCandidate = true;
|
|
return false // Stop walking
|
|
;
|
|
}
|
|
});
|
|
if (!hasClassesMatchingCandidate) {
|
|
sel.remove();
|
|
}
|
|
// We do NOT recursively eliminate sub selectors that don't have the base class
|
|
// as this is NOT a safe operation. For example, if we have:
|
|
// `.space-x-2 > :not([hidden]) ~ :not([hidden])`
|
|
// We cannot remove the [hidden] from the :not() because it would change the
|
|
// meaning of the selector.
|
|
// TODO: Can we do this for :matches, :is, and :where?
|
|
}
|
|
var ref1;
|
|
function finalizeSelector(format, { selector , candidate , context , isArbitraryVariant , // Split by the separator, but ignore the separator inside square brackets:
|
|
//
|
|
// E.g.: dark:lg:hover:[paint-order:markers]
|
|
// ┬ ┬ ┬ ┬
|
|
// │ │ │ ╰── We will not split here
|
|
// ╰──┴─────┴─────────────── We will split here
|
|
//
|
|
base =candidate.split(new RegExp(`\\${(ref1 = context === null || context === void 0 ? void 0 : (ref = context.tailwindConfig) === null || ref === void 0 ? void 0 : ref.separator) !== null && ref1 !== void 0 ? ref1 : ":"}(?![^[]*\\])`)).pop() , }) {
|
|
var ref2;
|
|
let ast = (0, _postcssSelectorParser.default)().astSync(selector);
|
|
// We explicitly DO NOT prefix classes in arbitrary variants
|
|
if ((context === null || context === void 0 ? void 0 : (ref2 = context.tailwindConfig) === null || ref2 === void 0 ? void 0 : ref2.prefix) && !isArbitraryVariant) {
|
|
format = (0, _prefixSelector.default)(context.tailwindConfig.prefix, format);
|
|
}
|
|
format = format.replace(PARENT, `.${(0, _escapeClassName.default)(candidate)}`);
|
|
let formatAst = (0, _postcssSelectorParser.default)().astSync(format);
|
|
// Remove extraneous selectors that do not include the base class/candidate being matched against
|
|
// For example if we have a utility defined `.a, .b { color: red}`
|
|
// And the formatted variant is sm:b then we want the final selector to be `.sm\:b` and not `.a, .sm\:b`
|
|
ast.each((sel)=>eliminateIrrelevantSelectors(sel, base));
|
|
// Normalize escaped classes, e.g.:
|
|
//
|
|
// The idea would be to replace the escaped `base` in the selector with the
|
|
// `format`. However, in css you can escape the same selector in a few
|
|
// different ways. This would result in different strings and therefore we
|
|
// can't replace it properly.
|
|
//
|
|
// base: bg-[rgb(255,0,0)]
|
|
// base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
|
|
// escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
|
|
//
|
|
ast.walkClasses((node)=>{
|
|
if (node.raws && node.value.includes(base)) {
|
|
node.raws.value = (0, _escapeClassName.default)((0, _unesc.default)(node.raws.value));
|
|
}
|
|
});
|
|
let simpleStart = _postcssSelectorParser.default.comment({
|
|
value: "/*__simple__*/"
|
|
});
|
|
let simpleEnd = _postcssSelectorParser.default.comment({
|
|
value: "/*__simple__*/"
|
|
});
|
|
// We can safely replace the escaped base now, since the `base` section is
|
|
// now in a normalized escaped value.
|
|
ast.walkClasses((node)=>{
|
|
if (node.value !== base) {
|
|
return;
|
|
}
|
|
let parent = node.parent;
|
|
let formatNodes = formatAst.nodes[0].nodes;
|
|
// Perf optimization: if the parent is a single class we can just replace it and be done
|
|
if (parent.nodes.length === 1) {
|
|
node.replaceWith(...formatNodes);
|
|
return;
|
|
}
|
|
let simpleSelector = simpleSelectorForNode(node);
|
|
parent.insertBefore(simpleSelector[0], simpleStart);
|
|
parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd);
|
|
for (let child of formatNodes){
|
|
parent.insertBefore(simpleSelector[0], child);
|
|
}
|
|
node.remove();
|
|
// Re-sort the simple selector to ensure it's in the correct order
|
|
simpleSelector = simpleSelectorForNode(simpleStart);
|
|
let firstNode = parent.index(simpleStart);
|
|
parent.nodes.splice(firstNode, simpleSelector.length, ...resortSelector(_postcssSelectorParser.default.selector({
|
|
nodes: simpleSelector
|
|
})).nodes);
|
|
simpleStart.remove();
|
|
simpleEnd.remove();
|
|
});
|
|
// This will make sure to move pseudo's to the correct spot (the end for
|
|
// pseudo elements) because otherwise the selector will never work
|
|
// anyway.
|
|
//
|
|
// E.g.:
|
|
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
|
|
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
|
|
//
|
|
// `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
|
|
function collectPseudoElements(selector) {
|
|
let nodes = [];
|
|
for (let node of selector.nodes){
|
|
if (isPseudoElement(node)) {
|
|
nodes.push(node);
|
|
selector.removeChild(node);
|
|
}
|
|
if (node === null || node === void 0 ? void 0 : node.nodes) {
|
|
nodes.push(...collectPseudoElements(node));
|
|
}
|
|
}
|
|
return nodes;
|
|
}
|
|
// Remove unnecessary pseudo selectors that we used as placeholders
|
|
ast.each((selector)=>{
|
|
selector.walkPseudos((p)=>{
|
|
if (selectorFunctions.has(p.value)) {
|
|
p.replaceWith(p.nodes);
|
|
}
|
|
});
|
|
let pseudoElements = collectPseudoElements(selector);
|
|
if (pseudoElements.length > 0) {
|
|
selector.nodes.push(pseudoElements.sort(sortSelector));
|
|
}
|
|
});
|
|
return ast.toString();
|
|
}
|
|
// Note: As a rule, double colons (::) should be used instead of a single colon
|
|
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
|
|
// this distinction was not present in older versions of the W3C spec, most
|
|
// browsers support both syntaxes for the original pseudo-elements.
|
|
let pseudoElementsBC = [
|
|
":before",
|
|
":after",
|
|
":first-line",
|
|
":first-letter"
|
|
];
|
|
// These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
|
|
let pseudoElementExceptions = [
|
|
"::file-selector-button"
|
|
];
|
|
// This will make sure to move pseudo's to the correct spot (the end for
|
|
// pseudo elements) because otherwise the selector will never work
|
|
// anyway.
|
|
//
|
|
// E.g.:
|
|
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
|
|
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
|
|
//
|
|
// `::before:hover` doesn't work, which means that we can make it work
|
|
// for you by flipping the order.
|
|
function sortSelector(a, z) {
|
|
// Both nodes are non-pseudo's so we can safely ignore them and keep
|
|
// them in the same order.
|
|
if (a.type !== "pseudo" && z.type !== "pseudo") {
|
|
return 0;
|
|
}
|
|
// If one of them is a combinator, we need to keep it in the same order
|
|
// because that means it will start a new "section" in the selector.
|
|
if (a.type === "combinator" ^ z.type === "combinator") {
|
|
return 0;
|
|
}
|
|
// One of the items is a pseudo and the other one isn't. Let's move
|
|
// the pseudo to the right.
|
|
if (a.type === "pseudo" ^ z.type === "pseudo") {
|
|
return (a.type === "pseudo") - (z.type === "pseudo");
|
|
}
|
|
// Both are pseudo's, move the pseudo elements (except for
|
|
// ::file-selector-button) to the right.
|
|
return isPseudoElement(a) - isPseudoElement(z);
|
|
}
|
|
function isPseudoElement(node) {
|
|
if (node.type !== "pseudo") return false;
|
|
if (pseudoElementExceptions.includes(node.value)) return false;
|
|
return node.value.startsWith("::") || pseudoElementsBC.includes(node.value);
|
|
}
|
|
function resolveFunctionArgument(haystack, needle, arg) {
|
|
let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle);
|
|
if (startIdx === -1) return null;
|
|
// Start inside the `(`
|
|
startIdx += needle.length + 1;
|
|
let target = "";
|
|
let count = 0;
|
|
for (let char of haystack.slice(startIdx)){
|
|
if (char !== "(" && char !== ")") {
|
|
target += char;
|
|
} else if (char === "(") {
|
|
target += char;
|
|
count++;
|
|
} else if (char === ")") {
|
|
if (--count < 0) break; // unbalanced
|
|
target += char;
|
|
}
|
|
}
|
|
return target;
|
|
}
|