Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): use recursive callback to resolve variant #1126

Merged
merged 7 commits into from Jun 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()
})
})