diff --git a/packages/core/src/generator/index.ts b/packages/core/src/generator/index.ts index 2cc1a40dd2..2d9ccd613e 100644 --- a/packages/core/src/generator/index.ts +++ b/packages/core/src/generator/index.ts @@ -564,9 +564,15 @@ export class UnoGenerator { 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, ] } @@ -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)}"]`) diff --git a/packages/preset-mini/src/_variants/default.ts b/packages/preset-mini/src/_variants/default.ts index 34fa3f4b86..73a440b6d5 100644 --- a/packages/preset-mini/src/_variants/default.ts +++ b/packages/preset-mini/src/_variants/default.ts @@ -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' @@ -35,7 +35,7 @@ export function variants(options: PresetMiniOptions): Variant[] { variantPseudoClassFunctions(), ...variantTaggedPseudoClasses(options), - partClasses, + variantPartClasses, ...variantColorsMediaOrClass(options), ...variantLanguageDirections, variantScope, diff --git a/packages/preset-mini/src/_variants/pseudo.test.ts b/packages/preset-mini/src/_variants/pseudo.test.ts new file mode 100644 index 0000000000..c7af8a8489 --- /dev/null +++ b/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;}" + `) +}) diff --git a/packages/preset-mini/src/_variants/pseudo.ts b/packages/preset-mini/src/_variants/pseudo.ts index afdeffcbfe..e425e23452 100644 --- a/packages/preset-mini/src/_variants/pseudo.ts +++ b/packages/preset-mini/src/_variants/pseudo.ts @@ -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 = Object.fromEntries([ // pseudo elements part 1 ['first-letter', '::first-letter'], @@ -15,29 +20,32 @@ const PseudoClasses: Record = 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', @@ -63,10 +71,14 @@ const PseudoClasses: Record = Object.fromEntries([ ['file', '::file-selector-button'], ].map(key => Array.isArray(key) ? key : [key, `:${key}`])) +const PseudoClassesKeys = Object.keys(PseudoClasses) + const PseudoClassesColon: Record = Object.fromEntries([ ['backdrop', '::backdrop'], ].map(key => Array.isArray(key) ? key : [key, `:${key}`])) +const PseudoClassesColonKeys = Object.keys(PseudoClassesColon) + const PseudoClassFunctions = [ 'not', 'is', @@ -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 @@ -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), }), } }, @@ -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 @@ -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) => { @@ -211,7 +216,8 @@ export function variantPseudoClassesAndElements(): VariantObject { return next({ ...input, ...selectors, - ...pseudoModifier(match[1]), + sort: index, + noMerge: true, }) }, } @@ -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) { diff --git a/packages/svelte-scoped/src/_preprocess/transformApply/getUtils.test.ts b/packages/svelte-scoped/src/_preprocess/transformApply/getUtils.test.ts index 179a3037ae..ae94a3e6b8 100644 --- a/packages/svelte-scoped/src/_preprocess/transformApply/getUtils.test.ts +++ b/packages/svelte-scoped/src/_preprocess/transformApply/getUtils.test.ts @@ -47,10 +47,10 @@ describe('getUtils', async () => { undefined, { "layer": undefined, - "sort": undefined, + "sort": 22, }, undefined, - undefined, + true, ], ] `) @@ -70,10 +70,10 @@ describe('getUtils', async () => { undefined, { "layer": undefined, - "sort": undefined, + "sort": 22, }, undefined, - undefined, + true, ], ] `) diff --git a/packages/svelte-scoped/src/_preprocess/transformClasses/index.test.ts b/packages/svelte-scoped/src/_preprocess/transformClasses/index.test.ts index e69983f6a0..697cc957a2 100644 --- a/packages/svelte-scoped/src/_preprocess/transformClasses/index.test.ts +++ b/packages/svelte-scoped/src/_preprocess/transformClasses/index.test.ts @@ -383,8 +383,8 @@ describe('transform', () => {