Skip to content

Commit 0e43169

Browse files
committed
Move common, trailing pseudo elements when generating selectors
1 parent 23e04c6 commit 0e43169

File tree

3 files changed

+165
-22
lines changed

3 files changed

+165
-22
lines changed

src/index.js

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const plugin = require('tailwindcss/plugin')
22
const merge = require('lodash.merge')
33
const castArray = require('lodash.castarray')
44
const styles = require('./styles')
5+
const { commonTrailingPseudos } = require('./utils')
56

67
const computed = {
78
// Reserved for future "magic properties", for example:
@@ -12,25 +13,11 @@ function inWhere(selector, { className, prefix }) {
1213
let prefixedNot = prefix(`.not-${className}`).slice(1)
1314
let selectorPrefix = selector.startsWith('>') ? `.${className} ` : ''
1415

15-
if (selector.endsWith('::before')) {
16-
return `:where(${selectorPrefix}${selector.slice(
17-
0,
18-
-8
19-
)}):not(:where([class~="${prefixedNot}"] *))::before`
20-
}
21-
22-
if (selector.endsWith('::after')) {
23-
return `:where(${selectorPrefix}${selector.slice(
24-
0,
25-
-7
26-
)}):not(:where([class~="${prefixedNot}"] *))::after`
27-
}
16+
// Parse the selector, if every component ends in the same pseudo element(s) then move it to the end
17+
let [trailingPseudo, rebuiltSelector] = commonTrailingPseudos(selector)
2818

29-
if (selector.endsWith('::marker')) {
30-
return `:where(${selectorPrefix}${selector.slice(
31-
0,
32-
-8
33-
)}):not(:where([class~="${prefixedNot}"] *))::marker`
19+
if (trailingPseudo) {
20+
return `:where(${selectorPrefix}${rebuiltSelector}):not(:where([class~="${prefixedNot}"] *))${trailingPseudo}`
3421
}
3522

3623
return `:where(${selectorPrefix}${selector}):not(:where([class~="${prefixedNot}"] *))`
@@ -118,11 +105,13 @@ module.exports = plugin.withOptions(
118105
]) {
119106
selectors = selectors.length === 0 ? [name] : selectors
120107

121-
let selector = target === 'legacy'
122-
? selectors.map(selector => `& ${selector}`)
123-
: selectors.join(', ')
108+
let selector =
109+
target === 'legacy' ? selectors.map((selector) => `& ${selector}`) : selectors.join(', ')
124110

125-
addVariant(`${className}-${name}`, target === 'legacy' ? selector : `& :is(${inWhere(selector, options)})`)
111+
addVariant(
112+
`${className}-${name}`,
113+
target === 'legacy' ? selector : `& :is(${inWhere(selector, options)})`
114+
)
126115
}
127116

128117
addComponents(

src/index.test.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,3 +1000,111 @@ it('should be possible to specify custom h5 and h6 styles', () => {
10001000
`)
10011001
})
10021002
})
1003+
1004+
it('should not break with multiple selectors with pseudo elements using variants', () => {
1005+
let config = {
1006+
darkMode: 'class',
1007+
plugins: [typographyPlugin()],
1008+
content: [
1009+
{
1010+
raw: html`<div class="dark:prose"></div>`,
1011+
},
1012+
],
1013+
theme: {
1014+
typography: {
1015+
DEFAULT: {
1016+
css: {
1017+
'ol li::before, ul li::before': {
1018+
color: 'red',
1019+
},
1020+
},
1021+
},
1022+
},
1023+
},
1024+
}
1025+
1026+
return run(config).then((result) => {
1027+
expect(result.css).toIncludeCss(css`
1028+
.dark .dark\:prose :where(ol li, ul li):not(:where([class~="not-prose"] *))::before {
1029+
color: red;
1030+
}
1031+
`)
1032+
})
1033+
})
1034+
1035+
it('multiple pseudo elements are lifted when they are in the same order across all selectors', () => {
1036+
let config = {
1037+
darkMode: 'class',
1038+
plugins: [typographyPlugin()],
1039+
content: [
1040+
{
1041+
raw: html`<div class="prose dark:prose"></div>`,
1042+
},
1043+
],
1044+
theme: {
1045+
typography: {
1046+
DEFAULT: {
1047+
css: {
1048+
'ol li::marker::before, ul li::marker::before': {
1049+
color: 'red',
1050+
},
1051+
},
1052+
},
1053+
},
1054+
},
1055+
}
1056+
1057+
return run(config).then((result) => {
1058+
expect(result.css).toIncludeCss(css`
1059+
.prose :where(ol li, ul li):not(:where([class~="not-prose"] *))::marker::before {
1060+
color: red;
1061+
}
1062+
`)
1063+
1064+
// TODO: The output here is a bug in tailwindcss variant selector rewriting
1065+
// IT should be ::marker::before
1066+
expect(result.css).toIncludeCss(css`
1067+
.dark .dark\:prose :where(ol li, ul li):not(:where([class~="not-prose"] *))::before::marker {
1068+
color: red;
1069+
}
1070+
`)
1071+
})
1072+
})
1073+
1074+
it('selectors with differing pseudo elements are not touched', () => {
1075+
let config = {
1076+
darkMode: 'class',
1077+
plugins: [typographyPlugin()],
1078+
content: [
1079+
{
1080+
raw: html`<div class="prose dark:prose"></div>`,
1081+
},
1082+
],
1083+
theme: {
1084+
typography: {
1085+
DEFAULT: {
1086+
css: {
1087+
'ol li::before, ul li::after': {
1088+
color: 'red',
1089+
},
1090+
},
1091+
},
1092+
},
1093+
},
1094+
}
1095+
1096+
return run(config).then((result) => {
1097+
expect(result.css).toIncludeCss(css`
1098+
.prose :where(ol li::before, ul li::after):not(:where([class~="not-prose"] *)) {
1099+
color: red;
1100+
}
1101+
`)
1102+
1103+
// TODO: The output here is a bug in tailwindcss variant selector rewriting
1104+
expect(result.css).toIncludeCss(css`
1105+
.dark .dark\:prose :where(ol li, ul li):not(:where([class~="not-prose"] *))::before,::after {
1106+
color: red;
1107+
}
1108+
`)
1109+
})
1110+
})

src/utils.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,53 @@
11
const isPlainObject = require('lodash.isplainobject')
22

3+
const parser = require('postcss-selector-parser')
4+
const parseSelector = parser()
5+
36
module.exports = {
47
isUsableColor(color, values) {
58
return isPlainObject(values) && color !== 'gray' && values[600]
69
},
10+
11+
/**
12+
* @param {string} selector
13+
*/
14+
commonTrailingPseudos(selector) {
15+
let ast = parseSelector.astSync(selector)
16+
17+
/** @type {Map<string, import('postcss-selector-parser').Pseudo[]>} */
18+
let pseudoMap = new Map()
19+
20+
for (const selector of ast.nodes) {
21+
for (const child of [...selector.nodes].reverse()) {
22+
if (child.type !== 'pseudo') {
23+
break
24+
} else if (!child.value.startsWith('::')) {
25+
break
26+
}
27+
28+
let existing = pseudoMap.get(child.value) || []
29+
pseudoMap.set(child.value, existing)
30+
31+
existing.push(child)
32+
}
33+
}
34+
35+
let commonPseudos = Array.from(pseudoMap.values()).filter(
36+
(pseudos) => pseudos.length === ast.nodes.length
37+
).reverse()
38+
39+
let trailingPseudos = parser.selector()
40+
41+
for (const pseudos of commonPseudos) {
42+
pseudos.forEach((pseudo) => pseudo.remove())
43+
44+
trailingPseudos.append(pseudos[0])
45+
}
46+
47+
if (trailingPseudos.nodes.length) {
48+
return [trailingPseudos.toString(), ast.toString()]
49+
}
50+
51+
return [null, selector]
52+
}
753
}

0 commit comments

Comments
 (0)