271 lines
11 KiB
JavaScript
271 lines
11 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, {
|
||
|
formatVariantSelector: function() {
|
||
|
return formatVariantSelector;
|
||
|
},
|
||
|
eliminateIrrelevantSelectors: function() {
|
||
|
return eliminateIrrelevantSelectors;
|
||
|
},
|
||
|
finalizeSelector: function() {
|
||
|
return finalizeSelector;
|
||
|
},
|
||
|
handleMergePseudo: function() {
|
||
|
return handleMergePseudo;
|
||
|
}
|
||
|
});
|
||
|
const _postcssselectorparser = /*#__PURE__*/ _interop_require_default(require("postcss-selector-parser"));
|
||
|
const _unesc = /*#__PURE__*/ _interop_require_default(require("postcss-selector-parser/dist/util/unesc"));
|
||
|
const _escapeClassName = /*#__PURE__*/ _interop_require_default(require("../util/escapeClassName"));
|
||
|
const _prefixSelector = /*#__PURE__*/ _interop_require_default(require("../util/prefixSelector"));
|
||
|
const _pseudoElements = require("./pseudoElements");
|
||
|
const _splitAtTopLevelOnly = require("./splitAtTopLevelOnly");
|
||
|
function _interop_require_default(obj) {
|
||
|
return obj && obj.__esModule ? obj : {
|
||
|
default: obj
|
||
|
};
|
||
|
}
|
||
|
/** @typedef {import('postcss-selector-parser').Root} Root */ /** @typedef {import('postcss-selector-parser').Selector} Selector */ /** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */ /** @typedef {import('postcss-selector-parser').Node} Node */ /** @typedef {{format: string, respectPrefix: boolean}[]} RawFormats */ /** @typedef {import('postcss-selector-parser').Root} ParsedFormats */ /** @typedef {RawFormats | ParsedFormats} AcceptedFormats */ let MERGE = ":merge";
|
||
|
function formatVariantSelector(formats, { context , candidate }) {
|
||
|
var _context_tailwindConfig_prefix;
|
||
|
let prefix = (_context_tailwindConfig_prefix = context === null || context === void 0 ? void 0 : context.tailwindConfig.prefix) !== null && _context_tailwindConfig_prefix !== void 0 ? _context_tailwindConfig_prefix : "";
|
||
|
// Parse the format selector into an AST
|
||
|
let parsedFormats = formats.map((format)=>{
|
||
|
let ast = (0, _postcssselectorparser.default)().astSync(format.format);
|
||
|
return {
|
||
|
...format,
|
||
|
ast: format.respectPrefix ? (0, _prefixSelector.default)(prefix, ast) : ast
|
||
|
};
|
||
|
});
|
||
|
// We start with the candidate selector
|
||
|
let formatAst = _postcssselectorparser.default.root({
|
||
|
nodes: [
|
||
|
_postcssselectorparser.default.selector({
|
||
|
nodes: [
|
||
|
_postcssselectorparser.default.className({
|
||
|
value: (0, _escapeClassName.default)(candidate)
|
||
|
})
|
||
|
]
|
||
|
})
|
||
|
]
|
||
|
});
|
||
|
// And iteratively merge each format selector into the candidate selector
|
||
|
for (let { ast } of parsedFormats){
|
||
|
[formatAst, ast] = handleMergePseudo(formatAst, ast);
|
||
|
// 2. Merge the format selector into the current selector AST
|
||
|
ast.walkNesting((nesting)=>nesting.replaceWith(...formatAst.nodes[0].nodes));
|
||
|
// 3. Keep going!
|
||
|
formatAst = ast;
|
||
|
}
|
||
|
return formatAst;
|
||
|
}
|
||
|
/**
|
||
|
* 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 {Node} node
|
||
|
* @returns {Node[]}
|
||
|
**/ function simpleSelectorForNode(node) {
|
||
|
/** @type {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 {Selector} sel
|
||
|
* @returns {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.startsWith("::")) {
|
||
|
return -1;
|
||
|
} else if (a.type === "pseudo" && a.value.startsWith("::") && 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?
|
||
|
}
|
||
|
function finalizeSelector(current, formats, { context , candidate , base }) {
|
||
|
var _context_tailwindConfig;
|
||
|
var _context_tailwindConfig_separator;
|
||
|
let separator = (_context_tailwindConfig_separator = context === null || context === void 0 ? void 0 : (_context_tailwindConfig = context.tailwindConfig) === null || _context_tailwindConfig === void 0 ? void 0 : _context_tailwindConfig.separator) !== null && _context_tailwindConfig_separator !== void 0 ? _context_tailwindConfig_separator : ":";
|
||
|
// 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 = base !== null && base !== void 0 ? base : (0, _splitAtTopLevelOnly.splitAtTopLevelOnly)(candidate, separator).pop();
|
||
|
// Parse the selector into an AST
|
||
|
let selector = (0, _postcssselectorparser.default)().astSync(current);
|
||
|
// 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\\)\\]
|
||
|
//
|
||
|
selector.walkClasses((node)=>{
|
||
|
if (node.raws && node.value.includes(base)) {
|
||
|
node.raws.value = (0, _escapeClassName.default)((0, _unesc.default)(node.raws.value));
|
||
|
}
|
||
|
});
|
||
|
// Remove extraneous selectors that do not include the base candidate
|
||
|
selector.each((sel)=>eliminateIrrelevantSelectors(sel, base));
|
||
|
// If ffter eliminating irrelevant selectors, we end up with nothing
|
||
|
// Then the whole "rule" this is associated with does not need to exist
|
||
|
// We use `null` as a marker value for that case
|
||
|
if (selector.length === 0) {
|
||
|
return null;
|
||
|
}
|
||
|
// If there are no formats that means there were no variants added to the candidate
|
||
|
// so we can just return the selector as-is
|
||
|
let formatAst = Array.isArray(formats) ? formatVariantSelector(formats, {
|
||
|
context,
|
||
|
candidate
|
||
|
}) : formats;
|
||
|
if (formatAst === null) {
|
||
|
return selector.toString();
|
||
|
}
|
||
|
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.
|
||
|
selector.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.clone());
|
||
|
}
|
||
|
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();
|
||
|
});
|
||
|
// Remove unnecessary pseudo selectors that we used as placeholders
|
||
|
selector.walkPseudos((p)=>{
|
||
|
if (p.value === MERGE) {
|
||
|
p.replaceWith(p.nodes);
|
||
|
}
|
||
|
});
|
||
|
// Move pseudo elements to the end of the selector (if necessary)
|
||
|
selector.each((sel)=>(0, _pseudoElements.movePseudos)(sel));
|
||
|
return selector.toString();
|
||
|
}
|
||
|
function handleMergePseudo(selector, format) {
|
||
|
/** @type {{pseudo: Pseudo, value: string}[]} */ let merges = [];
|
||
|
// Find all :merge() pseudo-classes in `selector`
|
||
|
selector.walkPseudos((pseudo)=>{
|
||
|
if (pseudo.value === MERGE) {
|
||
|
merges.push({
|
||
|
pseudo,
|
||
|
value: pseudo.nodes[0].toString()
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
// Find all :merge() "attachments" in `format` and attach them to the matching selector in `selector`
|
||
|
format.walkPseudos((pseudo)=>{
|
||
|
if (pseudo.value !== MERGE) {
|
||
|
return;
|
||
|
}
|
||
|
let value = pseudo.nodes[0].toString();
|
||
|
// Does `selector` contain a :merge() pseudo-class with the same value?
|
||
|
let existing = merges.find((merge)=>merge.value === value);
|
||
|
// Nope so there's nothing to do
|
||
|
if (!existing) {
|
||
|
return;
|
||
|
}
|
||
|
// Everything after `:merge()` up to the next combinator is what is attached to the merged selector
|
||
|
let attachments = [];
|
||
|
let next = pseudo.next();
|
||
|
while(next && next.type !== "combinator"){
|
||
|
attachments.push(next);
|
||
|
next = next.next();
|
||
|
}
|
||
|
let combinator = next;
|
||
|
existing.pseudo.parent.insertAfter(existing.pseudo, _postcssselectorparser.default.selector({
|
||
|
nodes: attachments.map((node)=>node.clone())
|
||
|
}));
|
||
|
pseudo.remove();
|
||
|
attachments.forEach((node)=>node.remove());
|
||
|
// What about this case:
|
||
|
// :merge(.group):focus > &
|
||
|
// :merge(.group):hover &
|
||
|
if (combinator && combinator.type === "combinator") {
|
||
|
combinator.remove();
|
||
|
}
|
||
|
});
|
||
|
return [
|
||
|
selector,
|
||
|
format
|
||
|
];
|
||
|
}
|