Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(preset-mini): pseudo-class function for custom arguments #2694

Merged
merged 11 commits into from Jun 4, 2023
34 changes: 27 additions & 7 deletions 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'

const PseudoClasses: Record<string, string> = Object.fromEntries([
// pseudo elements part 1
Expand Down Expand Up @@ -99,8 +99,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
Expand All @@ -119,7 +120,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
Expand All @@ -138,23 +139,39 @@ 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) {
if (!(splitRE && pseudoRE && pseudoColonRE)) {
splitRE = new RegExp(`(?:${ctx.generator.config.separators.join('|')})`)
pseudoRE = new RegExp(`^${tag}-(?:(?:(${PseudoClassFunctionsStr})-)?(${PseudoClassesStr}))(?:(/\\w+))?(?:${ctx.generator.config.separators.join('|')})`)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confused me on this, it seems can implement custom arguments at the beginning, but didn't

don't know if there has some edge case that I just add code

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.')

Expand Down Expand Up @@ -225,17 +242,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})`,
Expand Down
5 changes: 5 additions & 0 deletions test/assets/output/preset-mini-targets.css
Expand Up @@ -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;}
Expand Down
7 changes: 7 additions & 0 deletions test/assets/preset-mini-targets.ts
Expand Up @@ -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',
Expand Down