Skip to content
This repository has been archived by the owner on Apr 6, 2021. It is now read-only.

Commit

Permalink
Respect apply sibling order (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamwathan authored Mar 25, 2021
1 parent a718c06 commit 4a55876
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 108 deletions.
257 changes: 152 additions & 105 deletions src/lib/expandApplyAtRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,135 +40,182 @@ function extractApplyCandidates(params) {
return [candidates, false]
}

function expandApplyAtRules(context) {
return function processApply(root) {
let applyCandidates = new Set()
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)
}
}

// Collect all @apply rules and candidates
let applies = []
root.walkAtRules('apply', (rule) => {
let [candidates, important] = extractApplyCandidates(rule.params)
if (lastGroup.length > 0) {
nodeGroups.push(lastGroup)
}

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(', ')
}
if (nodeGroups.length === 1) {
continue
}

/** @type {Map<import('postcss').Node, [string, boolean, import('postcss').Node[]][]>} */
let perParentApplies = new Map()
for (let group of [...nodeGroups].reverse()) {
let newParent = rule.clone({ nodes: [] })
newParent.append(group)
rule.after(newParent)
}

// Collect all apply candidates and their rules
for (let apply of applies) {
let candidates = perParentApplies.get(apply.parent) || []
rule.remove()
}
}

perParentApplies.set(apply.parent, candidates)
function processApply(root, context) {
let applyCandidates = new Set()

let [applyCandidates, important] = extractApplyCandidates(apply.params)
// Collect all @apply rules and candidates
let applies = []
root.walkAtRules('apply', (rule) => {
let [candidates] = extractApplyCandidates(rule.params)

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.`
)
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) || []

let rules = applyClassCache.get(applyCandidate)
perParentApplies.set(apply.parent, candidates)

candidates.push([applyCandidate, important, rules])
let [applyCandidates, important] = extractApplyCandidates(apply.params)

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 (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')
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)
if (canRewriteSelector) {
root.walkRules((rule) => {
rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)

rule.walkDecls((d) => {
d.important = important
})
rule.walkDecls((d) => {
d.important = important
})
}

siblings.push([meta, root.nodes[0]])
})
}

siblings.push([meta, root.nodes[0]])
}
}

// Inject the rules, sorted, correctly
const nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
// Inject the rules, sorted, correctly
let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])

// `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
parent.after(nodes)
}
// 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()
}
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)
}

// Do it again, in case we have other `@apply` rules
processApply(root, context)
}
}

function expandApplyAtRules(context) {
return (root) => {
partitionApplyParents(root)
processApply(root, context)
}
}

Expand Down
30 changes: 28 additions & 2 deletions tests/10-apply.test.css
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,32 @@
color: green;
font-weight: 700;
}
.add-sibling-properties {
padding: 2rem;
padding-left: 1rem;
padding-right: 1rem;
}
.add-sibling-properties:hover {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
@media (min-width: 1024px) {
.add-sibling-properties {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
}
@media (min-width: 1280px) {
.add-sibling-properties:focus {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
}
.add-sibling-properties {
padding-top: 3px;
color: green;
font-weight: 700;
}
h1 {
font-size: 1.5rem;
line-height: 2rem;
Expand All @@ -285,13 +311,13 @@ h2 {
font-size: 1.5rem;
line-height: 2rem;
}
@media (min-width: 640px) {
@media (min-width: 1024px) {
h2 {
font-size: 1.5rem;
line-height: 2rem;
}
}
@media (min-width: 1024px) {
@media (min-width: 640px) {
h2 {
font-size: 1.5rem;
line-height: 2rem;
Expand Down
1 change: 1 addition & 0 deletions tests/10-apply.test.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<div class="recursive-apply-b"></div>
<div class="recursive-apply-c"></div>
<div class="use-with-other-properties-base use-with-other-properties-component"></div>
<div class="add-sibling-properties"></div>
<div class="a b"></div>
<div class="foo"></div>
<div class="bar"></div>
Expand Down
8 changes: 7 additions & 1 deletion tests/10-apply.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ test('@apply', () => {
@apply use-dependant-only-a font-normal;
}
.btn {
@apply font-bold py-2 px-4 rounded;
@apply font-bold py-2 px-4 rounded;
}
.btn-blue {
@apply btn bg-blue-500 hover:bg-blue-700 text-white;
Expand All @@ -99,6 +99,12 @@ test('@apply', () => {
.use-with-other-properties-component {
@apply use-with-other-properties-base;
}
.add-sibling-properties {
padding: 2rem;
@apply px-4 hover:px-2 lg:px-10 xl:focus:px-1;
padding-top: 3px;
@apply use-with-other-properties-base;
}
h1 {
@apply text-2xl lg:text-2xl sm:text-3xl;
Expand Down

0 comments on commit 4a55876

Please sign in to comment.