Skip to content

Commit 110c6f0

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

File tree

3 files changed

+261
-22
lines changed

3 files changed

+261
-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: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,3 +1000,198 @@ 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('lifts all common, trailing pseudo elements when the same 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('does not modify selectors with differing pseudo elements', () => {
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,
1106+
::after {
1107+
color: red;
1108+
}
1109+
`)
1110+
})
1111+
})
1112+
1113+
it('lifts only the common, trailing pseudo elements from selectors', () => {
1114+
let config = {
1115+
darkMode: 'class',
1116+
plugins: [typographyPlugin()],
1117+
content: [
1118+
{
1119+
raw: html`<div class="prose dark:prose"></div>`,
1120+
},
1121+
],
1122+
theme: {
1123+
typography: {
1124+
DEFAULT: {
1125+
css: {
1126+
'ol li::scroll-thumb::before, ul li::scroll-track::before': {
1127+
color: 'red',
1128+
},
1129+
},
1130+
},
1131+
},
1132+
},
1133+
}
1134+
1135+
return run(config).then((result) => {
1136+
expect(result.css).toIncludeCss(css`
1137+
.prose
1138+
:where(ol li::scroll-thumb, ul li::scroll-track):not(:where([class~='not-prose']
1139+
*))::before {
1140+
color: red;
1141+
}
1142+
`)
1143+
1144+
// TODO: The output here is a bug in tailwindcss variant selector rewriting
1145+
expect(result.css).toIncludeCss(css`
1146+
.dark .dark\:prose :where(ol li, ul li):not(:where([class~='not-prose'] *))::scroll-thumb,
1147+
::scroll-track,
1148+
::before {
1149+
color: red;
1150+
}
1151+
`)
1152+
})
1153+
})
1154+
1155+
it('ignores common non-trailing pseudo-elements in selectors', () => {
1156+
let config = {
1157+
darkMode: 'class',
1158+
plugins: [typographyPlugin()],
1159+
content: [
1160+
{
1161+
raw: html`<div class="prose dark:prose"></div>`,
1162+
},
1163+
],
1164+
theme: {
1165+
typography: {
1166+
DEFAULT: {
1167+
css: {
1168+
'ol li::before::scroll-thumb, ul li::before::scroll-track': {
1169+
color: 'red',
1170+
},
1171+
},
1172+
},
1173+
},
1174+
},
1175+
}
1176+
1177+
return run(config).then((result) => {
1178+
expect(result.css).toIncludeCss(css`
1179+
.prose
1180+
:where(ol li::before::scroll-thumb, ul
1181+
li::before::scroll-track):not(:where([class~='not-prose'] *)) {
1182+
color: red;
1183+
}
1184+
`)
1185+
1186+
// TODO: The output here is a bug in tailwindcss variant selector rewriting
1187+
expect(result.css).toIncludeCss(css`
1188+
.dark
1189+
.dark\:prose
1190+
:where(ol li::scroll-thumb, ul li::scroll-track):not(:where([class~='not-prose']
1191+
*))::before,
1192+
::before {
1193+
color: red;
1194+
}
1195+
`)
1196+
})
1197+
})

src/utils.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,62 @@
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 {import('postcss-selector-parser').Pseudo[][]} */
18+
let matrix = []
19+
20+
// Put the pseudo elements in reverse order in a sparse, column-major 2D array
21+
for (let [i, sel] of ast.nodes.entries()) {
22+
for (const [j, child] of [...sel.nodes].reverse().entries()) {
23+
// We only care about pseudo elements
24+
if (child.type !== 'pseudo' || !child.value.startsWith('::')) {
25+
break
26+
}
27+
28+
matrix[j] = matrix[j] || []
29+
matrix[j][i] = child
30+
}
31+
}
32+
33+
let trailingPseudos = parser.selector()
34+
35+
// At this point the pseudo elements are in a column-major 2D array
36+
// This means each row contains one "column" of pseudo elements from each selector
37+
// We can compare all the pseudo elements in a row to see if they are the same
38+
for (const pseudos of matrix) {
39+
// It's a sparse 2D array so there are going to be holes in the rows
40+
// We skip those
41+
if (!pseudos) {
42+
continue
43+
}
44+
45+
let values = new Set([...pseudos.map((p) => p.value)])
46+
47+
// The pseudo elements are not the same
48+
if (values.size > 1) {
49+
break
50+
}
51+
52+
pseudos.forEach((pseudo) => pseudo.remove())
53+
trailingPseudos.prepend(pseudos[0])
54+
}
55+
56+
if (trailingPseudos.nodes.length) {
57+
return [trailingPseudos.toString(), ast.toString()]
58+
}
59+
60+
return [null, selector]
61+
},
762
}

0 commit comments

Comments
 (0)