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

Respect apply sibling order #155

Merged
merged 1 commit into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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