diff --git a/packages/preset-mini/src/_utils/utilities.ts b/packages/preset-mini/src/_utils/utilities.ts index fe260059c1..fe31ef5521 100644 --- a/packages/preset-mini/src/_utils/utilities.ts +++ b/packages/preset-mini/src/_utils/utilities.ts @@ -227,6 +227,40 @@ export function makeGlobalStaticRules(prefix: string, property?: string) { return globalKeywords.map(keyword => [`${prefix}-${keyword}`, { [property ?? prefix]: keyword }] as Rule) } +export function getBracket(str: string, open: string, close: string) { + if (str === '') + return + + const l = str.length + let parenthesis = 0 + let opened = false + let openAt = 0 + for (let i = 0; i < l; i++) { + switch (str[i]) { + case open: + if (!opened) { + opened = true + openAt = i + } + parenthesis++ + break + + case close: + --parenthesis + if (parenthesis < 0) + return + if (parenthesis === 0) { + return [ + str.slice(openAt, i + 1), + str.slice(i + 1), + str.slice(0, openAt), + ] + } + break + } + } +} + export function getComponent(str: string, open: string, close: string, separators: string | string[]) { if (str === '') return diff --git a/packages/preset-mini/src/_utils/variants.ts b/packages/preset-mini/src/_utils/variants.ts index b5e9d06b4b..db46878648 100644 --- a/packages/preset-mini/src/_utils/variants.ts +++ b/packages/preset-mini/src/_utils/variants.ts @@ -1,6 +1,6 @@ import type { VariantHandler, VariantHandlerContext, VariantObject } from '@unocss/core' import { escapeRegExp } from '@unocss/core' -import { getComponent } from '../utils' +import { getBracket, getComponent } from '../utils' export const variantMatcher = (name: string, handler: (input: VariantHandlerContext) => Record): VariantObject => { const re = new RegExp(`^${escapeRegExp(name)}[:-]`) @@ -52,3 +52,15 @@ export const variantGetComponent = (name: string, matcher: string): string[] | u } } +export const variantGetBracket = (name: string, matcher: string, separators: string[]): string[] | undefined => { + if (matcher.startsWith(`${name}-[`)) { + const [match, rest] = getBracket(matcher.slice(name.length + 1), '[', ']') ?? [] + if (match && rest) { + for (const separator of separators) { + if (rest.startsWith(separator)) + return [match, rest.slice(separator.length), separator] + } + return [match, rest, ''] + } + } +} diff --git a/packages/preset-mini/src/_variants/default.ts b/packages/preset-mini/src/_variants/default.ts index d97edd06e2..e91caf5389 100644 --- a/packages/preset-mini/src/_variants/default.ts +++ b/packages/preset-mini/src/_variants/default.ts @@ -13,7 +13,6 @@ import { variantSupports } from './supports' import { partClasses, variantPseudoClassFunctions, variantPseudoClassesAndElements, variantTaggedPseudoClasses } from './pseudo' export const variants = (options: PresetMiniOptions): Variant[] => [ - variantVariables, variantCssLayer, variantSelector, @@ -34,4 +33,6 @@ export const variants = (options: PresetMiniOptions): Variant[] => [ ...variantColorsMediaOrClass(options), ...variantLanguageDirections, variantScope, + + variantVariables, ] diff --git a/packages/preset-mini/src/_variants/pseudo.ts b/packages/preset-mini/src/_variants/pseudo.ts index e8e57a355d..0da6b3b398 100644 --- a/packages/preset-mini/src/_variants/pseudo.ts +++ b/packages/preset-mini/src/_variants/pseudo.ts @@ -1,7 +1,7 @@ import type { VariantObject } from '@unocss/core' import { escapeRegExp, escapeSelector, warnOnce } from '@unocss/core' import type { PresetMiniOptions } from '..' -import { getComponent, handler as h } from '../_utils' +import { handler as h, variantGetBracket } from '../_utils' const PseudoClasses: Record = Object.fromEntries([ // pseudo elements part 1 @@ -83,61 +83,58 @@ const sortValue = (pseudo: string) => { } const taggedPseudoClassMatcher = (tag: string, parent: string, combinator: string): VariantObject => { - const rawRe = new RegExp(`^(${escapeRegExp(parent)}(?:<[^>]+>)?:)(\\S+)${escapeRegExp(combinator)}\\1`) - const maybeWithBracketRE = new RegExp(`^${tag}(?:<[^>]+>)?-\\[`) - const pseudoRE = new RegExp(`^${tag}(<[^>]+>)?-(?:(?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesStr}))[:-]`) - const pseudoColonRE = new RegExp(`^${tag}(<[^>]+>)?-(?:(?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesColonStr}))[:]`) + 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+))?[:]`) return { name: `pseudo:${tag}`, match(input: string) { if (!input.startsWith(tag)) return - if (input.match(maybeWithBracketRE)) { - const labelMatcher = input.substring(tag.length) - const [label, afterLabel] = getComponent(labelMatcher, '<', '>', '-') ?? ['', labelMatcher.slice(1)] - const body = getComponent(afterLabel, '[', ']', [':', '-']) - - if (!body) - return + let label: string + let prefix: string + let matcher: string + let sort: number | undefined + const body = variantGetBracket(tag, input, []) + if (body) { const [match, rest] = body const bracketValue = h.bracket(match) - if (bracketValue == null) return - if (label) - warnOnce('The labeled pseudo is experimental and may be changed in breaking ways at any time.') - - let prefix = `${parent}${escapeSelector(label)}` + label = rest.split(/[:-]/, 1)?.[0] ?? '' + prefix = `${parent}${escapeSelector(label)}` prefix = bracketValue.includes('&') ? bracketValue.replace(/&/g, prefix) : `${prefix}${bracketValue}` - - return { - matcher: input.slice(input.length - rest.length), - handle: (input, next) => next({ - ...input, - prefix: `${prefix}${combinator}${input.prefix}`.replace(rawRe, '$1$2:'), - }), - } + matcher = input.slice(input.length - (rest.length - label.length - 1)) } + else { + const match = input.match(pseudoRE) || input.match(pseudoColonRE) + if (!match) + return - const match = input.match(pseudoRE) || input.match(pseudoColonRE) - if (match) { - const [original, label = '', fn, pseudoKey] = match - if (label) - warnOnce('The labeled pseudo is experimental and may be changed in breaking ways at any time.') + const [original, fn, pseudoKey] = match let pseudo = PseudoClasses[pseudoKey] || PseudoClassesColon[pseudoKey] || `:${pseudoKey}` if (fn) pseudo = `:${fn}(${pseudo})` - return { - matcher: input.slice(original.length), - handle: (input, next) => next({ - ...input, - prefix: `${parent}${escapeSelector(label)}${pseudo}${combinator}${input.prefix}`.replace(rawRe, '$1$2:'), - sort: sortValue(pseudoKey), - }), - } + + label = match[3] ?? '' + prefix = `${parent}${escapeSelector(label)}${pseudo}` + matcher = input.slice(original.length) + sort = sortValue(pseudoKey) + } + + if (label !== '') + warnOnce('The labeled pseudo is experimental and may be changed in breaking ways at any time.') + + return { + matcher, + handle: (input, next) => next({ + ...input, + prefix: `${prefix}${combinator}${input.prefix}`.replace(rawRe, '$1$2:'), + }), + sort, } }, multiPass: true, diff --git a/test/__snapshots__/preset-mini.test.ts.snap b/test/__snapshots__/preset-mini.test.ts.snap index 0f6fa3b1f7..19c7e8738f 100644 --- a/test/__snapshots__/preset-mini.test.ts.snap +++ b/test/__snapshots__/preset-mini.test.ts.snap @@ -160,7 +160,7 @@ unocss .scope-\\\\[unocss\\\\]\\\\:block{display:block;} .hover\\\\:not-first\\\\:checked\\\\:bg-red\\\\/10:checked:not(:first-child):hover{background-color:rgba(248,113,113,0.1);} .marker\\\\:bg-violet-200::marker{--un-bg-opacity:1;background-color:rgba(221,214,254,var(--un-bg-opacity));} .peer:checked~.peer-checked\\\\:bg-blue-500{--un-bg-opacity:1;background-color:rgba(59,130,246,var(--un-bg-opacity));} -.previous\\\\:checked+.previous\\\\-checked\\\\:bg-red-500{--un-bg-opacity:1;background-color:rgba(239,68,68,var(--un-bg-opacity));} +.previous\\\\/label:checked+.previous-checked\\\\/label\\\\:bg-red-500{--un-bg-opacity:1;background-color:rgba(239,68,68,var(--un-bg-opacity));} .bg-opacity-45{--un-bg-opacity:0.45;} .all-\\\\[svg\\\\]\\\\:fill-red svg{--un-fill-opacity:1;fill:rgba(248,113,113,var(--un-fill-opacity));} .fill-\\\\[\\\\#123\\\\]{--un-fill-opacity:1;fill:rgba(17,34,51,var(--un-fill-opacity));} @@ -322,7 +322,7 @@ unocss .scope-\\\\[unocss\\\\]\\\\:block{display:block;} .text-size-\\\\$variable{font-size:var(--variable);} .text-size-unset{font-size:unset;} .as-parent .group .group-\\\\[\\\\.as-parent_\\\\&\\\\]\\\\:font-13{font-weight:13;} -.as-parent .group\\\\ .group\\\\-\\\\[\\\\.as-parent_\\\\&\\\\]\\\\:font-18{font-weight:18;} +.as-parent .group\\\\/label .group-\\\\[\\\\.as-parent_\\\\&\\\\]\\\\/label\\\\:font-18{font-weight:18;} .font-050, .font-50, .fw-050, @@ -332,11 +332,13 @@ unocss .scope-\\\\[unocss\\\\]\\\\:block{display:block;} .font-inherit{font-weight:inherit;} .font-thin{font-weight:100;} .group:hover .group-\\\\[\\\\:hover\\\\]\\\\:font-11{font-weight:11;} +.group:hover .group-hover\\\\:font-10{font-weight:10;} .group.not-parent .group-\\\\[\\\\.not-parent\\\\]\\\\:font-14{font-weight:14;} .group[data-attr] .group-\\\\[\\\\[data-attr\\\\]\\\\]\\\\:font-12{font-weight:12;} -.group\\\\:hover .group\\\\-\\\\[\\\\:hover\\\\]\\\\:font-16{font-weight:16;} -.group\\\\.not-parent .group\\\\-\\\\[\\\\.not-parent\\\\]\\\\:font-19{font-weight:19;} -.group\\\\[data-attr] .group\\\\-\\\\[\\\\[data-attr\\\\]\\\\]\\\\:font-17{font-weight:17;} +.group\\\\/label:hover .group-\\\\[\\\\:hover\\\\]\\\\/label\\\\:font-16{font-weight:16;} +.group\\\\/label:hover .group-hover\\\\/label\\\\:font-15{font-weight:15;} +.group\\\\/label.not-parent .group-\\\\[\\\\.not-parent\\\\]\\\\/label\\\\:font-19{font-weight:19;} +.group\\\\/label[data-attr] .group-\\\\[\\\\[data-attr\\\\]\\\\]\\\\/label\\\\:font-17{font-weight:17;} .font-leading-2, .leading-2{line-height:0.5rem;} .leading-\\\\$variable, diff --git a/test/assets/preset-mini-targets.ts b/test/assets/preset-mini-targets.ts index 1b1b24df62..a8bddfd409 100644 --- a/test/assets/preset-mini-targets.ts +++ b/test/assets/preset-mini-targets.ts @@ -964,15 +964,17 @@ export const presetMiniTargets: string[] = [ 'group-focus:p-4', 'peer-checked:bg-blue-500', 'parent-hover:text-center', - 'previous