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.
193 lines
8.1 KiB
193 lines
8.1 KiB
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { |
|
value: true |
|
}); |
|
exports.formatVariantSelector = formatVariantSelector; |
|
exports.finalizeSelector = finalizeSelector; |
|
exports.selectorFunctions = void 0; |
|
var _postcssSelectorParser = _interopRequireDefault(require("postcss-selector-parser")); |
|
var _unesc = _interopRequireDefault(require("postcss-selector-parser/dist/util/unesc")); |
|
var _escapeClassName = _interopRequireDefault(require("../util/escapeClassName")); |
|
var _prefixSelector = _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 |
|
]); |
|
exports.selectorFunctions = selectorFunctions; |
|
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; |
|
} |
|
var ref1; |
|
function finalizeSelector(format, { selector: selector1 , candidate , context , // 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(selector1); |
|
if (context === null || context === void 0 ? void 0 : (ref2 = context.tailwindConfig) === null || ref2 === void 0 ? void 0 : ref2.prefix) { |
|
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((node)=>{ |
|
let hasClassesMatchingCandidate = node.some((n)=>n.type === "class" && n.value === base); |
|
if (!hasClassesMatchingCandidate) { |
|
node.remove(); |
|
} |
|
}); |
|
// 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)); |
|
} |
|
}); |
|
// 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) { |
|
node.replaceWith(...formatAst.nodes); |
|
} |
|
}); |
|
// 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; |
|
}
|
|
|