|
import postcss from 'postcss' |
|
import parser from 'postcss-selector-parser' |
|
|
|
import { resolveMatches } from './generateRules' |
|
import escapeClassName from '../util/escapeClassName' |
|
import { applyImportantSelector } from '../util/applyImportantSelector' |
|
import { movePseudos } from '../util/pseudoElements' |
|
|
|
|
|
|
|
function extractClasses(node) { |
|
|
|
let groups = new Map() |
|
|
|
let container = postcss.root({ nodes: [node.clone()] }) |
|
|
|
container.walkRules((rule) => { |
|
parser((selectors) => { |
|
selectors.walkClasses((classSelector) => { |
|
let parentSelector = classSelector.parent.toString() |
|
|
|
let classes = groups.get(parentSelector) |
|
if (!classes) { |
|
groups.set(parentSelector, (classes = new Set())) |
|
} |
|
|
|
classes.add(classSelector.value) |
|
}) |
|
}).processSync(rule.selector) |
|
}) |
|
|
|
let normalizedGroups = Array.from(groups.values(), (classes) => Array.from(classes)) |
|
let classes = normalizedGroups.flat() |
|
|
|
return Object.assign(classes, { groups: normalizedGroups }) |
|
} |
|
|
|
let selectorExtractor = parser() |
|
|
|
|
|
|
|
|
|
function extractSelectors(ruleSelectors) { |
|
return selectorExtractor.astSync(ruleSelectors) |
|
} |
|
|
|
function extractBaseCandidates(candidates, separator) { |
|
let baseClasses = new Set() |
|
|
|
for (let candidate of candidates) { |
|
baseClasses.add(candidate.split(separator).pop()) |
|
} |
|
|
|
return Array.from(baseClasses) |
|
} |
|
|
|
function prefix(context, selector) { |
|
let prefix = context.tailwindConfig.prefix |
|
return typeof prefix === 'function' ? prefix(selector) : prefix + selector |
|
} |
|
|
|
function* pathToRoot(node) { |
|
yield node |
|
while (node.parent) { |
|
yield node.parent |
|
node = node.parent |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function shallowClone(node, overrides = {}) { |
|
let children = node.nodes |
|
node.nodes = [] |
|
|
|
let tmp = node.clone(overrides) |
|
|
|
node.nodes = children |
|
|
|
return tmp |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function nestedClone(node) { |
|
for (let parent of pathToRoot(node)) { |
|
if (node === parent) { |
|
continue |
|
} |
|
|
|
if (parent.type === 'root') { |
|
break |
|
} |
|
|
|
node = shallowClone(parent, { |
|
nodes: [node], |
|
}) |
|
} |
|
|
|
return node |
|
} |
|
|
|
|
|
|
|
|
|
function buildLocalApplyCache(root, context) { |
|
|
|
let cache = new Map() |
|
|
|
root.walkRules((rule) => { |
|
|
|
for (let node of pathToRoot(rule)) { |
|
if (node.raws.tailwind?.layer !== undefined) { |
|
return |
|
} |
|
} |
|
|
|
|
|
let container = nestedClone(rule) |
|
let sort = context.offsets.create('user') |
|
|
|
for (let className of extractClasses(rule)) { |
|
let list = cache.get(className) || [] |
|
cache.set(className, list) |
|
|
|
list.push([ |
|
{ |
|
layer: 'user', |
|
sort, |
|
important: false, |
|
}, |
|
container, |
|
]) |
|
} |
|
}) |
|
|
|
return cache |
|
} |
|
|
|
|
|
|
|
|
|
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 |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function lazyCache(buildCacheFn) { |
|
let cache = null |
|
|
|
return { |
|
get: (name) => { |
|
cache = cache || buildCacheFn() |
|
|
|
return cache.get(name) |
|
}, |
|
has: (name) => { |
|
cache = cache || buildCacheFn() |
|
|
|
return cache.has(name) |
|
}, |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function combineCaches(caches) { |
|
return { |
|
get: (name) => caches.flatMap((cache) => cache.get(name) || []), |
|
has: (name) => caches.some((cache) => cache.has(name)), |
|
} |
|
} |
|
|
|
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 processApply(root, context, localCache) { |
|
let applyCandidates = new Set() |
|
|
|
|
|
let applies = [] |
|
root.walkAtRules('apply', (rule) => { |
|
let [candidates] = extractApplyCandidates(rule.params) |
|
|
|
for (let util of candidates) { |
|
applyCandidates.add(util) |
|
} |
|
|
|
applies.push(rule) |
|
}) |
|
|
|
|
|
if (applies.length === 0) { |
|
return |
|
} |
|
|
|
|
|
let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function replaceSelector(selector, utilitySelectors, candidate) { |
|
let selectorList = extractSelectors(selector) |
|
let utilitySelectorsList = extractSelectors(utilitySelectors) |
|
let candidateList = extractSelectors(`.${escapeClassName(candidate)}`) |
|
let candidateClass = candidateList.nodes[0].nodes[0] |
|
|
|
selectorList.each((sel) => { |
|
|
|
let replaced = new Set() |
|
|
|
utilitySelectorsList.each((utilitySelector) => { |
|
let hasReplaced = false |
|
utilitySelector = utilitySelector.clone() |
|
|
|
utilitySelector.walkClasses((node) => { |
|
if (node.value !== candidateClass.value) { |
|
return |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (hasReplaced) { |
|
return |
|
} |
|
|
|
|
|
|
|
|
|
node.replaceWith(...sel.nodes.map((node) => node.clone())) |
|
|
|
|
|
replaced.add(utilitySelector) |
|
|
|
hasReplaced = true |
|
}) |
|
}) |
|
|
|
|
|
|
|
|
|
for (let sel of replaced) { |
|
let groups = [[]] |
|
for (let node of sel.nodes) { |
|
if (node.type === 'combinator') { |
|
groups.push(node) |
|
groups.push([]) |
|
} else { |
|
let last = groups[groups.length - 1] |
|
last.push(node) |
|
} |
|
} |
|
|
|
sel.nodes = [] |
|
|
|
for (let group of groups) { |
|
if (Array.isArray(group)) { |
|
group.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 0 |
|
}) |
|
} |
|
|
|
sel.nodes = sel.nodes.concat(group) |
|
} |
|
} |
|
|
|
sel.replaceWith(...replaced) |
|
}) |
|
|
|
return selectorList.toString() |
|
} |
|
|
|
let perParentApplies = new Map() |
|
|
|
|
|
for (let apply of applies) { |
|
let [candidates] = perParentApplies.get(apply.parent) || [[], apply.source] |
|
|
|
perParentApplies.set(apply.parent, [candidates, apply.source]) |
|
|
|
let [applyCandidates, important] = extractApplyCandidates(apply.params) |
|
|
|
if (apply.parent.type === 'atrule') { |
|
if (apply.parent.name === 'screen') { |
|
let 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 ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) { |
|
|
|
throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`) |
|
} |
|
|
|
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 (let [parent, [candidates, atApplySource]] of perParentApplies) { |
|
let siblings = [] |
|
|
|
for (let [applyCandidate, important, rules] of candidates) { |
|
let potentialApplyCandidates = [ |
|
applyCandidate, |
|
...extractBaseCandidates([applyCandidate], context.tailwindConfig.separator), |
|
] |
|
|
|
for (let [meta, node] of rules) { |
|
let parentClasses = extractClasses(parent) |
|
let nodeClasses = extractClasses(node) |
|
|
|
|
|
|
|
nodeClasses = nodeClasses.groups |
|
.filter((classList) => |
|
classList.some((className) => potentialApplyCandidates.includes(className)) |
|
) |
|
.flat() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodeClasses = nodeClasses.concat( |
|
extractBaseCandidates(nodeClasses, context.tailwindConfig.separator) |
|
) |
|
|
|
let intersects = parentClasses.some((selector) => nodeClasses.includes(selector)) |
|
if (intersects) { |
|
throw node.error( |
|
`You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.` |
|
) |
|
} |
|
|
|
let root = postcss.root({ nodes: [node.clone()] }) |
|
|
|
|
|
root.walk((node) => { |
|
node.source = atApplySource |
|
}) |
|
|
|
let canRewriteSelector = |
|
node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes') |
|
|
|
if (canRewriteSelector) { |
|
root.walkRules((rule) => { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!extractClasses(rule).some((candidate) => candidate === applyCandidate)) { |
|
rule.remove() |
|
return |
|
} |
|
|
|
|
|
let importantSelector = |
|
typeof context.tailwindConfig.important === 'string' |
|
? context.tailwindConfig.important |
|
: null |
|
|
|
|
|
|
|
let isGenerated = parent.raws.tailwind !== undefined |
|
|
|
let parentSelector = |
|
isGenerated && importantSelector && parent.selector.indexOf(importantSelector) === 0 |
|
? parent.selector.slice(importantSelector.length) |
|
: parent.selector |
|
|
|
|
|
|
|
|
|
if (parentSelector === '') { |
|
parentSelector = parent.selector |
|
} |
|
|
|
rule.selector = replaceSelector(parentSelector, rule.selector, applyCandidate) |
|
|
|
|
|
if (importantSelector && parentSelector !== parent.selector) { |
|
rule.selector = applyImportantSelector(rule.selector, importantSelector) |
|
} |
|
|
|
rule.walkDecls((d) => { |
|
d.important = meta.important || important |
|
}) |
|
|
|
|
|
let selector = parser().astSync(rule.selector) |
|
selector.each((sel) => movePseudos(sel)) |
|
rule.selector = selector.toString() |
|
}) |
|
} |
|
|
|
|
|
|
|
if (!root.nodes[0]) { |
|
continue |
|
} |
|
|
|
|
|
siblings.push([meta.sort, root.nodes[0]]) |
|
} |
|
} |
|
|
|
|
|
let nodes = context.offsets.sort(siblings).map((s) => s[1]) |
|
|
|
|
|
parent.after(nodes) |
|
} |
|
|
|
for (let apply of applies) { |
|
|
|
if (apply.parent.nodes.length > 1) { |
|
apply.remove() |
|
} else { |
|
|
|
apply.parent.remove() |
|
} |
|
} |
|
|
|
|
|
processApply(root, context, localCache) |
|
} |
|
|
|
export default function expandApplyAtRules(context) { |
|
return (root) => { |
|
|
|
let localCache = lazyCache(() => buildLocalApplyCache(root, context)) |
|
|
|
processApply(root, context, localCache) |
|
} |
|
} |
|
|