diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index f95eb6ef1a..df502a3506 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -49,7 +49,7 @@ export function resolveConfig( const layers = Object.assign(DEFAULT_LAYERS, ...rawPresets.map(i => i.layers), userConfig.layers) - function mergePresets(key: T): Required>[T] { + function mergePresets(key: T): Required>[T] { return uniq([ ...sortedPresets.flatMap(p => toArray(p[key] || []) as any[]), ...toArray(config[key] || []) as any[], @@ -95,6 +95,10 @@ export function resolveConfig( .sort((a, b) => (a.order || 0) - (b.order || 0)), } + let separators = toArray(mergePresets('separators')) + if (!separators.length) + separators = [':', '-'] + return { mergeSelectors: true, warn: true, @@ -117,5 +121,6 @@ export function resolveConfig( shortcuts: resolveShortcuts(mergePresets('shortcuts')).reverse(), extractors, safelist: mergePresets('safelist'), + separators, } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 65c42210e3..8a793c1449 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -309,6 +309,13 @@ export interface ConfigBase { */ rules?: Rule[] + /** + * Variant separator + * + * @default [':', '-'] + */ + separators?: Arrayable + /** * Variants that preprocess the selectors, * having the ability to rewrite the CSS object. @@ -661,6 +668,7 @@ RequiredByKey, 'mergeSelectors' | 'theme' | 'rules' | 'variant templates: (AutoCompleteFunction | AutoCompleteTemplate)[] extractors: AutoCompleteExtractor[] } + separators: string[] } export interface GenerateResult { diff --git a/packages/core/src/utils/variantGroup.ts b/packages/core/src/utils/variantGroup.ts index 9a37823781..b8a9044bc8 100644 --- a/packages/core/src/utils/variantGroup.ts +++ b/packages/core/src/utils/variantGroup.ts @@ -1,11 +1,19 @@ import type MagicString from 'magic-string' -export const regexClassGroup = /((?:[!@\w+:_/-]|\[&?>?:?.*\])+?)([:-])\(((?:[~!\w\s:/\\,%#.$?-]|\[.*?\])+?)\)(?!\s*?=>)/gm +const regexCache: Record = {} + +export function makeRegexClassGroup(separators = ['-', ':']) { + const key = separators.join('|') + if (!regexCache[key]) + regexCache[key] = new RegExp(`((?:[!@\\w+:_/-]|\\[&?>?:?.*\\])+?)(${key})\\(((?:[~!\\w\\s:/\\\\,%#.$?-]|\\[.*?\\])+?)\\)(?!\\s*?=>)`, 'gm') + regexCache[key].lastIndex = 0 + return regexCache[key] +} export function expandVariantGroup(str: string, separators?: string[], depth?: number): string export function expandVariantGroup(str: MagicString, separators?: string[], depth?: number): MagicString export function expandVariantGroup(str: string | MagicString, separators = ['-', ':'], depth = 5) { - regexClassGroup.lastIndex = 0 + const regexClassGroup = makeRegexClassGroup(separators) let hasChanged = false let content = str.toString() do { diff --git a/packages/preset-mini/src/_utils/variants.ts b/packages/preset-mini/src/_utils/variants.ts index ea51ea15b9..a993e3a7f5 100644 --- a/packages/preset-mini/src/_utils/variants.ts +++ b/packages/preset-mini/src/_utils/variants.ts @@ -3,10 +3,13 @@ import { escapeRegExp } from '@unocss/core' import { getBracket } from '../utils' export const variantMatcher = (name: string, handler: (input: VariantHandlerContext) => Record): VariantObject => { - const re = new RegExp(`^${escapeRegExp(name)}[:-]`) + let re: RegExp return { name, - match(input) { + match(input, ctx) { + if (!re) + re = new RegExp(`^${escapeRegExp(name)}(?:${ctx.generator.config.separators.join('|')})`) + const match = input.match(re) if (match) { return { @@ -23,10 +26,13 @@ export const variantMatcher = (name: string, handler: (input: VariantHandlerCont } export const variantParentMatcher = (name: string, parent: string): VariantObject => { - const re = new RegExp(`^${escapeRegExp(name)}[:-]`) + let re: RegExp return { name, - match(input) { + match(input, ctx) { + if (!re) + re = new RegExp(`^${escapeRegExp(name)}(?:${ctx.generator.config.separators.join('|')})`) + const match = input.match(re) if (match) { return { diff --git a/packages/preset-mini/src/_variants/aria.ts b/packages/preset-mini/src/_variants/aria.ts index 26caf67468..ee588cab4d 100644 --- a/packages/preset-mini/src/_variants/aria.ts +++ b/packages/preset-mini/src/_variants/aria.ts @@ -4,11 +4,11 @@ import { handler as h, variantGetParameter } from '../utils' export const variantAria: VariantObject = { name: 'aria', - match(matcher, { theme }: VariantContext) { - const variant = variantGetParameter('aria-', matcher, [':', '-']) + match(matcher, ctx: VariantContext) { + const variant = variantGetParameter('aria-', matcher, ctx.generator.config.separators) if (variant) { const [match, rest] = variant - const aria = h.bracket(match) ?? theme.aria?.[match] ?? '' + const aria = h.bracket(match) ?? ctx.theme.aria?.[match] ?? '' if (aria) { return { matcher: rest, diff --git a/packages/preset-mini/src/_variants/breakpoints.ts b/packages/preset-mini/src/_variants/breakpoints.ts index a2e208fb87..3a19aeee68 100644 --- a/packages/preset-mini/src/_variants/breakpoints.ts +++ b/packages/preset-mini/src/_variants/breakpoints.ts @@ -1,8 +1,5 @@ -import type { Variant } from '@unocss/core' +import type { VariantObject } from '@unocss/core' import { resolveBreakpoints } from '../utils' -import type { Theme } from '../theme' - -const regexCache: Record = {} export const calcMaxWidthBySize = (size: string) => { const value = size.match(/^-?[0-9]+\.?[0-9]*/)?.[0] || '' @@ -11,69 +8,72 @@ export const calcMaxWidthBySize = (size: string) => { return Number.isNaN(maxWidth) ? size : `${maxWidth}${unit}` } -export const variantBreakpoints: Variant = { - name: 'breakpoints', - match(matcher, context) { - const variantEntries: Array<[string, string, number]> - = Object.entries(resolveBreakpoints(context) ?? {}).map(([point, size], idx) => [point, size, idx]) - for (const [point, size, idx] of variantEntries) { - if (!regexCache[point]) - regexCache[point] = new RegExp(`^((?:[al]t-)?${point}[:-])`) +export const variantBreakpoints = (): VariantObject => { + const regexCache: Record = {} + return { + name: 'breakpoints', + match(matcher, context) { + const variantEntries: Array<[string, string, number]> + = Object.entries(resolveBreakpoints(context) ?? {}).map(([point, size], idx) => [point, size, idx]) + for (const [point, size, idx] of variantEntries) { + if (!regexCache[point]) + regexCache[point] = new RegExp(`^((?:[al]t-)?${point}(?:${context.generator.config.separators.join('|')}))`) - const match = matcher.match(regexCache[point]) - if (!match) - continue + const match = matcher.match(regexCache[point]) + if (!match) + continue - const [, pre] = match + const [, pre] = match - const m = matcher.slice(pre.length) - // container rule is responsive, but also is breakpoint aware - // it is handled on its own module (container.ts) and so we - // exclude it from here - if (m === 'container') - continue + const m = matcher.slice(pre.length) + // container rule is responsive, but also is breakpoint aware + // it is handled on its own module (container.ts) and so we + // exclude it from here + if (m === 'container') + continue - const isLtPrefix = pre.startsWith('lt-') - const isAtPrefix = pre.startsWith('at-') + const isLtPrefix = pre.startsWith('lt-') + const isAtPrefix = pre.startsWith('at-') - let order = 1000 // parseInt(size) + let order = 1000 // parseInt(size) - if (isLtPrefix) { - order -= (idx + 1) - return { - matcher: m, - handle: (input, next) => next({ - ...input, - parent: `${input.parent ? `${input.parent} $$ ` : ''}@media (max-width: ${calcMaxWidthBySize(size)})`, - parentOrder: order, - }), + if (isLtPrefix) { + order -= (idx + 1) + return { + matcher: m, + handle: (input, next) => next({ + ...input, + parent: `${input.parent ? `${input.parent} $$ ` : ''}@media (max-width: ${calcMaxWidthBySize(size)})`, + parentOrder: order, + }), + } } - } - order += (idx + 1) + order += (idx + 1) + + // support for windicss @ => last breakpoint will not have the upper bound + if (isAtPrefix && idx < variantEntries.length - 1) { + return { + matcher: m, + handle: (input, next) => next({ + ...input, + parent: `${input.parent ? `${input.parent} $$ ` : ''}@media (min-width: ${size}) and (max-width: ${calcMaxWidthBySize(variantEntries[idx + 1][1])})`, + parentOrder: order, + }), + } + } - // support for windicss @ => last breakpoint will not have the upper bound - if (isAtPrefix && idx < variantEntries.length - 1) { return { matcher: m, handle: (input, next) => next({ ...input, - parent: `${input.parent ? `${input.parent} $$ ` : ''}@media (min-width: ${size}) and (max-width: ${calcMaxWidthBySize(variantEntries[idx + 1][1])})`, + parent: `${input.parent ? `${input.parent} $$ ` : ''}@media (min-width: ${size})`, parentOrder: order, }), } } - - return { - matcher: m, - handle: (input, next) => next({ - ...input, - parent: `${input.parent ? `${input.parent} $$ ` : ''}@media (min-width: ${size})`, - parentOrder: order, - }), - } - } - }, - multiPass: true, - autocomplete: '(at-|lt-|)$breakpoints:', + }, + multiPass: true, + autocomplete: '(at-|lt-|)$breakpoints:', + } } diff --git a/packages/preset-mini/src/_variants/combinators.ts b/packages/preset-mini/src/_variants/combinators.ts index bea85d797c..ad69c5923a 100644 --- a/packages/preset-mini/src/_variants/combinators.ts +++ b/packages/preset-mini/src/_variants/combinators.ts @@ -3,13 +3,14 @@ import { handler as h, variantGetBracket } from '../utils' const scopeMatcher = (name: string, combinator: string): VariantObject => ({ name: `combinator:${name}`, - match(matcher) { + match(matcher, ctx) { if (!matcher.startsWith(name)) return - let body = variantGetBracket(`${name}-`, matcher, [':', '-']) + const separators = ctx.generator.config.separators + let body = variantGetBracket(`${name}-`, matcher, separators) if (!body) { - for (const separator of [':', '-']) { + for (const separator of separators) { if (matcher.startsWith(`${name}${separator}`)) { body = ['', matcher.slice(name.length + separator.length)] break diff --git a/packages/preset-mini/src/_variants/container.ts b/packages/preset-mini/src/_variants/container.ts index 8d7062810f..d63cb56670 100644 --- a/packages/preset-mini/src/_variants/container.ts +++ b/packages/preset-mini/src/_variants/container.ts @@ -5,11 +5,11 @@ import { handler as h, variantGetParameter } from '../utils' export const variantContainerQuery: VariantObject = { name: '@', - match(matcher, { theme }: VariantContext) { + match(matcher, ctx: VariantContext) { if (matcher.startsWith('@container')) return - const variant = variantGetParameter('@', matcher, [':', '-']) + const variant = variantGetParameter('@', matcher, ctx.generator.config.separators) if (variant) { const [match, rest, label] = variant const unbracket = h.bracket(match) @@ -20,7 +20,7 @@ export const variantContainerQuery: VariantObject = { container = `(min-width: ${minWidth})` } else { - container = theme.containers?.[match] ?? '' + container = ctx.theme.containers?.[match] ?? '' } if (container) { diff --git a/packages/preset-mini/src/_variants/data.ts b/packages/preset-mini/src/_variants/data.ts index 8bd9315920..08ca050e10 100644 --- a/packages/preset-mini/src/_variants/data.ts +++ b/packages/preset-mini/src/_variants/data.ts @@ -4,11 +4,11 @@ import { handler as h, variantGetParameter } from '../utils' export const variantDataAttribute: VariantObject = { name: 'data', - match(matcher, { theme }: VariantContext) { - const variant = variantGetParameter('data-', matcher, [':', '-']) + match(matcher, ctx: VariantContext) { + const variant = variantGetParameter('data-', matcher, ctx.generator.config.separators) if (variant) { const [match, rest] = variant - const dataAttribute = h.bracket(match) ?? theme.data?.[match] ?? '' + const dataAttribute = h.bracket(match) ?? ctx.theme.data?.[match] ?? '' if (dataAttribute) { return { matcher: rest, diff --git a/packages/preset-mini/src/_variants/default.ts b/packages/preset-mini/src/_variants/default.ts index d9426d92b2..faf620f14b 100644 --- a/packages/preset-mini/src/_variants/default.ts +++ b/packages/preset-mini/src/_variants/default.ts @@ -23,15 +23,15 @@ export const variants = (options: PresetMiniOptions): Variant[] => [ variantSelector, variantInternalLayer, variantNegative, - variantImportant, + variantImportant(), variantSupports, variantPrint, variantCustomMedia, - variantBreakpoints, + variantBreakpoints(), ...variantCombinators, - variantPseudoClassesAndElements, - variantPseudoClassFunctions, + variantPseudoClassesAndElements(), + variantPseudoClassFunctions(), ...variantTaggedPseudoClasses(options), partClasses, diff --git a/packages/preset-mini/src/_variants/important.ts b/packages/preset-mini/src/_variants/important.ts index ae8cfd7f2c..9640146750 100644 --- a/packages/preset-mini/src/_variants/important.ts +++ b/packages/preset-mini/src/_variants/important.ts @@ -1,27 +1,32 @@ -import type { Variant } from '@unocss/core' +import type { VariantObject } from '@unocss/core' -export const variantImportant: Variant = { - name: 'important', - match(matcher) { - let base: string | undefined +export const variantImportant = (): VariantObject => { + let re: RegExp + return { + name: 'important', + match(matcher, ctx) { + if (!re) + re = new RegExp(`^(important(?:${ctx.generator.config.separators.join('|')})|!)`) - const match = matcher.match(/^(important[:-]|!)/) - if (match) - base = matcher.slice(match[0].length) - else if (matcher.endsWith('!')) - base = matcher.slice(0, -1) + let base: string | undefined + const match = matcher.match(re) + if (match) + base = matcher.slice(match[0].length) + else if (matcher.endsWith('!')) + base = matcher.slice(0, -1) - if (base) { - return { - matcher: base, - body: (body) => { - body.forEach((v) => { - if (v[1]) - v[1] += ' !important' - }) - return body - }, + if (base) { + return { + matcher: base, + body: (body) => { + body.forEach((v) => { + if (v[1]) + v[1] += ' !important' + }) + return body + }, + } } - } - }, + }, + } } diff --git a/packages/preset-mini/src/_variants/media.ts b/packages/preset-mini/src/_variants/media.ts index 90fcd1259a..9799519df2 100644 --- a/packages/preset-mini/src/_variants/media.ts +++ b/packages/preset-mini/src/_variants/media.ts @@ -1,19 +1,19 @@ -import type { Variant, VariantContext, VariantObject } from '@unocss/core' +import type { VariantContext, VariantObject } from '@unocss/core' import type { Theme } from '../theme' import { handler as h, variantGetParameter, variantParentMatcher } from '../utils' -export const variantPrint: Variant = variantParentMatcher('print', '@media print') +export const variantPrint: VariantObject = variantParentMatcher('print', '@media print') export const variantCustomMedia: VariantObject = { name: 'media', - match(matcher, { theme }: VariantContext) { - const variant = variantGetParameter('media-', matcher, [':', '-']) + match(matcher, ctx: VariantContext) { + const variant = variantGetParameter('media-', matcher, ctx.generator.config.separators) if (variant) { const [match, rest] = variant let media = h.bracket(match) ?? '' if (media === '') - media = theme.media?.[match] ?? '' + media = ctx.theme.media?.[match] ?? '' if (media) { return { diff --git a/packages/preset-mini/src/_variants/misc.ts b/packages/preset-mini/src/_variants/misc.ts index 3392bbbece..75958a0c0e 100644 --- a/packages/preset-mini/src/_variants/misc.ts +++ b/packages/preset-mini/src/_variants/misc.ts @@ -3,8 +3,8 @@ import { getBracket, handler as h, variantGetBracket, variantGetParameter } from export const variantSelector: Variant = { name: 'selector', - match(matcher) { - const variant = variantGetBracket('selector-', matcher, [':', '-']) + match(matcher, ctx) { + const variant = variantGetBracket('selector-', matcher, ctx.generator.config.separators) if (variant) { const [match, rest] = variant const selector = h.bracket(match) @@ -20,8 +20,8 @@ export const variantSelector: Variant = { export const variantCssLayer: Variant = { name: 'layer', - match(matcher) { - const variant = variantGetParameter('layer-', matcher, [':', '-']) + match(matcher, ctx) { + const variant = variantGetParameter('layer-', matcher, ctx.generator.config.separators) if (variant) { const [match, rest] = variant const layer = h.bracket(match) ?? match @@ -40,8 +40,8 @@ export const variantCssLayer: Variant = { export const variantInternalLayer: Variant = { name: 'uno-layer', - match(matcher) { - const variant = variantGetParameter('uno-layer-', matcher, [':', '-']) + match(matcher, ctx) { + const variant = variantGetParameter('uno-layer-', matcher, ctx.generator.config.separators) if (variant) { const [match, rest] = variant const layer = h.bracket(match) ?? match @@ -57,8 +57,8 @@ export const variantInternalLayer: Variant = { export const variantScope: Variant = { name: 'scope', - match(matcher) { - const variant = variantGetBracket('scope-', matcher, [':', '-']) + match(matcher, ctx) { + const variant = variantGetBracket('scope-', matcher, ctx.generator.config.separators) if (variant) { const [match, rest] = variant const scope = h.bracket(match) @@ -74,7 +74,7 @@ export const variantScope: Variant = { export const variantVariables: Variant = { name: 'variables', - match(matcher) { + match(matcher, ctx) { if (!matcher.startsWith('[')) return @@ -83,7 +83,7 @@ export const variantVariables: Variant = { return let newMatcher: string | undefined - for (const separator of [':', '-']) { + for (const separator of ctx.generator.config.separators) { if (rest.startsWith(separator)) { newMatcher = rest.slice(separator.length) break diff --git a/packages/preset-mini/src/_variants/pseudo.ts b/packages/preset-mini/src/_variants/pseudo.ts index e018cb0b17..4bd789b7c7 100644 --- a/packages/preset-mini/src/_variants/pseudo.ts +++ b/packages/preset-mini/src/_variants/pseudo.ts @@ -84,8 +84,9 @@ const sortValue = (pseudo: string) => { const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: string): VariantObject => { const rawRE = new RegExp(`^(${escapeRegExp(parent)}:)(\\S+)${escapeRegExp(combinator)}\\1`) - const pseudoRE = new RegExp(`^${tag}-(?:(?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesStr}))(?:(/\\w+))?[:-]`) - const pseudoColonRE = new RegExp(`^${tag}-(?:(?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesColonStr}))(?:(/\\w+))?[:]`) + let splitRE: RegExp + let pseudoRE: RegExp + let pseudoColonRE: RegExp const matchBracket = (input: string) => { const body = variantGetBracket(`${tag}-`, input, []) @@ -97,7 +98,7 @@ const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: strin if (bracketValue == null) return - const label = rest.split(/[:-]/, 1)?.[0] ?? '' + const label = rest.split(splitRE, 1)?.[0] ?? '' const prefix = `${parent}${escapeSelector(label)}` return [ label, @@ -127,7 +128,13 @@ const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: strin return { name: `pseudo:${tag}`, - match(input) { + match(input, ctx) { + if (!(splitRE && pseudoRE && pseudoColonRE)) { + splitRE = new RegExp(`(?:${ctx.generator.config.separators.join('|')})`) + pseudoRE = new RegExp(`^${tag}-(?:(?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesStr}))(?:(/\\w+))?(?:${ctx.generator.config.separators.join('|')})`) + pseudoColonRE = new RegExp(`^${tag}-(?:(?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesColonStr}))(?:(/\\w+))?(?:${ctx.generator.config.separators.filter(x => x !== '-').join('|')})`) + } + if (!input.startsWith(tag)) return @@ -154,54 +161,68 @@ const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: strin const PseudoClassesAndElementsStr = Object.entries(PseudoClasses).map(([key]) => key).join('|') const PseudoClassesAndElementsColonStr = Object.entries(PseudoClassesColon).map(([key]) => key).join('|') -const PseudoClassesAndElementsRE = new RegExp(`^(${PseudoClassesAndElementsStr})[:-]`) -const PseudoClassesAndElementsColonRE = new RegExp(`^(${PseudoClassesAndElementsColonStr})[:]`) -export const variantPseudoClassesAndElements: VariantObject = { - name: 'pseudo', - match(input) { - const match = input.match(PseudoClassesAndElementsRE) || input.match(PseudoClassesAndElementsColonRE) - if (match) { - const pseudo = PseudoClasses[match[1]] || PseudoClassesColon[match[1]] || `:${match[1]}` - return { - matcher: input.slice(match[0].length), - handle: (input, next) => { - const selectors = pseudo.startsWith('::') - ? { - pseudo: `${input.pseudo}${pseudo}`, - } - : { - selector: `${input.selector}${pseudo}`, - } - - return next({ - ...input, - ...selectors, - sort: sortValue(match[1]), - }) - }, +export const variantPseudoClassesAndElements = (): VariantObject => { + let PseudoClassesAndElementsRE: RegExp + let PseudoClassesAndElementsColonRE: RegExp + return { + name: 'pseudo', + match(input, ctx) { + if (!(PseudoClassesAndElementsRE && PseudoClassesAndElementsRE)) { + PseudoClassesAndElementsRE = new RegExp(`^(${PseudoClassesAndElementsStr})(?:${ctx.generator.config.separators.join('|')})`) + PseudoClassesAndElementsColonRE = new RegExp(`^(${PseudoClassesAndElementsColonStr})(?:${ctx.generator.config.separators.filter(x => x !== '-').join('|')})`) } - } - }, - multiPass: true, - autocomplete: `(${PseudoClassesAndElementsStr}):`, + + const match = input.match(PseudoClassesAndElementsRE) || input.match(PseudoClassesAndElementsColonRE) + if (match) { + const pseudo = PseudoClasses[match[1]] || PseudoClassesColon[match[1]] || `:${match[1]}` + return { + matcher: input.slice(match[0].length), + handle: (input, next) => { + const selectors = pseudo.startsWith('::') + ? { + pseudo: `${input.pseudo}${pseudo}`, + } + : { + selector: `${input.selector}${pseudo}`, + } + + return next({ + ...input, + ...selectors, + sort: sortValue(match[1]), + }) + }, + } + } + }, + multiPass: true, + autocomplete: `(${PseudoClassesAndElementsStr}|${PseudoClassesAndElementsColonStr}):`, + } } -const PseudoClassFunctionsRE = new RegExp(`^(${PseudoClassFunctionsStr})-(${PseudoClassesStr})[:-]`) -const PseudoClassColonFunctionsRE = new RegExp(`^(${PseudoClassFunctionsStr})-(${PseudoClassesColonStr})[:]`) -export const variantPseudoClassFunctions: VariantObject = { - match(input) { - const match = input.match(PseudoClassFunctionsRE) || input.match(PseudoClassColonFunctionsRE) - if (match) { - const fn = match[1] - const pseudo = PseudoClasses[match[2]] || PseudoClassesColon[match[2]] || `:${match[2]}` - return { - matcher: input.slice(match[0].length), - selector: s => `${s}:${fn}(${pseudo})`, +export const variantPseudoClassFunctions = (): VariantObject => { + let PseudoClassFunctionsRE: RegExp + let PseudoClassColonFunctionsRE: RegExp + return { + match(input, ctx) { + if (!(PseudoClassFunctionsRE && PseudoClassColonFunctionsRE)) { + PseudoClassFunctionsRE = new RegExp(`^(${PseudoClassFunctionsStr})-(${PseudoClassesStr})(?:${ctx.generator.config.separators.join('|')})`) + PseudoClassColonFunctionsRE = new RegExp(`^(${PseudoClassFunctionsStr})-(${PseudoClassesColonStr})(?:${ctx.generator.config.separators.filter(x => x !== '-').join('|')})`) } - } - }, - multiPass: true, - autocomplete: `(${PseudoClassFunctionsStr})-(${PseudoClassesStr}|${PseudoClassesColonStr}):`, + + const match = input.match(PseudoClassFunctionsRE) || input.match(PseudoClassColonFunctionsRE) + if (match) { + const fn = match[1] + const pseudo = PseudoClasses[match[2]] || PseudoClassesColon[match[2]] || `:${match[2]}` + return { + matcher: input.slice(match[0].length), + selector: s => `${s}:${fn}(${pseudo})`, + } + } + }, + multiPass: true, + autocomplete: `(${PseudoClassFunctionsStr})-(${PseudoClassesStr}|${PseudoClassesColonStr}):`, + } } export const variantTaggedPseudoClasses = (options: PresetMiniOptions = {}): VariantObject[] => { diff --git a/packages/preset-mini/src/_variants/supports.ts b/packages/preset-mini/src/_variants/supports.ts index 14e7d5ff73..1aba48782c 100644 --- a/packages/preset-mini/src/_variants/supports.ts +++ b/packages/preset-mini/src/_variants/supports.ts @@ -4,14 +4,14 @@ import { handler as h, variantGetParameter } from '../utils' export const variantSupports: VariantObject = { name: 'supports', - match(matcher, { theme }: VariantContext) { - const variant = variantGetParameter('supports-', matcher, [':', '-']) + match(matcher, ctx: VariantContext) { + const variant = variantGetParameter('supports-', matcher, ctx.generator.config.separators) if (variant) { const [match, rest] = variant let supports = h.bracket(match) ?? '' if (supports === '') - supports = theme.supports?.[match] ?? '' + supports = ctx.theme.supports?.[match] ?? '' if (supports) { return { diff --git a/packages/preset-uno/src/index.ts b/packages/preset-uno/src/index.ts index 5012d61e59..5aa6a43031 100644 --- a/packages/preset-uno/src/index.ts +++ b/packages/preset-uno/src/index.ts @@ -20,7 +20,7 @@ export const presetUno = (options: PresetUnoOptions = {}): Preset => { shortcuts, variants: [ ...variants(options), - variantColorMix, + variantColorMix(), ], options, postprocess: options.variablePrefix && options.variablePrefix !== 'un-' diff --git a/packages/preset-uno/src/variants/mix.ts b/packages/preset-uno/src/variants/mix.ts index 54a176e17b..e599989132 100644 --- a/packages/preset-uno/src/variants/mix.ts +++ b/packages/preset-uno/src/variants/mix.ts @@ -1,4 +1,4 @@ -import type { CSSColorValue, Variant } from '@unocss/core' +import type { CSSColorValue, VariantObject } from '@unocss/core' import { colorToString, parseCssColor } from '@unocss/preset-mini/utils' const mixComponent = (v1: string | number, v2: string | number, w: string | number) => `calc(${v2} + (${v1} - ${v2}) * ${w} / 100)` @@ -59,24 +59,33 @@ const fns: Record { - const m = matcher.match(/^mix-(tint|shade|shift)-(-?\d{1,3})[-:]/) - if (m) { - return { - matcher: matcher.slice(m[0].length), - body: (body) => { - body.forEach((v) => { - if (v[1]) { - const color = parseCssColor(`${v[1]}`) - if (color) { - const mixed = fns[m[1]](color, m[2]) - if (mixed) - v[1] = colorToString(mixed) - } - } - }) - return body - }, - } +export const variantColorMix = (): VariantObject => { + let re: RegExp + return { + name: 'mix', + match(matcher, ctx) { + if (!re) + re = new RegExp(`^mix-(tint|shade|shift)-(-?\\d{1,3})(?:${ctx.generator.config.separators.join('|')})`) + + const m = matcher.match(re) + if (m) { + return { + matcher: matcher.slice(m[0].length), + body: (body) => { + body.forEach((v) => { + if (v[1]) { + const color = parseCssColor(`${v[1]}`) + if (color) { + const mixed = fns[m[1]](color, m[2]) + if (mixed) + v[1] = colorToString(mixed) + } + } + }) + return body + }, + } + } + }, } } diff --git a/packages/shared-common/src/index.ts b/packages/shared-common/src/index.ts index b865693342..94da641eb2 100644 --- a/packages/shared-common/src/index.ts +++ b/packages/shared-common/src/index.ts @@ -1,5 +1,5 @@ import type { ExtractorContext, UnoGenerator } from '@unocss/core' -import { arbitraryPropertyRE, escapeRegExp, isAttributifySelector, regexClassGroup } from '@unocss/core' +import { arbitraryPropertyRE, escapeRegExp, isAttributifySelector, makeRegexClassGroup } from '@unocss/core' import MagicString from 'magic-string' // https://github.com/dsblv/string-replace-async/blob/main/index.js @@ -80,7 +80,7 @@ export function getPlainClassMatchedPositionsForPug(codeSplit: string, matchedPl return result } -export function getMatchedPositions(code: string, matched: string[], hasVariantGroup = false, isPug = false) { +export function getMatchedPositions(code: string, matched: string[], hasVariantGroup = false, isPug = false, uno: UnoGenerator | undefined = undefined) { const result: [number, number, string][] = [] const attributify: RegExpMatchArray[] = [] const plain = new Set() @@ -120,7 +120,7 @@ export function getMatchedPositions(code: string, matched: string[], hasVariantG // highlight for variant group if (hasVariantGroup) { - Array.from(code.matchAll(regexClassGroup)) + Array.from(code.matchAll(makeRegexClassGroup(uno?.config.separators))) .forEach((match) => { const [, pre, sep, body] = match const index = match.index! @@ -182,5 +182,5 @@ export async function getMatchedPositionsFromCode(uno: UnoGenerator, code: strin const { pug, code: pugCode } = await isPug(uno, s.toString(), id) const result = await uno.generate(pug ? pugCode : s.toString(), { preflights: false }) - return getMatchedPositions(code, [...result.matched], hasVariantGroup, pug) + return getMatchedPositions(code, [...result.matched], hasVariantGroup, pug, uno) } diff --git a/test/__snapshots__/parser.test.ts.snap b/test/__snapshots__/parser.test.ts.snap new file mode 100644 index 0000000000..e0b83647d5 --- /dev/null +++ b/test/__snapshots__/parser.test.ts.snap @@ -0,0 +1,6 @@ +// Vitest Snapshot v1 + +exports[`split string with custom separator 1`] = ` +"/* layer: default */ +.backdrop__shadow-green::backdrop{--un-shadow-opacity:1;--un-shadow-color:rgba(74,222,128,var(--un-shadow-opacity));}" +`; diff --git a/test/parser.test.ts b/test/parser.test.ts new file mode 100644 index 0000000000..db978ae560 --- /dev/null +++ b/test/parser.test.ts @@ -0,0 +1,27 @@ +import { createGenerator } from '@unocss/core' +import presetUno from '@unocss/preset-uno' +import { expect, it } from 'vitest' + +it('split string with custom separator', async () => { + const uno = createGenerator({ + presets: [ + presetUno(), + ], + separators: ['__'], + }) + + const { css } = await uno.generate('backdrop__shadow-green', { preflights: false }) + expect(css).toMatchSnapshot() +}) + +it('unable to generate token variant with explicit separator without dash', async () => { + const uno = createGenerator({ + presets: [ + presetUno(), + ], + separators: '-', + }) + + const { css } = await uno.generate('backdrop-shadow-green', { preflights: false }) + expect(css).eql('') +})