From db339c3a06fa1107e4c2251b8e71eb53d3392bfe Mon Sep 17 00:00:00 2001 From: enpitsulin Date: Sun, 4 Jun 2023 20:40:51 +0800 Subject: [PATCH] feat(preset-mini): pseudo-class function for custom arguments (#2694) --- packages/preset-mini/src/_variants/pseudo.ts | 34 ++++++++++++++++---- test/assets/output/preset-mini-targets.css | 5 +++ test/assets/preset-mini-targets.ts | 7 ++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/packages/preset-mini/src/_variants/pseudo.ts b/packages/preset-mini/src/_variants/pseudo.ts index e425e23452..a543feee5c 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 { handler as h, variantGetBracket } from '../_utils' +import { getBracket, handler as h, variantGetBracket } from '../_utils' /** * Note: the order of following pseudo classes will affect the order of generated css. @@ -95,8 +95,9 @@ function taggedPseudoClassMatcher(tag: string, parent: string, combinator: strin let splitRE: RegExp let pseudoRE: RegExp let pseudoColonRE: RegExp + let pseudoVarRE: RegExp - const matchBracket = (input: string) => { + const matchBracket = (input: string): [label: string, rest: string, prefix: string] | undefined => { const body = variantGetBracket(`${tag}-`, input, []) if (!body) return @@ -115,7 +116,7 @@ function taggedPseudoClassMatcher(tag: string, parent: string, combinator: strin ] } - const matchPseudo = (input: string) => { + const matchPseudo = (input: string): [label: string, rest: string, prefix: string, pseudoKey: string] | undefined => { const match = input.match(pseudoRE) || input.match(pseudoColonRE) if (!match) return @@ -134,6 +135,21 @@ function taggedPseudoClassMatcher(tag: string, parent: string, combinator: strin ] } + const matchPseudoVar = (input: string): [label: string, rest: string, prefix: string] | undefined => { + const match = input.match(pseudoVarRE) + if (!match) + return + const [original, fn, pseudoValue] = match + const label = match[3] ?? '' + const pseudo = `:${fn}(${pseudoValue})` + + return [ + label, + input.slice(original.length), + `${parent}${escapeSelector(label)}${pseudo}`, + ] + } + return { name: `pseudo:${tag}`, match(input, ctx) { @@ -141,16 +157,17 @@ function taggedPseudoClassMatcher(tag: string, parent: string, combinator: strin 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('|')})`) + pseudoVarRE = new RegExp(`^${tag}-(?:(${PseudoClassFunctionsStr})-)?\\[(.+)\\](?:(/\\w+))?(?:${ctx.generator.config.separators.filter(x => x !== '-').join('|')})`) } if (!input.startsWith(tag)) return - const result = matchBracket(input) || matchPseudo(input) + const result = matchBracket(input) || matchPseudo(input) || matchPseudoVar(input) if (!result) return - const [label, matcher, prefix, pseudoName = ''] = result as [string, string, string, string | undefined] + const [label, matcher, prefix, pseudoName = ''] = result if (label !== '') warnOnce('The labeled variant is experimental and may not follow semver.') @@ -231,17 +248,20 @@ export function variantPseudoClassesAndElements(): VariantObject { export function variantPseudoClassFunctions(): VariantObject { let PseudoClassFunctionsRE: RegExp let PseudoClassColonFunctionsRE: RegExp + let PseudoClassVarFunctionRE: 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('|')})`) + PseudoClassVarFunctionRE = new RegExp(`^(${PseudoClassFunctionsStr})-(\\[.+\\])(?:${ctx.generator.config.separators.filter(x => x !== '-').join('|')})`) } - const match = input.match(PseudoClassFunctionsRE) || input.match(PseudoClassColonFunctionsRE) + const match = input.match(PseudoClassFunctionsRE) || input.match(PseudoClassColonFunctionsRE) || input.match(PseudoClassVarFunctionRE) if (match) { const fn = match[1] - const pseudo = PseudoClasses[match[2]] || PseudoClassesColon[match[2]] || `:${match[2]}` + const fnVal = getBracket(match[2], '[', ']') + const pseudo = fnVal ? h.bracket(match[2]) : (PseudoClasses[match[2]] || PseudoClassesColon[match[2]] || `:${match[2]}`) return { matcher: input.slice(match[0].length), selector: s => `${s}:${fn}(${pseudo})`, diff --git a/test/assets/output/preset-mini-targets.css b/test/assets/output/preset-mini-targets.css index 5664a19ca5..fa6e750238 100644 --- a/test/assets/output/preset-mini-targets.css +++ b/test/assets/output/preset-mini-targets.css @@ -77,6 +77,11 @@ .children\:m-auto>*, .m-auto{margin:auto;} .all\:m1\/1 *{margin:100%;} +.group:not([data-potato]) .group-not-\[\[data-potato\]\]\:m-1, +.has-\[\:hover\]\:m-1:has(:hover), +.parent:not(#someId)>.parent-not-\[\#someId\]\:m-1, +.peer:where(.child)~.peer-where-\[\.child\]\:m-1, +.previous:is(div)+.previous-is-\[div\]\:m-1{margin:0.25rem;} .m-\[3em\]{margin:3em;} .m-0, .m-none{margin:0;} diff --git a/test/assets/preset-mini-targets.ts b/test/assets/preset-mini-targets.ts index 8176ea5b23..67011863f4 100644 --- a/test/assets/preset-mini-targets.ts +++ b/test/assets/preset-mini-targets.ts @@ -1039,6 +1039,13 @@ export const presetMiniTargets: string[] = [ 'group-has-placeholder-shown:text-4xl', 'focus-within:has-first:checked:bg-gray/20', + // variants - pseudo function with custom value + 'has-[:hover]:m-1', + 'group-not-[[data-potato]]:m-1', + 'previous-is-[div]:m-1', + 'peer-where-[.child]:m-1', + 'parent-not-[#someId]:m-1', + // variants scope 'scope-[.scope\\_class]:translate-0', 'scope-[unocss]:block',