mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
239 lines
6.8 KiB
JavaScript
239 lines
6.8 KiB
JavaScript
const postcss = require('postcss')
|
|
const { resolveMatches } = require('./generateRules')
|
|
const { bigSign, escapeClassName } = require('./utils')
|
|
|
|
function buildApplyCache(applyCandidates, context) {
|
|
for (let candidate of applyCandidates) {
|
|
if (context.notClassCache.has(candidate) || context.applyClassCache.has(candidate)) {
|
|
continue
|
|
}
|
|
|
|
if (context.classCache.has(candidate)) {
|
|
context.applyClassCache.set(
|
|
candidate,
|
|
context.classCache.get(candidate).map(([meta, rule]) => [meta, rule.clone()])
|
|
)
|
|
continue
|
|
}
|
|
|
|
let matches = Array.from(resolveMatches(candidate, context))
|
|
|
|
if (matches.length === 0) {
|
|
context.notClassCache.add(candidate)
|
|
continue
|
|
}
|
|
|
|
context.applyClassCache.set(candidate, matches)
|
|
}
|
|
|
|
return context.applyClassCache
|
|
}
|
|
|
|
// TODO: Apply `!important` stuff correctly instead of just skipping it
|
|
function extractApplyCandidates(params) {
|
|
let candidates = params.split(/[\s\t\n]+/g)
|
|
|
|
if (candidates[candidates.length - 1] === '!important') {
|
|
return [candidates.slice(0, -1), true]
|
|
}
|
|
|
|
return [candidates, false]
|
|
}
|
|
|
|
function partitionApplyParents(root) {
|
|
let applyParents = new Set()
|
|
|
|
root.walkAtRules('apply', (rule) => {
|
|
applyParents.add(rule.parent)
|
|
})
|
|
|
|
for (let rule of applyParents) {
|
|
let nodeGroups = []
|
|
let lastGroup = []
|
|
|
|
for (let node of rule.nodes) {
|
|
if (node.type === 'atrule' && node.name === 'apply') {
|
|
if (lastGroup.length > 0) {
|
|
nodeGroups.push(lastGroup)
|
|
lastGroup = []
|
|
}
|
|
nodeGroups.push([node])
|
|
} else {
|
|
lastGroup.push(node)
|
|
}
|
|
}
|
|
|
|
if (lastGroup.length > 0) {
|
|
nodeGroups.push(lastGroup)
|
|
}
|
|
|
|
if (nodeGroups.length === 1) {
|
|
continue
|
|
}
|
|
|
|
for (let group of [...nodeGroups].reverse()) {
|
|
let newParent = rule.clone({ nodes: [] })
|
|
newParent.append(group)
|
|
rule.after(newParent)
|
|
}
|
|
|
|
rule.remove()
|
|
}
|
|
}
|
|
|
|
function processApply(root, context) {
|
|
let applyCandidates = new Set()
|
|
|
|
// Collect all @apply rules and candidates
|
|
let applies = []
|
|
root.walkAtRules('apply', (rule) => {
|
|
let [candidates] = extractApplyCandidates(rule.params)
|
|
|
|
for (let util of candidates) {
|
|
applyCandidates.add(util)
|
|
}
|
|
applies.push(rule)
|
|
})
|
|
|
|
// Start the @apply process if we have rules with @apply in them
|
|
if (applies.length > 0) {
|
|
// Fill up some caches!
|
|
let applyClassCache = buildApplyCache(applyCandidates, context)
|
|
|
|
/**
|
|
* When we have an apply like this:
|
|
*
|
|
* .abc {
|
|
* @apply hover:font-bold;
|
|
* }
|
|
*
|
|
* What we essentially will do is resolve to this:
|
|
*
|
|
* .abc {
|
|
* @apply .hover\:font-bold:hover {
|
|
* font-weight: 500;
|
|
* }
|
|
* }
|
|
*
|
|
* Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
|
|
* What happens in this function is that we prepend a `.` and escape the candidate.
|
|
* This will result in `.hover\:font-bold`
|
|
* Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
|
|
*/
|
|
// TODO: Should we use postcss-selector-parser for this instead?
|
|
function replaceSelector(selector, utilitySelectors, candidate) {
|
|
let needle = `.${escapeClassName(candidate)}`
|
|
let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g)
|
|
|
|
return selector
|
|
.split(/\s*,\s*/g)
|
|
.map((s) => {
|
|
let replaced = []
|
|
|
|
for (let utilitySelector of utilitySelectorsList) {
|
|
let replacedSelector = utilitySelector.replace(needle, s)
|
|
if (replacedSelector === utilitySelector) {
|
|
continue
|
|
}
|
|
replaced.push(replacedSelector)
|
|
}
|
|
return replaced.join(', ')
|
|
})
|
|
.join(', ')
|
|
}
|
|
|
|
/** @type {Map<import('postcss').Node, [string, boolean, import('postcss').Node[]][]>} */
|
|
let perParentApplies = new Map()
|
|
|
|
// Collect all apply candidates and their rules
|
|
for (let apply of applies) {
|
|
let candidates = perParentApplies.get(apply.parent) || []
|
|
|
|
perParentApplies.set(apply.parent, candidates)
|
|
|
|
let [applyCandidates, important] = extractApplyCandidates(apply.params)
|
|
|
|
if (apply.parent.type === 'atrule') {
|
|
if (apply.parent.name === 'screen') {
|
|
const screenType = apply.parent.params
|
|
|
|
throw apply.error(
|
|
`@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates
|
|
.map((c) => `${screenType}:${c}`)
|
|
.join(' ')} instead.`
|
|
)
|
|
}
|
|
|
|
throw apply.error(
|
|
`@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.`
|
|
)
|
|
}
|
|
|
|
for (let applyCandidate of applyCandidates) {
|
|
if (!applyClassCache.has(applyCandidate)) {
|
|
throw apply.error(
|
|
`The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
|
|
)
|
|
}
|
|
|
|
let rules = applyClassCache.get(applyCandidate)
|
|
|
|
candidates.push([applyCandidate, important, rules])
|
|
}
|
|
}
|
|
|
|
for (const [parent, candidates] of perParentApplies) {
|
|
let siblings = []
|
|
|
|
for (let [applyCandidate, important, rules] of candidates) {
|
|
for (let [meta, node] of rules) {
|
|
let root = postcss.root({ nodes: [node.clone()] })
|
|
let canRewriteSelector =
|
|
node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
|
|
|
|
if (canRewriteSelector) {
|
|
root.walkRules((rule) => {
|
|
rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)
|
|
|
|
rule.walkDecls((d) => {
|
|
d.important = important
|
|
})
|
|
})
|
|
}
|
|
|
|
siblings.push([meta, root.nodes[0]])
|
|
}
|
|
}
|
|
|
|
// Inject the rules, sorted, correctly
|
|
let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
|
|
|
|
// console.log(parent)
|
|
// `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
|
|
parent.after(nodes)
|
|
}
|
|
|
|
for (let apply of applies) {
|
|
// If there are left-over declarations, just remove the @apply
|
|
if (apply.parent.nodes.length > 1) {
|
|
apply.remove()
|
|
} else {
|
|
// The node is empty, drop the full node
|
|
apply.parent.remove()
|
|
}
|
|
}
|
|
|
|
// Do it again, in case we have other `@apply` rules
|
|
processApply(root, context)
|
|
}
|
|
}
|
|
|
|
function expandApplyAtRules(context) {
|
|
return (root) => {
|
|
partitionApplyParents(root)
|
|
processApply(root, context)
|
|
}
|
|
}
|
|
|
|
module.exports = expandApplyAtRules
|