Skip to content

Commit

Permalink
feat(core): use recursive callback to resolve variant (#1126)
Browse files Browse the repository at this point in the history
  • Loading branch information
chu121su12 committed Jun 24, 2022
1 parent a95a877 commit 7c88219
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 25 deletions.
58 changes: 37 additions & 21 deletions packages/core/src/generator/index.ts
@@ -1,5 +1,5 @@
import { createNanoEvents } from '../utils/events'
import type { CSSEntries, CSSObject, ExtractorContext, GenerateOptions, GenerateResult, ParsedUtil, PreflightContext, PreparedRule, RawUtil, ResolvedConfig, Rule, RuleContext, RuleMeta, Shortcut, StringifiedUtil, UserConfig, UserConfigDefaults, UtilObject, Variant, VariantContext, VariantHandler, VariantMatchedResult } from '../types'
import type { CSSEntries, CSSObject, ExtractorContext, GenerateOptions, GenerateResult, ParsedUtil, PreflightContext, PreparedRule, RawUtil, ResolvedConfig, Rule, RuleContext, RuleMeta, Shortcut, StringifiedUtil, UserConfig, UserConfigDefaults, UtilObject, Variant, VariantContext, VariantHandler, VariantHandlerContext, VariantMatchedResult } from '../types'
import { resolveConfig } from '../config'
import { CONTROL_SHORTCUT_NO_MERGE, TwoKeyMap, e, entriesToCss, expandVariantGroup, isRawUtil, isStaticShortcut, noop, normalizeCSSEntries, normalizeCSSValues, notNull, uniq, warnOnce } from '../utils'
import { version } from '../../package.json'
Expand Down Expand Up @@ -322,8 +322,6 @@ export class UnoGenerator {
if (typeof handler === 'string')
handler = { matcher: handler }
processed = handler.matcher
if (Array.isArray(handler.parent))
this.parentOrders.set(handler.parent[0], handler.parent[1])
handlers.unshift(handler)
variants.add(v)
applied = true
Expand All @@ -340,27 +338,41 @@ export class UnoGenerator {
}

applyVariants(parsed: ParsedUtil, variantHandlers = parsed[4], raw = parsed[1]): UtilObject {
const handlers = [...variantHandlers].sort((a, b) => (a.order || 0) - (b.order || 0))

let entries: CSSEntries = parsed[2]
let selector = toEscapedSelector(raw)
let parent: string | undefined
let layer: string | undefined
let sort: number | undefined
handlers.forEach((v) => {
entries = v.body?.(entries) || entries
selector = v.selector?.(selector, entries) || selector
parent = Array.isArray(v.parent) ? v.parent[0] : v.parent || parent
layer = v.layer || layer
sort = v.sort || sort
const handler = [...variantHandlers]
.sort((a, b) => (a.order || 0) - (b.order || 0))
.reverse()
.reduce(
(previous, v) =>
(input: VariantHandlerContext) => {
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)({
entries,
selector: v.selector?.(input.selector, entries) || input.selector,
parent: parents[0] || input.parent,
parentOrder: parents[1] || input.parentOrder,
layer: v.layer || input.layer,
sort: v.sort || input.sort,
}, previous)
},
(input: VariantHandlerContext) => input,
)

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

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

const obj: UtilObject = {
selector: movePseudoElementsEnd(selector),
entries,
entries: variantContextResult.entries,
parent,
layer,
sort,
layer: variantContextResult.layer,
sort: variantContextResult.sort,
}

for (const p of this.config.postprocess)
Expand Down Expand Up @@ -550,14 +562,14 @@ export class UnoGenerator {
mapItem[0].push([entries, !!item[3]?.noMerge, sort ?? 0])
}
return rawStringfieldUtil.concat(selectorMap
.map(([e, index], selector, mediaQuery) => {
.map(([e, index], selector, joinedParents) => {
const stringify = (flatten: boolean, noMerge: boolean, entrySortPair: [CSSEntries, number][]): (StringifiedUtil | undefined)[] => {
const maxSort = Math.max(...entrySortPair.map(e => e[1]))
const entriesList = entrySortPair.map(e => e[0])
return (flatten ? [entriesList.flat(1)] : entriesList).map((entries: CSSEntries): StringifiedUtil | undefined => {
const body = entriesToCss(entries)
if (body)
return [index, selector, body, mediaQuery, { ...meta, noMerge, sort: maxSort }, context]
return [index, selector, body, joinedParents, { ...meta, noMerge, sort: maxSort }, context]
return undefined
})
}
Expand Down Expand Up @@ -610,3 +622,7 @@ export function toEscapedSelector(raw: string) {
return raw.replace(attributifyRe, (_, n, s, i) => `[${e(n)}${s}"${e(i)}"]`)
return `.${e(raw)}`
}

function defaultVariantHandler(input: VariantHandlerContext, next: (input: VariantHandlerContext) => VariantHandlerContext) {
return next(input)
}
39 changes: 35 additions & 4 deletions packages/core/src/types.ts
Expand Up @@ -189,11 +189,46 @@ export interface Preflight<Theme extends {} = {}> {

export type BlocklistRule = string | RegExp

export interface VariantHandlerContext {
/**
* Rewrite the output selector. Often be used to append pesudo classes or parents.
*/
selector: string
/**
* Rewrite the output css body. The input come in [key,value][] pairs.
*/
entries: CSSEntries
/**
* Provide a parent selector(e.g. media query) to the output css.
*/
parent?: string
/**
* Provide order to the `parent` parent selector within layer.
*/
parentOrder?: number
/**
* Override layer to the output css.
*/
layer?: string
/**
* Order in which the variant is sorted within single rule.
*/
sort?: number
}

export interface VariantHandler {
/**
* Callback to process the handler.
*/
handle?: (input: VariantHandlerContext, next: (input: VariantHandlerContext) => VariantHandlerContext) => VariantHandlerContext
/**
* The result rewritten selector for the next round of matching
*/
matcher: string
/**
* Order in which the variant is applied to selector.
*/
order?: number
/**
* Rewrite the output selector. Often be used to append pesudo classes or parents.
*/
Expand All @@ -206,10 +241,6 @@ export interface VariantHandler {
* Provide a parent selector(e.g. media query) to the output css.
*/
parent?: string | [string, number] | undefined
/**
* Order in which the variant is applied to selector.
*/
order?: number
/**
* Order in which the variant is sorted within single rule.
*/
Expand Down
17 changes: 17 additions & 0 deletions test/__snapshots__/variant-handler.test.ts.snap
@@ -0,0 +1,17 @@
// Vitest Snapshot v1

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;}"
`;

exports[`variants > variant context is propagated 1`] = `
"/* layer: default */
.foo{name:bar;}
/* layer: variant */
@supports{
.selector{name:bar !important;}
}"
`;
91 changes: 91 additions & 0 deletions test/variant-handler.test.ts
@@ -0,0 +1,91 @@
import { createGenerator } from '@unocss/core'
import { describe, expect, test } from 'vitest'

describe('variants', () => {
test('variant context is propagated', async () => {
const uno = createGenerator({
rules: [
['foo', { name: 'bar' }],
],
variants: [
{
match(input) {
const match = input.match(/^var:/)
if (match) {
return {
matcher: input.slice(match[0].length),
handle: (input, next) => next({
selector: '.selector',
entries: input.entries.map((entry) => {
entry[1] += ' !important'
return entry
}),
parent: '@supports',
layer: 'variant',
}),
}
}
},
},
],
})

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

expect(css).toMatchSnapshot()
})

test('variant can stack', async () => {
const uno = createGenerator({
rules: [
['foo', { name: 'bar' }],
],
variants: [
{
multiPass: true,
match(input) {
const match = input.match(/^(first|second|third):/)
if (match) {
return {
matcher: input.slice(match[0].length),
handle: (input, next) => next({
...input,
selector: `${input.selector} > :${match[1]}`,
}),
}
}
},
},
{
multiPass: true,
match(input) {
const match = input.match(/^(one|two|three):/)
if (match) {
return {
matcher: input.slice(match[0].length),
handle: (input, next) => {
const result = next(input)
return {
...result,
selector: `${result.selector} + :${match[1]}`,
}
},
}
}
},
},
],
})

const { css } = await uno.generate([
'first:second:third:foo',
'one:two:three:foo',
'first:three:two:foo',
].join(' '), { preflights: false })

expect(css).toMatchSnapshot()
})
})

0 comments on commit 7c88219

Please sign in to comment.