Skip to content

Commit

Permalink
Improve arbitrary variant (+ parent selector) support
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-rogerson committed Sep 19, 2022
1 parent 5347428 commit 8de0ce7
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 16 deletions.
21 changes: 21 additions & 0 deletions __fixtures__/arbitraryVariants/arbitraryVariants.tsx
Expand Up @@ -21,3 +21,24 @@ tw`[p]:(mt-4 mb-4)`

tw`[@media (min-width: 800px)]:block`
tw`[content\\!]:block`

// Combinations
tw`[&:nth-child(1)]:block`
tw`[:nth-child(1)]:block`
tw`[@media ...]:block`
tw`[.selector]:block`
tw`[section]:block`
tw`[section &]:block`
tw`md:[section]:block`
tw`[section]:[bla]:block`
tw`[section &]:[pre &]:block`
tw`[section &]:[& pre]:block`
tw`[section &]:first:[pre &]:block`
tw`[section &]:first:[& pre]:block`
tw`first:[section &]:[pre &]:block`
tw`first:[section &]:[& pre]:block`
tw`first:[section &]:[& pre]:mt-[2px]`
tw`first:[section &]:[& pre]:[display:inline]`
tw`[pre]:[display:inline]`
tw`[& pre]:[display:inline]`
tw`[:hover]:[display:inline]`
123 changes: 121 additions & 2 deletions __snapshots__/plugin.test.js.snap
Expand Up @@ -4090,6 +4090,27 @@ tw\`[p]:(mt-4 mb-4)\`
tw\`[@media (min-width: 800px)]:block\`
tw\`[content\\\\!]:block\`

// Combinations
tw\`[&:nth-child(1)]:block\`
tw\`[:nth-child(1)]:block\`
tw\`[@media ...]:block\`
tw\`[.selector]:block\`
tw\`[section]:block\`
tw\`[section &]:block\`
tw\`md:[section]:block\`
tw\`[section]:[bla]:block\`
tw\`[section &]:[pre &]:block\`
tw\`[section &]:[& pre]:block\`
tw\`[section &]:first:[pre &]:block\`
tw\`[section &]:first:[& pre]:block\`
tw\`first:[section &]:[pre &]:block\`
tw\`first:[section &]:[& pre]:block\`
tw\`first:[section &]:[& pre]:mt-[2px]\`
tw\`first:[section &]:[& pre]:[display:inline]\`
tw\`[pre]:[display:inline]\`
tw\`[& pre]:[display:inline]\`
tw\`[:hover]:[display:inline]\`

↓ ↓ ↓ ↓ ↓ ↓

// @ts-nocheck
Expand Down Expand Up @@ -4121,13 +4142,13 @@ tw\`[content\\\\!]:block\`
}) // Classes

;({
'.class1 .class2 &': {
'& .class2 .class1': {
display: 'block',
},
}) // Multiple dynamic variants

;({
'.class1 .class2 .class3 &': {
'& .class3 .class1 .class2': {
display: 'block',
},
}) // Multiple dynamic variants
Expand Down Expand Up @@ -4167,6 +4188,104 @@ tw\`[content\\\\!]:block\`
'& content!': {
display: 'block',
},
}) // Combinations

;({
':nth-child(1)': {
display: 'block',
},
})
;({
':nth-child(1)': {
display: 'block',
},
})
;({
'@media ...': {
display: 'block',
},
})
;({
'& .selector': {
display: 'block',
},
})
;({
'& section': {
display: 'block',
},
})
;({
'section &': {
display: 'block',
},
})
;({
'@media (min-width: 768px)': {
'section &': {
display: 'block',
},
},
})
;({
'& bla section': {
display: 'block',
},
})
;({
'section pre &': {
display: 'block',
},
})
;({
'section & pre': {
display: 'block',
},
})
;({
'section pre &:first-child': {
display: 'block',
},
})
;({
'section & pre:first-child': {
display: 'block',
},
})
;({
'section pre &:first-child': {
display: 'block',
},
})
;({
'section & pre:first-child': {
display: 'block',
},
})
;({
'section & pre:first-child': {
marginTop: '2px',
},
})
;({
'section & pre:first-child': {
display: 'inline',
},
})
;({
'& pre': {
display: 'inline',
},
})
;({
'& pre': {
display: 'inline',
},
})
;({
':hover': {
display: 'inline',
},
})


Expand Down
14 changes: 7 additions & 7 deletions src/core/extractRuleStyles.ts
Expand Up @@ -16,9 +16,9 @@ import type * as P from 'postcss'

const ESC_DIGIT = /\\3(\d)/g
const ESC_COMMA = /\\2c /g
const ESC_DBL_BACKSLASHES = /\\(?!\d!)(?=.|$)/g
const COMMAS_OUTSIDE_BRACKETS =
/,(?=(?:(?:(?!\)).)*\()|[^()]*$)(?=(?:(?:(?!]).)*\[)|[^[\]]*$)/g
/,(?=(?:(?:(?!\)).)*\()|[^()]*$)(?=(?:(?:(?!]).)*\[)|[^[\]]*$)/g // eg: Avoid `:where(ul, ul)`
const ARBITRARY_SELECTOR = /[.:]\[/

function transformImportant(value: string, params: TransformDecl): string {
if (params.passChecks === true) return value
Expand Down Expand Up @@ -58,7 +58,6 @@ function extractFromRule(
.replace(ESC_DIGIT, '$1') // Remove digit escaping
const selector = unescape(selectorForUnescape)
.replace(ESC_COMMA, ',') // Remove comma escaping
.replace(ESC_DBL_BACKSLASHES, '') // Remove \\ escaping
.replace(LINEFEED, ' ')
.replace(/{{PRESERVED_DOUBLE_ESCAPE}}/g, '\\')
return [selector, extractRuleStyles(rule.nodes, params)] as [
Expand Down Expand Up @@ -129,10 +128,11 @@ const ruleTypes = {

params.debug('styles extracted', [selector, styles])

const selectorList = selector
// Avoid split when comma is outside `()` + `[]`, eg: `:where(ul, ul)`
.split(COMMAS_OUTSIDE_BRACKETS)
.filter(s => params.selectorMatchReg?.test(s))
const selectorList = (
ARBITRARY_SELECTOR.test(selector)
? [selector]
: selector.split(COMMAS_OUTSIDE_BRACKETS)
).filter(s => params.selectorMatchReg?.test(s))

if (selectorList.length === 0) {
params.debug('no selector match', selector, 'warn')
Expand Down
35 changes: 28 additions & 7 deletions src/core/lib/convertClassName.ts
Expand Up @@ -7,7 +7,8 @@ import { SPACE_ID, SPACE_ID_TEMP_ALL } from '../constants'

const SPLIT_COLON_AVOID_WITHIN_SQUARE_BRACKETS =
/:(?=(?:(?:(?!]).)*\[)|[^[\]]*$)/g
const ARBITRARY_VARIANTS = /(?!\[)([^[\]]+)(?=]:)/g
const ARBITRARY_VARIANTS = /(?<=\[)(.+?)(?=]:)/g
const ALL_COMMAS = /,/g

type ConvertShortCssToArbitraryPropertyParameters = {
disableShortCss: CoreContext['twinConfig']['disableShortCss']
Expand Down Expand Up @@ -107,16 +108,36 @@ function convertClassName(

// Add a parent selector if it's missing from the arbitrary variant
const arbitraryVariantsCount = className.match(ARBITRARY_VARIANTS)
className = className.replace(ARBITRARY_VARIANTS, (v, _, offset) => {
if (v.includes('&') || v.startsWith('@')) return v
if (arbitraryVariantsCount && arbitraryVariantsCount.length > 1)
return `${v}_&`
return offset === 1 ? `&_${v}` : `${v}_&`
})
className = className.replace(ARBITRARY_VARIANTS, (v, _, offset) =>
addParentSelector(v, offset, arbitraryVariantsCount)
)

debug('class after format', className)

return className
}

function escapeCommas(className: string): string {
return className.replace(ALL_COMMAS, '\\2c')
}

function addParentSelector(
value: string,
offset: number,
// eslint-disable-next-line @typescript-eslint/ban-types
arbitraryVariantsCount: string[] | null
): string {
// Tailwindcss requires pre-encoded commas - unencoded are removed and we end up with an invalid selector
const selector = escapeCommas(value)
// Preserve selectors with parent selector or media queries
if (selector.includes('&') || selector.startsWith('@')) return selector
// pseudo
if (selector.startsWith(':')) return `&${selector}`
// Selectors with multiple arbitrary variants are too hard to determine so follow a basic rule instead
if (arbitraryVariantsCount && arbitraryVariantsCount.length > 1)
return `&_${selector}`
// If the arbitrary variant is the first selector, add a parent selector
return offset === 1 ? `&_${selector}` : `${selector}_&`
}

export default convertClassName

0 comments on commit 8de0ce7

Please sign in to comment.