From 5c05c4ebbd9bf7c7f8222deb8c49fa1abe18ab79 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 2 Jul 2022 18:56:27 +0800 Subject: [PATCH] feat(core): support inline css in shortcuts --- packages/core/src/generator/index.ts | 41 +++++++++++++---------- packages/core/src/types.ts | 7 ++-- packages/core/src/utils/basic.ts | 4 +++ packages/core/src/utils/object.ts | 7 ++-- test/__snapshots__/shortcuts.test.ts.snap | 8 +++++ test/shortcuts.test.ts | 12 +++++++ 6 files changed, 55 insertions(+), 24 deletions(-) diff --git a/packages/core/src/generator/index.ts b/packages/core/src/generator/index.ts index a7f73f0d3e..8c7ac3d5b0 100644 --- a/packages/core/src/generator/index.ts +++ b/packages/core/src/generator/index.ts @@ -1,7 +1,7 @@ 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, VariantHandlerContext, VariantMatchedResult } from '../types' +import type { CSSEntries, CSSObject, ExtractorContext, GenerateOptions, GenerateResult, ParsedUtil, PreflightContext, PreparedRule, RawUtil, ResolvedConfig, Rule, RuleContext, RuleMeta, Shortcut, ShortcutValue, 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 { CONTROL_SHORTCUT_NO_MERGE, TwoKeyMap, e, entriesToCss, expandVariantGroup, isRawUtil, isStaticShortcut, isString, noop, normalizeCSSEntries, normalizeCSSValues, notNull, uniq, warnOnce } from '../utils' import { version } from '../../package.json' import { LAYER_DEFAULT, LAYER_PREFLIGHTS } from '../constants' @@ -134,7 +134,7 @@ export class UnoGenerator { minify = false, } = options - const tokens: Readonly> = typeof input === 'string' + const tokens: Readonly> = isString(input) ? await this.applyExtractors(input, id) : Array.isArray(input) ? new Set(input) @@ -323,7 +323,7 @@ export class UnoGenerator { let handler = v.match(processed, context) if (!handler) continue - if (typeof handler === 'string') + if (isString(handler)) handler = { matcher: handler } processed = handler.matcher handlers.unshift(handler) @@ -394,7 +394,7 @@ export class UnoGenerator { constructCustomCSS(context: Readonly, body: CSSObject | CSSEntries, overrideSelector?: string) { const normalizedBody = normalizeCSSEntries(body) - if (typeof normalizedBody === 'string') + if (isString(normalizedBody)) return normalizedBody const { selector, entries, parent } = this.applyVariants([0, overrideSelector || context.rawSelector, normalizedBody, undefined, context.variantHandlers]) @@ -405,7 +405,7 @@ export class UnoGenerator { } async parseUtil(input: string | VariantMatchedResult, context: RuleContext, internal = false): Promise<(ParsedUtil | RawUtil)[] | undefined> { - const [raw, processed, variantHandlers] = typeof input === 'string' + const [raw, processed, variantHandlers] = isString(input) ? this.matchVariants(input) : input @@ -424,7 +424,7 @@ export class UnoGenerator { const index = staticMatch[0] const entry = normalizeCSSEntries(staticMatch[1]) const meta = staticMatch[2] - if (typeof entry === 'string') + if (isString(entry)) return [[index, entry, meta]] else return [[index, raw, entry, meta, variantHandlers]] @@ -465,7 +465,7 @@ export class UnoGenerator { const entries = normalizeCSSValues(result).filter(i => i.length) if (entries.length) { return entries.map((e) => { - if (typeof e === 'string') + if (isString(e)) return [i, e, meta] else return [i, raw, e, meta, variantHandlers] @@ -495,7 +495,7 @@ export class UnoGenerator { return [parsed[0], selector, body, parent, ruleMeta, this.config.details ? context : undefined] } - expandShortcut(input: string, context: RuleContext, depth = 5): [string[], RuleMeta | undefined] | undefined { + expandShortcut(input: string, context: RuleContext, depth = 5): [ShortcutValue[], RuleMeta | undefined] | undefined { if (depth === 0) return @@ -507,7 +507,7 @@ export class UnoGenerator { : noop let meta: RuleMeta | undefined - let result: string | string[] | undefined + let result: string | ShortcutValue[] | undefined for (const s of this.config.shortcuts) { const unprefixed = s[2]?.prefix ? input.slice(s[2].prefix.length) : input if (isStaticShortcut(s)) { @@ -531,18 +531,18 @@ export class UnoGenerator { } // expand nested shotcuts - if (typeof result === 'string') + if (isString(result)) result = expandVariantGroup(result).split(/\s+/g) // expand nested shortcuts with variants if (!result) { - const [raw, inputWithoutVariant] = typeof input === 'string' + const [raw, inputWithoutVariant] = isString(input) ? this.matchVariants(input) : input if (raw !== inputWithoutVariant) { const expanded = this.expandShortcut(inputWithoutVariant, context, depth - 1) if (expanded) - result = expanded[0].map(item => raw.replace(inputWithoutVariant, item)) + result = expanded[0].map(item => isString(item) ? raw.replace(inputWithoutVariant, item) : item) } } @@ -551,7 +551,7 @@ export class UnoGenerator { return [ result - .flatMap(r => this.expandShortcut(r, context, depth - 1)?.[0] || [r]) + .flatMap(r => (isString(r) ? this.expandShortcut(r, context, depth - 1)?.[0] : undefined) || [r]) .filter(Boolean), meta, ] @@ -560,17 +560,22 @@ export class UnoGenerator { async stringifyShortcuts( parent: VariantMatchedResult, context: RuleContext, - expanded: string[], + expanded: ShortcutValue[], meta: RuleMeta = { layer: this.config.shortcutsLayer }, ): Promise { const selectorMap = new TwoKeyMap() const parsed = ( await Promise.all(uniq(expanded) .map(async (i) => { - const result = await this.parseUtil(i, context, true) + const result = isString(i) + // rule + ? await this.parseUtil(i, context, true) as ParsedUtil[] + // inline CSS value in shortcut + : [[Infinity, '{inline}', normalizeCSSEntries(i), undefined, []] as ParsedUtil] + if (!result) warnOnce(`unmatched utility "${i}" in shortcut "${parent[1]}"`) - return (result || []) as ParsedUtil[] + return result || [] }))) .flat(1) .filter(Boolean) @@ -618,7 +623,7 @@ export class UnoGenerator { } isBlocked(raw: string) { - return !raw || this.config.blocklist.some(e => typeof e === 'string' ? e === raw : e.test(raw)) + return !raw || this.config.blocklist.some(e => isString(e) ? e === raw : e.test(raw)) } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f222e7f21e..4a87c737a1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -177,13 +177,14 @@ export type DynamicRule = [RegExp, DynamicMatcher] export type StaticRule = [string, CSSObject | CSSEntries] | [string, CSSObject | CSSEntries, RuleMeta] export type Rule = DynamicRule | StaticRule -export type DynamicShortcutMatcher = ((match: RegExpMatchArray, context: Readonly>) => (string | string [] | undefined)) +export type DynamicShortcutMatcher = ((match: RegExpMatchArray, context: Readonly>) => (string | ShortcutValue[] | undefined)) -export type StaticShortcut = [string, string | string[]] | [string, string | string[], RuleMeta] -export type StaticShortcutMap = Record +export type StaticShortcut = [string, string | ShortcutValue[]] | [string, string | ShortcutValue[], RuleMeta] +export type StaticShortcutMap = Record export type DynamicShortcut = [RegExp, DynamicShortcutMatcher] | [RegExp, DynamicShortcutMatcher, RuleMeta] export type UserShortcuts = StaticShortcutMap | (StaticShortcut | DynamicShortcut | StaticShortcutMap)[] export type Shortcut = StaticShortcut | DynamicShortcut +export type ShortcutValue = string | CSSValue export type FilterPattern = ReadonlyArray | string | RegExp | null diff --git a/packages/core/src/utils/basic.ts b/packages/core/src/utils/basic.ts index 7b1ff17be4..211564bc1e 100644 --- a/packages/core/src/utils/basic.ts +++ b/packages/core/src/utils/basic.ts @@ -10,3 +10,7 @@ export function mergeSet(target: Set, append: Set): Set { append.forEach(i => target.add(i)) return target } + +export function isString(s: any): s is string { + return typeof s === 'string' +} diff --git a/packages/core/src/utils/object.ts b/packages/core/src/utils/object.ts index 5131a1a160..c468e4dd91 100644 --- a/packages/core/src/utils/object.ts +++ b/packages/core/src/utils/object.ts @@ -1,7 +1,8 @@ import type { CSSEntries, CSSObject, CSSValue, DeepPartial, Rule, Shortcut, StaticRule, StaticShortcut } from '../types' +import { isString } from './basic' export function normalizeCSSEntries(obj: string | CSSEntries | CSSObject): string | CSSEntries { - if (typeof obj === 'string') + if (isString(obj)) return obj return (!Array.isArray(obj) ? Object.entries(obj) : obj).filter(i => i[1] != null) } @@ -101,9 +102,9 @@ export function clone(val: T): T { } export function isStaticRule(rule: Rule): rule is StaticRule { - return typeof rule[0] === 'string' + return isString(rule[0]) } export function isStaticShortcut(sc: Shortcut): sc is StaticShortcut { - return typeof sc[0] === 'string' + return isString(sc[0]) } diff --git a/test/__snapshots__/shortcuts.test.ts.snap b/test/__snapshots__/shortcuts.test.ts.snap index 2187f07fec..7e7a75d8fd 100644 --- a/test/__snapshots__/shortcuts.test.ts.snap +++ b/test/__snapshots__/shortcuts.test.ts.snap @@ -82,6 +82,14 @@ exports[`shortcuts > shortcut of nested pseudo 1`] = ` .hover\\\\:btn3:hover:hover{--un-bg-opacity:1;background-color:rgba(0,0,0,var(--un-bg-opacity));--un-text-opacity:1;color:rgba(21,94,117,var(--un-text-opacity));text-decoration-line:underline;}" `; +exports[`shortcuts > shortcut with inline body 1`] = ` +"/* layer: shortcuts */ +.hover\\\\:shortcut-inline-body:hover, +.shortcut-inline-body{padding:0.5rem;margin:3px;} +.shortcut-inline-dynamic-1{padding:0.25rem;margin:1px;} +.shortcut-inline-dynamic-2{padding:0.5rem;margin:2px;}" +`; + exports[`shortcuts > static 1`] = ` "/* layer: shortcuts */ .sh1{margin:0.75rem;padding-left:0.5rem;padding-right:0.5rem;padding-top:0.75rem;padding-bottom:0.75rem;} diff --git a/test/shortcuts.test.ts b/test/shortcuts.test.ts index 7182e61a54..02bbd0afb0 100644 --- a/test/shortcuts.test.ts +++ b/test/shortcuts.test.ts @@ -28,6 +28,8 @@ describe('shortcuts', () => { ['shortcut-hover-active-1', 'focus:bg-green-300 hover:bg-green-300 active:bg-green-300'], ['shortcut-hover-active-2', 'focus:bg-red-300 hover:bg-yellow-300 active:bg-blue-300'], ['loading', 'animate-spin duration-1000'], + ['shortcut-inline-body', ['p2', { margin: '3px' }]], + [/^shortcut-inline-dynamic-(\d)$/, ([,d]) => [`p${d}`, { margin: `${d}px` }]], ], presets: [ presetUno(), @@ -109,4 +111,14 @@ describe('shortcuts', () => { const { css } = await uno.generate('btn3 focus:btn3 hover:btn3 focus:hover:btn3', { preflights: false }) expect(css).toMatchSnapshot() }) + + test('shortcut with inline body', async () => { + const { css } = await uno.generate(` + shortcut-inline-body + hover:shortcut-inline-body + shortcut-inline-dynamic-1 + shortcut-inline-dynamic-2 + `, { preflights: false }) + expect(css).toMatchSnapshot() + }) })