Skip to content

Commit

Permalink
fix(preset-mini)!: fix pseudo variants generation order, close #2713 (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jun 4, 2023
1 parent 4cdac21 commit 5c68112
Show file tree
Hide file tree
Showing 20 changed files with 200 additions and 132 deletions.
13 changes: 10 additions & 3 deletions packages/core/src/generator/index.ts
Expand Up @@ -564,9 +564,15 @@ export class UnoGenerator<Theme extends {} = {}> {
return

return [
(await Promise.all(result.map(
async r => (isString(r) ? (await this.expandShortcut(r, context, depth - 1))?.[0] : undefined) || [r],
))).flat(1).filter(Boolean),
(await Promise.all(result.map(async r =>
(
isString(r)
? (await this.expandShortcut(r, context, depth - 1))?.[0]
: undefined
) || [r],
)))
.flat(1)
.filter(Boolean),
meta,
]
}
Expand Down Expand Up @@ -658,6 +664,7 @@ function applyScope(css: string, scope?: string) {
}

const attributifyRe = /^\[(.+?)(~?=)"(.*)"\]$/

export function toEscapedSelector(raw: string) {
if (attributifyRe.test(raw))
return raw.replace(attributifyRe, (_, n, s, i) => `[${e(n)}${s}"${e(i)}"]`)
Expand Down
4 changes: 2 additions & 2 deletions packages/preset-mini/src/_variants/default.ts
Expand Up @@ -10,7 +10,7 @@ import { variantNegative } from './negative'
import { variantImportant } from './important'
import { variantCustomMedia, variantPrint } from './media'
import { variantSupports } from './supports'
import { partClasses, variantPseudoClassFunctions, variantPseudoClassesAndElements, variantTaggedPseudoClasses } from './pseudo'
import { variantPartClasses, variantPseudoClassFunctions, variantPseudoClassesAndElements, variantTaggedPseudoClasses } from './pseudo'
import { variantAria } from './aria'
import { variantDataAttribute } from './data'
import { variantContainerQuery } from './container'
Expand All @@ -35,7 +35,7 @@ export function variants(options: PresetMiniOptions): Variant<Theme>[] {
variantPseudoClassFunctions(),
...variantTaggedPseudoClasses(options),

partClasses,
variantPartClasses,
...variantColorsMediaOrClass(options),
...variantLanguageDirections,
variantScope,
Expand Down
50 changes: 50 additions & 0 deletions packages/preset-mini/src/_variants/pseudo.test.ts
@@ -0,0 +1,50 @@
import { expect, test } from 'vitest'
import { createGenerator } from '@unocss/core'
import { variantPseudoClassesAndElements } from './pseudo'

// https://github.com/unocss/unocss/issues/2713
test('pseudo variant order', async () => {
const uno = createGenerator({
variants: [
variantPseudoClassesAndElements(),
],
rules: [
[/^foo-(\d)$/, ([_, a]) => ({ text: `foo-${a}` })],
],
})

const css = await uno.generate([
'foo-1',
'hover:foo-2',
'focus:foo-3',
'disabled:foo-4',
]).then(r => r.css)

expect(css.indexOf('foo-1')).toBeLessThan(css.indexOf('foo-2'))
expect(css.indexOf('foo-2')).toBeLessThan(css.indexOf('foo-3'))
expect(css.indexOf('foo-3')).toBeLessThan(css.indexOf('foo-4'))
expect(css)
.toMatchInlineSnapshot(`
"/* layer: default */
.foo-1{text:foo-1;}
.hover\\\\:foo-2:hover{text:foo-2;}
.focus\\\\:foo-3:focus{text:foo-3;}
.disabled\\\\:foo-4:disabled{text:foo-4;}"
`)

const css2 = await uno.generate([
'foo-1',
'hover:foo-1',
'focus:foo-1',
'disabled:foo-1',
]).then(r => r.css)

expect(css2)
.toMatchInlineSnapshot(`
"/* layer: default */
.foo-1{text:foo-1;}
.hover\\\\:foo-1:hover{text:foo-1;}
.focus\\\\:foo-1:focus{text:foo-1;}
.disabled\\\\:foo-1:disabled{text:foo-1;}"
`)
})
77 changes: 42 additions & 35 deletions packages/preset-mini/src/_variants/pseudo.ts
Expand Up @@ -3,6 +3,11 @@ import { escapeRegExp, escapeSelector, warnOnce } from '@unocss/core'
import type { PresetMiniOptions } from '..'
import { handler as h, variantGetBracket } from '../_utils'

/**
* Note: the order of following pseudo classes will affect the order of generated css.
*
* Reference: https://github.com/tailwindlabs/tailwindcss/blob/main/src/corePlugins.js#L83
*/
const PseudoClasses: Record<string, string> = Object.fromEntries([
// pseudo elements part 1
['first-letter', '::first-letter'],
Expand All @@ -15,29 +20,32 @@ const PseudoClasses: Record<string, string> = Object.fromEntries([
'target',
['open', '[open]'],

// user action
'hover',
'active',
'focus-visible',
'focus-within',
'focus',

// input
'autofill',
'enabled',
'disabled',
'read-only',
'read-write',
'placeholder-shown',
// forms
'default',
'checked',
'indeterminate',
'placeholder-shown',
'autofill',
'optional',
'required',
'valid',
'invalid',
'in-range',
'out-of-range',
'required',
'optional',
'read-only',
'read-write',

// content
'empty',

// interactions
'focus-within',
'hover',
'focus',
'focus-visible',
'active',
'enabled',
'disabled',

// tree-structural
'root',
Expand All @@ -63,10 +71,14 @@ const PseudoClasses: Record<string, string> = Object.fromEntries([
['file', '::file-selector-button'],
].map(key => Array.isArray(key) ? key : [key, `:${key}`]))

const PseudoClassesKeys = Object.keys(PseudoClasses)

const PseudoClassesColon: Record<string, string> = Object.fromEntries([
['backdrop', '::backdrop'],
].map(key => Array.isArray(key) ? key : [key, `:${key}`]))

const PseudoClassesColonKeys = Object.keys(PseudoClassesColon)

const PseudoClassFunctions = [
'not',
'is',
Expand All @@ -78,22 +90,6 @@ const PseudoClassesStr = Object.entries(PseudoClasses).filter(([, pseudo]) => !p
const PseudoClassesColonStr = Object.entries(PseudoClassesColon).filter(([, pseudo]) => !pseudo.startsWith('::')).map(([key]) => key).join('|')
const PseudoClassFunctionsStr = PseudoClassFunctions.join('|')

function pseudoModifier(pseudo: string) {
if (pseudo === 'focus') {
return {
sort: 10,
noMerge: true,
}
}

if (pseudo === 'active') {
return {
sort: 20,
noMerge: true,
}
}
}

function taggedPseudoClassMatcher(tag: string, parent: string, combinator: string): VariantObject {
const rawRE = new RegExp(`^(${escapeRegExp(parent)}:)(\\S+)${escapeRegExp(combinator)}\\1`)
let splitRE: RegExp
Expand Down Expand Up @@ -163,7 +159,7 @@ function taggedPseudoClassMatcher(tag: string, parent: string, combinator: strin
handle: (input, next) => next({
...input,
prefix: `${prefix}${combinator}${input.prefix}`.replace(rawRE, '$1$2:'),
...pseudoModifier(pseudoName),
sort: PseudoClassesKeys.indexOf(pseudoName) ?? PseudoClassesColonKeys.indexOf(pseudoName),
}),
}
},
Expand All @@ -183,6 +179,7 @@ const excludedPseudo = [
]
const PseudoClassesAndElementsStr = Object.entries(PseudoClasses).map(([key]) => key).join('|')
const PseudoClassesAndElementsColonStr = Object.entries(PseudoClassesColon).map(([key]) => key).join('|')

export function variantPseudoClassesAndElements(): VariantObject {
let PseudoClassesAndElementsRE: RegExp
let PseudoClassesAndElementsColonRE: RegExp
Expand All @@ -197,6 +194,14 @@ export function variantPseudoClassesAndElements(): VariantObject {
const match = input.match(PseudoClassesAndElementsRE) || input.match(PseudoClassesAndElementsColonRE)
if (match) {
const pseudo = PseudoClasses[match[1]] || PseudoClassesColon[match[1]] || `:${match[1]}`

// order of pseudo classes
let index: number | undefined = PseudoClassesKeys.indexOf(match[1])
if (index === -1)
index = PseudoClassesColonKeys.indexOf(match[1])
if (index === -1)
index = undefined

return {
matcher: input.slice(match[0].length),
handle: (input, next) => {
Expand All @@ -211,7 +216,8 @@ export function variantPseudoClassesAndElements(): VariantObject {
return next({
...input,
...selectors,
...pseudoModifier(match[1]),
sort: index,
noMerge: true,
})
},
}
Expand Down Expand Up @@ -259,7 +265,8 @@ export function variantTaggedPseudoClasses(options: PresetMiniOptions = {}): Var
}

const PartClassesRE = /(part-\[(.+)]:)(.+)/
export const partClasses: VariantObject = {

export const variantPartClasses: VariantObject = {
match(input) {
const match = input.match(PartClassesRE)
if (match) {
Expand Down
Expand Up @@ -47,10 +47,10 @@ describe('getUtils', async () => {
undefined,
{
"layer": undefined,
"sort": undefined,
"sort": 22,
},
undefined,
undefined,
true,
],
]
`)
Expand All @@ -70,10 +70,10 @@ describe('getUtils', async () => {
undefined,
{
"layer": undefined,
"sort": undefined,
"sort": 22,
},
undefined,
undefined,
true,
],
]
`)
Expand Down
Expand Up @@ -383,8 +383,8 @@ describe('transform', () => {
<Button class="hover:text-red text-sm" />
</div>
`.trim()
expect(await transform(code)).toMatchFileSnapshot('./test-outputs/EverythingProd.svelte')
expect(await transform(code, { combine: false })).toMatchFileSnapshot('./test-outputs/EverythingDev.svelte')
await expect(await transform(code)).toMatchFileSnapshot('./test-outputs/EverythingProd.svelte')
await expect(await transform(code, { combine: false })).toMatchFileSnapshot('./test-outputs/EverythingDev.svelte')
})

// BUG: When this plugin is run on a component library first, and then in a project second, make sure to use different hashing prefixes because when `uno.parseToken()` checks a previously hashed class like `.uno-ssrvwc` it will add it to uno's cache of non-matches, then when `uno.generate()` runs it will not output the result of that shortcut. I don't know the proper solution to this and I don't think clearing uno's cache of non-matches is right. To see this bug run the following test:
Expand Down
Expand Up @@ -72,14 +72,14 @@
font-size: 0.875rem;
line-height: 1.25rem;
}
:global(._hover\:text-red_7dkb0w:hover) {
--un-text-opacity: 1;
color: rgba(248, 113, 113, var(--un-text-opacity));
}
:global(._text-orange-400_7dkb0w) {
--un-text-opacity: 1;
color: rgba(251, 146, 60, var(--un-text-opacity));
}
:global(._hover\:text-red_7dkb0w:hover) {
--un-text-opacity: 1;
color: rgba(248, 113, 113, var(--un-text-opacity));
}
@media (min-width: 640px) {
:global(._sm\:text-left_7dkb0w) {
text-align: left;
Expand Down
Expand Up @@ -53,13 +53,17 @@
font-size: 0.875rem;
line-height: 1.25rem;
}
:global(.uno-deus5r:hover, .uno-wt89uz:hover) {
:global(.uno-y37ej3) {
--un-text-opacity: 1;
color: rgba(251, 146, 60, var(--un-text-opacity));
}
:global(.uno-deus5r:hover) {
--un-text-opacity: 1;
color: rgba(248, 113, 113, var(--un-text-opacity));
}
:global(.uno-y37ej3) {
:global(.uno-wt89uz:hover) {
--un-text-opacity: 1;
color: rgba(251, 146, 60, var(--un-text-opacity));
color: rgba(248, 113, 113, var(--un-text-opacity));
}
@media (min-width: 640px) {
:global(.uno-rdvmaz) {
Expand Down
4 changes: 2 additions & 2 deletions test/__snapshots__/order.test.ts.snap
Expand Up @@ -20,8 +20,8 @@ exports[`order > multiple variant sorting 1`] = `

exports[`order > pseudo-elements sorting 1`] = `
"/* layer: default */
.dark .dark\\\\:file\\\\:marker\\\\:hover\\\\:bg-red-600:hover::file-selector-button::marker,
.dark .dark\\\\:hover\\\\:file\\\\:marker\\\\:bg-red-600::file-selector-button:hover::marker{--un-bg-opacity:1;background-color:rgba(220,38,38,var(--un-bg-opacity));}"
.dark .dark\\\\:hover\\\\:file\\\\:marker\\\\:bg-red-600::file-selector-button:hover::marker{--un-bg-opacity:1;background-color:rgba(220,38,38,var(--un-bg-opacity));}
.dark .dark\\\\:file\\\\:marker\\\\:hover\\\\:bg-red-600:hover::file-selector-button::marker{--un-bg-opacity:1;background-color:rgba(220,38,38,var(--un-bg-opacity));}"
`;

exports[`order > variant ordering 1`] = `
Expand Down
2 changes: 1 addition & 1 deletion test/__snapshots__/prefix.test.ts.snap
Expand Up @@ -49,7 +49,7 @@ exports[`prefix > multiple preset prefix 1`] = `
exports[`prefix > preset prefix 1`] = `
"/* layer: default */
.h-container{width:100%;}
.dark .dark\\\\:children\\\\:hover\\\\:h-space-x-4:hover>*>:not([hidden])~:not([hidden]),
.dark .dark\\\\:children\\\\:hover\\\\:h-space-x-4:hover>*>:not([hidden])~:not([hidden]){--un-space-x-reverse:0;margin-left:calc(1rem * calc(1 - var(--un-space-x-reverse)));margin-right:calc(1rem * var(--un-space-x-reverse));}
.dark .dark\\\\:hover\\\\:children\\\\:h-space-x-4>*:hover>:not([hidden])~:not([hidden]){--un-space-x-reverse:0;margin-left:calc(1rem * calc(1 - var(--un-space-x-reverse)));margin-right:calc(1rem * var(--un-space-x-reverse));}
.dark .dark\\\\:hover\\\\:children\\\\:h-divide-x>*:hover>:not([hidden])~:not([hidden]){--un-divide-x-reverse:0;border-left-width:calc(1px * calc(1 - var(--un-divide-x-reverse)));border-right-width:calc(1px * var(--un-divide-x-reverse));}
.hover\\\\:h-p4:hover{padding:1rem;}
Expand Down
2 changes: 1 addition & 1 deletion test/__snapshots__/scope.test.ts.snap
Expand Up @@ -8,8 +8,8 @@ exports[`scope 1`] = `
.foo-scope .hover\\\\:p-4:hover{padding:1rem;}
.foo-scope .\\\\!hover\\\\:px-10:hover{padding-left:2.5rem !important;padding-right:2.5rem !important;}
.foo-scope .pl-10px{padding-left:10px;}
.dark .foo-scope .dark\\\\:hover\\\\:text-xl:hover,
.dark .foo-scope .dark\\\\:text-xl{font-size:1.25rem;line-height:1.75rem;}
.dark .foo-scope .dark\\\\:hover\\\\:text-xl:hover{font-size:1.25rem;line-height:1.75rem;}
.variant .foo-scope .scope-\\\\[\\\\.variant\\\\]\\\\:c-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
@media (min-width: 640px){
.foo-scope .sm\\\\:text-red-100{--un-text-opacity:1;color:rgba(254,226,226,var(--un-text-opacity));}
Expand Down

0 comments on commit 5c68112

Please sign in to comment.