diff options
| author | Philipp Tanlak <philipp.tanlak@gmail.com> | 2025-11-24 20:54:57 +0100 |
|---|---|---|
| committer | Philipp Tanlak <philipp.tanlak@gmail.com> | 2025-11-24 20:57:48 +0100 |
| commit | b1e2c8fd5cb5dfa46bc440a12eafaf56cd844b1c (patch) | |
| tree | 49d360fd6cbc6a2754efe93524ac47ff0fbe0f7d /node_modules/tailwindcss/lib/util/formatVariantSelector.js | |
Docs
Diffstat (limited to 'node_modules/tailwindcss/lib/util/formatVariantSelector.js')
| -rw-r--r-- | node_modules/tailwindcss/lib/util/formatVariantSelector.js | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/node_modules/tailwindcss/lib/util/formatVariantSelector.js b/node_modules/tailwindcss/lib/util/formatVariantSelector.js new file mode 100644 index 0000000..8e778e8 --- /dev/null +++ b/node_modules/tailwindcss/lib/util/formatVariantSelector.js @@ -0,0 +1,270 @@ +"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 + ]; +} |