Skip to content

Commit

Permalink
feat(core,presets)!: update to use the recursive callback
Browse files Browse the repository at this point in the history
  • Loading branch information
chu121su12 committed Jun 21, 2022
1 parent 84dfb34 commit e6582c7
Show file tree
Hide file tree
Showing 15 changed files with 155 additions and 41 deletions.
15 changes: 11 additions & 4 deletions packages/core/src/generator/index.ts
Expand Up @@ -322,7 +322,7 @@ export class UnoGenerator {
if (typeof handler === 'string')
handler = { matcher: handler }
processed = handler.matcher
handlers.unshift(handler)
handlers.push(handler)
variants.add(v)
applied = true
break
Expand All @@ -347,6 +347,7 @@ export class UnoGenerator {
const entries = v.body?.(input.entries) || input.entries
const parents: [string | undefined, number | undefined] = Array.isArray(v.parent) ? v.parent : [v.parent, undefined]
return (v.handle ?? defaultVariantHandler)({
...input,
entries,
selector: v.selector?.(input.selector, entries) || input.selector,
parent: parents[0] || input.parent,
Expand All @@ -359,16 +360,22 @@ export class UnoGenerator {
)

const variantContextResult = handler({
entries: parsed[2],
prefix: '',
selector: toEscapedSelector(raw),
pseudo: '',
entries: parsed[2],
})

const { parent, parentOrder, selector } = variantContextResult
const { parent, parentOrder } = variantContextResult
if (parent != null && parentOrder != null)
this.parentOrders.set(parent, parentOrder)

const obj: UtilObject = {
selector: movePseudoElementsEnd(selector),
selector: movePseudoElementsEnd([
variantContextResult.prefix,
variantContextResult.selector,
variantContextResult.pseudo,
].join('')),
entries: variantContextResult.entries,
parent,
layer: variantContextResult.layer,
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/types.ts
Expand Up @@ -191,9 +191,17 @@ export type BlocklistRule = string | RegExp

export interface VariantHandlerContext {
/**
* Rewrite the output selector. Often be used to append pesudo classes or parents.
* Rewrite the output selector. Often be used to append parents.
*/
prefix: string
/**
* Rewrite the output selector. Often be used to append pesudo classes.
*/
selector: string
/**
* Rewrite the output selector. Often be used to append pesudo elements.
*/
pseudo: string
/**
* Rewrite the output css body. The input come in [key,value][] pairs.
*/
Expand Down
9 changes: 6 additions & 3 deletions packages/preset-mini/src/utils/variants.ts
@@ -1,7 +1,7 @@
import type { VariantHandler, VariantObject } from '@unocss/core'
import type { VariantHandler, VariantHandlerContext, VariantObject } from '@unocss/core'
import { escapeRegExp } from '@unocss/core'

export const variantMatcher = (name: string, selector?: (input: string) => string | undefined): VariantObject => {
export const variantMatcher = (name: string, handler: (input: VariantHandlerContext) => Record<string, any>): VariantObject => {
const re = new RegExp(`^${escapeRegExp(name)}[:-]`)
return {
name,
Expand All @@ -10,7 +10,10 @@ export const variantMatcher = (name: string, selector?: (input: string) => strin
if (match) {
return {
matcher: input.slice(match[0].length),
selector,
handle: (input, next) => next({
...input,
...handler(input),
}),
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions packages/preset-mini/src/variants/dark.ts
Expand Up @@ -5,8 +5,8 @@ import { variantMatcher, variantParentMatcher } from '../utils'
export const variantColorsMediaOrClass = (options: PresetMiniOptions = {}): Variant[] => {
if (options?.dark === 'class') {
return [
variantMatcher('dark', input => `.dark $$ ${input}`),
variantMatcher('light', input => `.light $$ ${input}`),
variantMatcher('dark', input => ({ prefix: `${input.prefix}.dark $$ ` })),
variantMatcher('light', input => ({ prefix: `${input.prefix}.light $$ ` })),
]
}

Expand Down
4 changes: 2 additions & 2 deletions packages/preset-mini/src/variants/directions.ts
Expand Up @@ -2,6 +2,6 @@ import type { Variant } from '@unocss/core'
import { variantMatcher } from '../utils'

export const variantLanguageDirections: Variant[] = [
variantMatcher('rtl', input => `[dir="rtl"] $$ ${input}`),
variantMatcher('ltr', input => `[dir="ltr"] $$ ${input}`),
variantMatcher('rtl', input => ({ prefix: `${input.prefix}[dir="rtl"] $$ ` })),
variantMatcher('ltr', input => ({ prefix: `${input.prefix}[dir="ltr"] $$ ` })),
]
30 changes: 23 additions & 7 deletions packages/preset-mini/src/variants/pseudo.ts
Expand Up @@ -82,7 +82,7 @@ const sortValue = (pseudo: string) => {
}

const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: string): VariantObject => {
const rawRe = new RegExp(`^${escapeRegExp(parent)}:`)
const rawRe = new RegExp(`${escapeRegExp(parent)}:`)
const pseudoRE = new RegExp(`^${tag}-((?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesStr}))[:-]`)
const pseudoColonRE = new RegExp(`^${tag}-((?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesColonStr}))[:]`)
return {
Expand All @@ -95,10 +95,13 @@ const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: strin
pseudo = `:${match[2]}(${pseudo})`
return {
matcher: input.slice(match[0].length),
selector: s => rawRe.test(s)
? s.replace(rawRe, `${parent}${pseudo}:`)
: `${parent}${pseudo}${combinator}${s}`,
sort: sortValue(match[3]),
handle: (input, next) => next({
...input,
prefix: rawRe.test(input.prefix)
? input.prefix.replace(rawRe, `${parent}${pseudo}:`)
: `${input.prefix}${parent}${pseudo}${combinator}`,
sort: sortValue(match[3]),
}),
}
}
},
Expand All @@ -117,8 +120,21 @@ export const variantPseudoClassesAndElements: VariantObject = {
const pseudo = PseudoClasses[match[1]] || PseudoClassesColon[match[1]] || `:${match[1]}`
return {
matcher: input.slice(match[0].length),
selector: s => `${s}${pseudo}`,
sort: sortValue(match[1]),
handle: (input, next) => {
const selectors = pseudo.startsWith('::')
? {
pseudo: `${input.pseudo}${pseudo}`,
}
: {
selector: `${input.selector}${pseudo}`,
}

return next({
...input,
...selectors,
sort: sortValue(match[1]),
})
},
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/preset-wind/src/variants/combinators.ts
Expand Up @@ -2,5 +2,5 @@ import type { Variant } from '@unocss/core'
import { variantMatcher } from '@unocss/preset-mini/utils'

export const variantCombinators: Variant[] = [
variantMatcher('svg', input => `${input} svg`),
variantMatcher('svg', input => ({ selector: `${input.selector} svg` })),
]
4 changes: 2 additions & 2 deletions packages/preset-wind/src/variants/dark.ts
Expand Up @@ -2,8 +2,8 @@ import type { Variant } from '@unocss/core'
import { variantMatcher, variantParentMatcher } from '@unocss/preset-mini/utils'

export const variantColorsScheme: Variant[] = [
variantMatcher('.dark', input => `.dark $$ ${input}`),
variantMatcher('.light', input => `.light $$ ${input}`),
variantMatcher('.dark', input => ({ prefix: `${input.prefix}.dark $$ ` })),
variantMatcher('.light', input => ({ prefix: `${input.prefix}.light $$ ` })),
variantParentMatcher('@dark', '@media (prefers-color-scheme: dark)'),
variantParentMatcher('@light', '@media (prefers-color-scheme: light)'),
]
16 changes: 12 additions & 4 deletions test/__snapshots__/order.test.ts.snap
Expand Up @@ -14,18 +14,26 @@ exports[`order > movePseudoElementsEnd 1`] = `".marker\\\\:file\\\\:hover\\\\:se

exports[`order > multiple variant sorting 1`] = `
"/* layer: default */
.dark .group:hover:focus-within .dark\\\\:group-hover\\\\:group-focus-within\\\\:bg-blue-600{--un-bg-opacity:1;background-color:rgba(37,99,235,var(--un-bg-opacity));}
.group:hover:focus-within .dark .group-hover\\\\:group-focus-within\\\\:dark\\\\:bg-red-600{--un-bg-opacity:1;background-color:rgba(220,38,38,var(--un-bg-opacity));}"
.dark .group:focus-within:hover .dark\\\\:group-hover\\\\:group-focus-within\\\\:bg-blue-600{--un-bg-opacity:1;background-color:rgba(37,99,235,var(--un-bg-opacity));}
.group:focus-within:hover .dark .group-hover\\\\:group-focus-within\\\\:dark\\\\:bg-red-600{--un-bg-opacity:1;background-color:rgba(220,38,38,var(--un-bg-opacity));}"
`;

exports[`order > variant ordering 1`] = `
"/* layer: default */
.dark .group .dark\\\\:group\\\\:foo-3{name:foo-3;}
.dark .group .group\\\\:dark\\\\:foo-4{name:foo-4;}
.group .dark .dark\\\\:group\\\\:foo-3{name:foo-3;}
.group .dark .group\\\\:dark\\\\:foo-4{name:foo-4;}
.group .light .group\\\\:light\\\\:foo-2{name:foo-2;}
.light .group .light\\\\:group\\\\:foo-1{name:foo-1;}"
`;

exports[`order > variant ordering reversed 1`] = `
"/* layer: default */
.dark .group .dark\\\\:group\\\\:foo-3{name:foo-3;}
.dark .group .group\\\\:dark\\\\:foo-4{name:foo-4;}
.group .light .light\\\\:group\\\\:foo-1{name:foo-1;}
.light .group .group\\\\:light\\\\:foo-2{name:foo-2;}"
`;

exports[`order > variant sorting 1`] = `
"/* layer: default */
pre .pre\\\\:foo-1,
Expand Down
14 changes: 8 additions & 6 deletions test/__snapshots__/preset-mini.test.ts.snap
Expand Up @@ -112,12 +112,12 @@ div:hover .group-\\\\[div\\\\:hover\\\\]-\\\\[combinator\\\\:test-4\\\\]{combina
.file\\\\:bg-violet-50::file-selector-button{--un-bg-opacity:1;background-color:rgba(245,243,255,var(--un-bg-opacity));}
.first-letter\\\\:bg-green-400::first-letter,
.first-line\\\\:bg-green-400::first-line{--un-bg-opacity:1;background-color:rgba(74,222,128,var(--un-bg-opacity));}
.focus-within\\\\:has-first\\\\:checked\\\\:bg-gray\\\\/20:checked:has(:first-child):focus-within,
.focus-within\\\\:where-first\\\\:checked\\\\:bg-gray\\\\/20:checked:where(:first-child):focus-within{background-color:rgba(156,163,175,0.2);}
.focus-within\\\\:has-first\\\\:checked\\\\:bg-gray\\\\/20:focus-within:has(:first-child):checked,
.focus-within\\\\:where-first\\\\:checked\\\\:bg-gray\\\\/20:focus-within:where(:first-child):checked{background-color:rgba(156,163,175,0.2);}
.hover\\\\:file\\\\:bg-violet-100:hover::file-selector-button{--un-bg-opacity:1;background-color:rgba(237,233,254,var(--un-bg-opacity));}
.hover\\\\:is-first\\\\:checked\\\\:bg-true-gray\\\\/10:checked:is(:first-child):hover,
.hover\\\\:not-first\\\\:checked\\\\:bg-true-gray\\\\/10:checked:not(:first-child):hover{background-color:rgba(163,163,163,0.1);}
.hover\\\\:not-first\\\\:checked\\\\:bg-red\\\\/10:checked:not(:first-child):hover{background-color:rgba(248,113,113,0.1);}
.hover\\\\:is-first\\\\:checked\\\\:bg-true-gray\\\\/10:hover:is(:first-child):checked,
.hover\\\\:not-first\\\\:checked\\\\:bg-true-gray\\\\/10:hover:not(:first-child):checked{background-color:rgba(163,163,163,0.1);}
.hover\\\\:not-first\\\\:checked\\\\:bg-red\\\\/10:hover:not(:first-child):checked{background-color:rgba(248,113,113,0.1);}
.marker\\\\:bg-violet-200::marker{--un-bg-opacity:1;background-color:rgba(221,214,254,var(--un-bg-opacity));}
.peer:checked~.peer-checked\\\\:bg-blue-500{--un-bg-opacity:1;background-color:rgba(59,130,246,var(--un-bg-opacity));}
.previous:checked+.previous-checked\\\\:bg-red-500{--un-bg-opacity:1;background-color:rgba(239,68,68,var(--un-bg-opacity));}
Expand Down Expand Up @@ -331,7 +331,7 @@ div:hover .group-\\\\[div\\\\:hover\\\\]-\\\\[combinator\\\\:test-4\\\\]{combina
.text-shadow-color-op-30{--un-text-shadow-opacity:0.3;}
.case-upper{text-transform:uppercase;}
.case-normal{text-transform:none;}
.group:hover:focus .group-hover\\\\:group-focus\\\\:text-center,
.group:focus:hover .group-hover\\\\:group-focus\\\\:text-center,
.parent:hover>.parent-hover\\\\:text-center{text-align:center;}
.text-left,
[dir=\\"ltr\\"] .ltr\\\\:text-left{text-align:left;}
Expand Down Expand Up @@ -367,6 +367,7 @@ div:hover .group-\\\\[div\\\\:hover\\\\]-\\\\[combinator\\\\:test-4\\\\]{combina
.c-\\\\$color-variable,
.c-\\\\$color-variable\\\\/\\\\$opacity-variable,
.c-\\\\$color-variable\\\\/10{color:var(--color-variable);}
.checked\\\\:next\\\\:text-slate-100:checked+*{--un-text-opacity:1;color:rgba(241,245,249,var(--un-text-opacity));}
.color-\\\\$red{color:var(--red);}
.color-blue,
.color-blue-400{--un-text-opacity:1;color:rgba(96,165,250,var(--un-text-opacity));}
Expand All @@ -383,6 +384,7 @@ div:hover .group-\\\\[div\\\\:hover\\\\]-\\\\[combinator\\\\:test-4\\\\]{combina
.in-range\\\\:color-pink-100:in-range,
.open\\\\:color-pink-100[open],
.out-of-range\\\\:color-pink-100:out-of-range{--un-text-opacity:1;color:rgba(252,231,243,var(--un-text-opacity));}
.next\\\\:checked\\\\:text-slate-200+*:checked{--un-text-opacity:1;color:rgba(226,232,240,var(--un-text-opacity));}
.placeholder-color-red-1::placeholder,
.text-red-100,
.text-red100{--un-text-opacity:1;color:rgba(254,226,226,var(--un-text-opacity));}
Expand Down
16 changes: 12 additions & 4 deletions test/__snapshots__/variant-handler.test.ts.snap
@@ -1,17 +1,25 @@
// Vitest Snapshot v1

exports[`variants > selector section is merged in order 1`] = `
"/* layer: default */
.prefix .pre\\\\:back\\\\:foo::pseudo,
.prefix .replaced,
.prefix .replaced::pseudo,
.replaced::pseudo{name:bar;}"
`;

exports[`variants > variant can stack 1`] = `
"/* layer: default */
.first\\\\:second\\\\:third\\\\:foo > :third > :second > :first,
.first\\\\:three\\\\:two\\\\:foo > :first + :three + :two,
.one\\\\:two\\\\:three\\\\:foo + :one + :two + :three{name:bar;}"
.first\\\\:second\\\\:third\\\\:foo > :first > :second > :third,
.first\\\\:three\\\\:two\\\\:foo > :first + :two + :three,
.one\\\\:two\\\\:three\\\\:foo + :three + :two + :one{name:bar;}"
`;

exports[`variants > variant context is propagated 1`] = `
"/* layer: default */
.foo{name:bar;}
/* layer: variant */
@supports{
.selector{name:bar !important;}
:prefix > .selector::pseudo{name:bar !important;}
}"
`;
4 changes: 4 additions & 0 deletions test/assets/preset-mini-targets.ts
Expand Up @@ -872,6 +872,10 @@ export const presetMiniTargets: string[] = [
'peer-checked:bg-blue-500',
'parent-hover:text-center',
'previous-checked:bg-red-500',

// variants - tagged
'checked:next:text-slate-100',
'next:checked:text-slate-200',
]

export const presetMiniNonTargets = [
Expand Down
38 changes: 35 additions & 3 deletions test/order.test.ts
Expand Up @@ -35,10 +35,42 @@ describe('order', () => {
],
presets: [],
variants: [
variantMatcher('light', input => `.light $$ ${input}`),
variantMatcher('group', input => `.group ${input}`),
variantMatcher('light', input => ({ prefix: `${input.prefix}.light $$ ` })),
variantMatcher('group', input => ({ prefix: `${input.prefix}.group ` })),
(v, ctx) => {
const match = variantMatcher('dark', input => `.dark $$ ${input}`).match(v, ctx)
const match = variantMatcher('dark', input => ({ prefix: `${input.prefix}.dark $$ ` })).match(v, ctx)
if (typeof match === 'object') {
return {
...match,
order: 1,
}
}
},
],
})
const code = [
'light:group:foo-1',
'group:light:foo-2',
'dark:group:foo-3',
'group:dark:foo-4',
].join(' ')
const { css } = await uno.generate(code, { preflights: false })
const { css: css2 } = await uno.generate(code, { preflights: false })
expect(css).toMatchSnapshot()
expect(css).toEqual(css2)
})

test('variant ordering reversed', async () => {
const uno = createGenerator({
rules: [
[/^foo-.$/, ([m]) => ({ name: m })],
],
presets: [],
variants: [
variantMatcher('light', input => ({ prefix: `.light $$ ${input.prefix}` })),
variantMatcher('group', input => ({ prefix: `.group ${input.prefix}` })),
(v, ctx) => {
const match = variantMatcher('dark', input => ({ prefix: `.dark $$ ${input.prefix}` })).match(v, ctx)
if (typeof match === 'object') {
return {
...match,
Expand Down
4 changes: 2 additions & 2 deletions test/selector-no-merge.test.ts
Expand Up @@ -25,8 +25,8 @@ describe('variant', () => {
[/^m3-(.+)$/, ([, s]) => `moz:${s} merge-candidate-early`],
],
variants: [
variantMatcher('moz', s => `${s}::non-breaking`),
variantMatcher('webkit', s => `${s}::breaking`),
variantMatcher('moz', s => ({ pseudo: `${s.pseudo}::non-breaking` })),
variantMatcher('webkit', s => ({ pseudo: `${s.pseudo}::breaking` })),
],
rules: [
[/^no-merge$/, () => ({ merged: 1 }), { noMerge: true }],
Expand Down
26 changes: 26 additions & 0 deletions test/variant-handler.test.ts
@@ -1,5 +1,6 @@
import { createGenerator } from '@unocss/core'
import { describe, expect, test } from 'vitest'
import { variantMatcher } from '@unocss/preset-mini/utils'

describe('variants', () => {
test('variant context is propagated', async () => {
Expand All @@ -15,7 +16,9 @@ describe('variants', () => {
return {
matcher: input.slice(match[0].length),
handle: (input, next) => next({
prefix: ':prefix > ',
selector: '.selector',
pseudo: '::pseudo',
entries: input.entries.map((entry) => {
entry[1] += ' !important'
return entry
Expand All @@ -38,6 +41,29 @@ describe('variants', () => {
expect(css).toMatchSnapshot()
})

test('selector section is merged in order', async () => {
const uno = createGenerator({
rules: [
['foo', { name: 'bar' }],
],
variants: [
variantMatcher('pre', () => ({ prefix: '.prefix ' })),
variantMatcher('main', () => ({ selector: '.replaced' })),
variantMatcher('back', () => ({ pseudo: '::pseudo' })),
],
})

const { css } = await uno.generate([
'pre:main:foo',
'pre:back:foo',
'main:back:foo',
'pre:main:back:foo',
'back:main:pre:foo',
].join(' '), { preflights: false })

expect(css).toMatchSnapshot()
})

test('variant can stack', async () => {
const uno = createGenerator({
rules: [
Expand Down

0 comments on commit e6582c7

Please sign in to comment.