Skip to content

Commit

Permalink
feat(presets): configurable separator (#1791)
Browse files Browse the repository at this point in the history
  • Loading branch information
chu121su12 committed Jan 25, 2023
1 parent 3b5fad4 commit 2629e03
Show file tree
Hide file tree
Showing 20 changed files with 284 additions and 188 deletions.
7 changes: 6 additions & 1 deletion packages/core/src/config.ts
Expand Up @@ -49,7 +49,7 @@ export function resolveConfig<Theme extends {} = {}>(

const layers = Object.assign(DEFAULT_LAYERS, ...rawPresets.map(i => i.layers), userConfig.layers)

function mergePresets<T extends 'rules' | 'variants' | 'extractors' | 'shortcuts' | 'preflights' | 'preprocess' | 'postprocess' | 'extendTheme' | 'safelist'>(key: T): Required<UserConfig<Theme>>[T] {
function mergePresets<T extends 'rules' | 'variants' | 'extractors' | 'shortcuts' | 'preflights' | 'preprocess' | 'postprocess' | 'extendTheme' | 'safelist' | 'separators'>(key: T): Required<UserConfig<Theme>>[T] {
return uniq([
...sortedPresets.flatMap(p => toArray(p[key] || []) as any[]),
...toArray(config[key] || []) as any[],
Expand Down Expand Up @@ -95,6 +95,10 @@ export function resolveConfig<Theme extends {} = {}>(
.sort((a, b) => (a.order || 0) - (b.order || 0)),
}

let separators = toArray(mergePresets('separators'))
if (!separators.length)
separators = [':', '-']

return {
mergeSelectors: true,
warn: true,
Expand All @@ -117,5 +121,6 @@ export function resolveConfig<Theme extends {} = {}>(
shortcuts: resolveShortcuts(mergePresets('shortcuts')).reverse(),
extractors,
safelist: mergePresets('safelist'),
separators,
}
}
8 changes: 8 additions & 0 deletions packages/core/src/types.ts
Expand Up @@ -309,6 +309,13 @@ export interface ConfigBase<Theme extends {} = {}> {
*/
rules?: Rule<Theme>[]

/**
* Variant separator
*
* @default [':', '-']
*/
separators?: Arrayable<string>

/**
* Variants that preprocess the selectors,
* having the ability to rewrite the CSS object.
Expand Down Expand Up @@ -661,6 +668,7 @@ RequiredByKey<UserConfig<Theme>, 'mergeSelectors' | 'theme' | 'rules' | 'variant
templates: (AutoCompleteFunction | AutoCompleteTemplate)[]
extractors: AutoCompleteExtractor[]
}
separators: string[]
}

export interface GenerateResult {
Expand Down
12 changes: 10 additions & 2 deletions 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<string, RegExp> = {}

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 {
Expand Down
14 changes: 10 additions & 4 deletions packages/preset-mini/src/_utils/variants.ts
Expand Up @@ -3,10 +3,13 @@ import { escapeRegExp } from '@unocss/core'
import { getBracket } from '../utils'

export const variantMatcher = (name: string, handler: (input: VariantHandlerContext) => Record<string, any>): 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 {
Expand All @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions packages/preset-mini/src/_variants/aria.ts
Expand Up @@ -4,11 +4,11 @@ import { handler as h, variantGetParameter } from '../utils'

export const variantAria: VariantObject = {
name: 'aria',
match(matcher, { theme }: VariantContext<Theme>) {
const variant = variantGetParameter('aria-', matcher, [':', '-'])
match(matcher, ctx: VariantContext<Theme>) {
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,
Expand Down
104 changes: 52 additions & 52 deletions 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<string, RegExp> = {}

export const calcMaxWidthBySize = (size: string) => {
const value = size.match(/^-?[0-9]+\.?[0-9]*/)?.[0] || ''
Expand All @@ -11,69 +8,72 @@ export const calcMaxWidthBySize = (size: string) => {
return Number.isNaN(maxWidth) ? size : `${maxWidth}${unit}`
}

export const variantBreakpoints: Variant<Theme> = {
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<string, RegExp> = {}
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 @<breakpoint> => 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 @<breakpoint> => 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:',
}
}
7 changes: 4 additions & 3 deletions packages/preset-mini/src/_variants/combinators.ts
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/preset-mini/src/_variants/container.ts
Expand Up @@ -5,11 +5,11 @@ import { handler as h, variantGetParameter } from '../utils'

export const variantContainerQuery: VariantObject = {
name: '@',
match(matcher, { theme }: VariantContext<Theme>) {
match(matcher, ctx: VariantContext<Theme>) {
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)
Expand All @@ -20,7 +20,7 @@ export const variantContainerQuery: VariantObject = {
container = `(min-width: ${minWidth})`
}
else {
container = theme.containers?.[match] ?? ''
container = ctx.theme.containers?.[match] ?? ''
}

if (container) {
Expand Down
6 changes: 3 additions & 3 deletions packages/preset-mini/src/_variants/data.ts
Expand Up @@ -4,11 +4,11 @@ import { handler as h, variantGetParameter } from '../utils'

export const variantDataAttribute: VariantObject = {
name: 'data',
match(matcher, { theme }: VariantContext<Theme>) {
const variant = variantGetParameter('data-', matcher, [':', '-'])
match(matcher, ctx: VariantContext<Theme>) {
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,
Expand Down
8 changes: 4 additions & 4 deletions packages/preset-mini/src/_variants/default.ts
Expand Up @@ -23,15 +23,15 @@ export const variants = (options: PresetMiniOptions): Variant<Theme>[] => [
variantSelector,
variantInternalLayer,
variantNegative,
variantImportant,
variantImportant(),
variantSupports,
variantPrint,
variantCustomMedia,
variantBreakpoints,
variantBreakpoints(),
...variantCombinators,

variantPseudoClassesAndElements,
variantPseudoClassFunctions,
variantPseudoClassesAndElements(),
variantPseudoClassFunctions(),
...variantTaggedPseudoClasses(options),

partClasses,
Expand Down
49 changes: 27 additions & 22 deletions 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
},
}
}
}
},
},
}
}

0 comments on commit 2629e03

Please sign in to comment.