summaryrefslogtreecommitdiff
path: root/node_modules/tailwindcss/src/util/formatVariantSelector.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/tailwindcss/src/util/formatVariantSelector.js')
-rw-r--r--node_modules/tailwindcss/src/util/formatVariantSelector.js324
1 files changed, 324 insertions, 0 deletions
diff --git a/node_modules/tailwindcss/src/util/formatVariantSelector.js b/node_modules/tailwindcss/src/util/formatVariantSelector.js
new file mode 100644
index 0000000..6ba6f2c
--- /dev/null
+++ b/node_modules/tailwindcss/src/util/formatVariantSelector.js
@@ -0,0 +1,324 @@
+import selectorParser from 'postcss-selector-parser'
+import unescape from 'postcss-selector-parser/dist/util/unesc'
+import escapeClassName from '../util/escapeClassName'
+import prefixSelector from '../util/prefixSelector'
+import { movePseudos } from './pseudoElements'
+import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
+
+/** @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'
+
+/**
+ * @param {RawFormats} formats
+ * @param {{context: any, candidate: string, base: string | null}} options
+ * @returns {ParsedFormats | null}
+ */
+export function formatVariantSelector(formats, { context, candidate }) {
+ let prefix = context?.tailwindConfig.prefix ?? ''
+
+ // Parse the format selector into an AST
+ let parsedFormats = formats.map((format) => {
+ let ast = selectorParser().astSync(format.format)
+
+ return {
+ ...format,
+ ast: format.respectPrefix ? prefixSelector(prefix, ast) : ast,
+ }
+ })
+
+ // We start with the candidate selector
+ let formatAst = selectorParser.root({
+ nodes: [
+ selectorParser.selector({
+ nodes: [selectorParser.className({ value: escapeClassName(candidate) })],
+ }),
+ ],
+ })
+
+ // And iteratively merge each format selector into the candidate selector
+ for (let { ast } of parsedFormats) {
+ // 1. Handle :merge() special pseudo-class
+ ;[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
+}
+
+/**
+ * Remove extraneous selectors that do not include the base class/candidate
+ *
+ * Example:
+ * Given the utility `.a, .b { color: red}`
+ * Given the candidate `sm:b`
+ *
+ * The final selector should be `.sm\:b` and not `.a, .sm\:b`
+ *
+ * @param {Selector} ast
+ * @param {string} base
+ */
+export 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?
+}
+
+/**
+ * @param {string} current
+ * @param {AcceptedFormats} formats
+ * @param {{context: any, candidate: string, base: string | null}} options
+ * @returns {string}
+ */
+export function finalizeSelector(current, formats, { context, candidate, base }) {
+ let separator = 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 ?? splitAtTopLevelOnly(candidate, separator).pop()
+
+ // Parse the selector into an AST
+ let selector = selectorParser().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 = escapeClassName(unescape(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 = selectorParser.comment({ value: '/*__simple__*/' })
+ let simpleEnd = selectorParser.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(selectorParser.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) => movePseudos(sel))
+
+ return selector.toString()
+}
+
+/**
+ *
+ * @param {Selector} selector
+ * @param {Selector} format
+ */
+export 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,
+ selectorParser.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]
+}