Skip to content

Commit

Permalink
fix(transformer-directive): get raw string in directive, support slas…
Browse files Browse the repository at this point in the history
…h & hash (#3790)

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
zyyv and antfu committed May 10, 2024
1 parent 9eb6e29 commit fa49753
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 128 deletions.
36 changes: 19 additions & 17 deletions packages/transformer-directives/src/apply.ts
Expand Up @@ -2,8 +2,8 @@ import type { StringifiedUtil } from '@unocss/core'
import { expandVariantGroup, notNull, regexScopePlaceholder } from '@unocss/core'
import type { CssNode, Rule, Selector, SelectorList } from 'css-tree'
import { List, clone, generate, parse } from 'css-tree'
import type { TransformerDirectivesContext } from '.'
import { transformDirectives } from '.'
import { transformDirectives } from './transform'
import type { TransformerDirectivesContext } from './types'

type Writeable<T> = { -readonly [P in keyof T]: T[P] }

Expand All @@ -25,29 +25,27 @@ export async function parseApply({ code, uno, offset, applyVariable }: Transform

let body: string | undefined
if (childNode.type === 'Atrule' && childNode.name === 'apply' && childNode.prelude && childNode.prelude.type === 'Raw') {
body = childNode.prelude.value.trim()
body = removeQuotes(childNode.prelude.value.trim())
}

else if (childNode!.type === 'Declaration' && applyVariable.includes(childNode.property) && childNode.value.type === 'Value') {
body = childNode.value.children.reduce((str, nodeItem) => {
switch (nodeItem.type) {
case 'String':
return `${str} ${nodeItem.value}`
case 'Identifier':
return `${str} ${nodeItem.name}`
default:
return str
}
}, '').trim()
// Get raw value of the declaration
// as csstree would try to parse the content with operators, but we don't need them.
let rawValue = code.original.slice(
calcOffset(childNode.value.loc!.start.offset),
calcOffset(childNode.value.loc!.end.offset),
)
rawValue = removeQuotes(rawValue)
const items = rawValue
.split(/\s+/g)
.filter(Boolean)
.map(i => removeQuotes(i))
body = items.join(' ')
}

if (!body)
return

// remove quotes
if (/^(['"]).*\1$/.test(body))
body = body.slice(1, -1)

const classNames = expandVariantGroup(body)
.split(/\s+/g)
.map(className => className.trim().replace(/\\/, ''))
Expand Down Expand Up @@ -120,3 +118,7 @@ export async function parseApply({ code, uno, offset, applyVariable }: Transform
calcOffset(childNode!.loc!.end.offset + simicolonOffset),
)
}

function removeQuotes(value: string) {
return value.replace(/^(['"])(.*)\1$/, '$2')
}
2 changes: 1 addition & 1 deletion packages/transformer-directives/src/functions.ts
@@ -1,6 +1,6 @@
import type { FunctionNode, StringNode } from 'css-tree'
import { transformThemeString } from '@unocss/rule-utils'
import type { TransformerDirectivesContext } from '.'
import type { TransformerDirectivesContext } from './types'

export function handleFunction({ code, uno, options }: TransformerDirectivesContext, node: FunctionNode) {
const { throwOnMissing = true } = options
Expand Down
113 changes: 5 additions & 108 deletions packages/transformer-directives/src/index.ts
@@ -1,51 +1,9 @@
import { cssIdRE, toArray } from '@unocss/core'
import type { SourceCodeTransformer, UnoGenerator } from '@unocss/core'
import type { CssNode, List, ListItem } from 'css-tree'
import { parse, walk } from 'css-tree'
import type MagicString from 'magic-string'
import { hasThemeFn as hasThemeFunction } from '@unocss/rule-utils'
import { handleScreen } from './screen'
import { handleApply } from './apply'
import { handleFunction } from './functions'
import { cssIdRE } from '@unocss/core'
import type { SourceCodeTransformer } from '@unocss/core'
import { transformDirectives } from './transform'
import type { TransformerDirectivesOptions } from './types'

export interface TransformerDirectivesOptions {
enforce?: SourceCodeTransformer['enforce']

/**
* Throw an error if utils or themes are not found.
*
* @default true
*/
throwOnMissing?: boolean

/**
* Treat CSS custom properties as @apply directives for CSS syntax compatibility.
*
* Pass `false` to disable.
*
* @default ['--at-apply', '--uno-apply', '--uno']
*/
applyVariable?: false | string | string[]

/**
* Treat CSS custom properties as directives for CSS syntax compatibility.
*
* Pass `false` to disable, or a string to use as a prefix.
*
* @deprecated use `applyVariable` to specify the full var name instead.
* @default '--at-'
*/
varStyle?: false | string
}

export interface TransformerDirectivesContext {
code: MagicString
uno: UnoGenerator
options: TransformerDirectivesOptions
applyVariable: string[]
offset?: number
filename?: string
}
export * from './types'

export default function transformerDirectives(options: TransformerDirectivesOptions = {}): SourceCodeTransformer {
return {
Expand All @@ -57,64 +15,3 @@ export default function transformerDirectives(options: TransformerDirectivesOpti
},
}
}

export async function transformDirectives(
code: MagicString,
uno: UnoGenerator,
options: TransformerDirectivesOptions,
filename?: string,
originalCode?: string,
offset?: number,
) {
let { applyVariable } = options
const varStyle = options.varStyle
if (applyVariable === undefined) {
if (varStyle !== undefined)
applyVariable = varStyle ? [`${varStyle}apply`] : []
applyVariable = ['--at-apply', '--uno-apply', '--uno']
}
applyVariable = toArray(applyVariable || [])

const hasApply = code.original.includes('@apply') || applyVariable.some(s => code.original.includes(s))
const hasScreen = code.original.includes('@screen')
const hasThemeFn = hasThemeFunction(code.original)

if (!hasApply && !hasThemeFn && !hasScreen)
return

const ast = parse(originalCode || code.original, {
parseCustomProperty: true,
parseAtrulePrelude: false,
positions: true,
filename,
})

if (ast.type !== 'StyleSheet')
return

const stack: Promise<void>[] = []

const ctx: TransformerDirectivesContext = {
options,
applyVariable,
uno,
code,
filename,
offset,
}

const processNode = async (node: CssNode, _item: ListItem<CssNode>, _list: List<CssNode>) => {
if (hasScreen && node.type === 'Atrule')
handleScreen(ctx, node)

if (node.type === 'Function')
handleFunction(ctx, node)

if (hasApply && node.type === 'Rule')
await handleApply(ctx, node)
}

walk(ast, (...args) => stack.push(processNode(...args)))

await Promise.all(stack)
}
2 changes: 1 addition & 1 deletion packages/transformer-directives/src/screen.ts
@@ -1,6 +1,6 @@
import type { Theme } from '@unocss/preset-mini'
import type { Atrule } from 'css-tree'
import type { TransformerDirectivesContext } from '.'
import type { TransformerDirectivesContext } from './types'

const screenRuleRE = /(@screen) (.+) /g

Expand Down
71 changes: 71 additions & 0 deletions packages/transformer-directives/src/transform.ts
@@ -0,0 +1,71 @@
import { toArray } from '@unocss/core'
import type { UnoGenerator } from '@unocss/core'
import type { CssNode, List, ListItem } from 'css-tree'
import { parse, walk } from 'css-tree'
import type MagicString from 'magic-string'
import { hasThemeFn as hasThemeFunction } from '@unocss/rule-utils'
import { handleScreen } from './screen'
import { handleApply } from './apply'
import { handleFunction } from './functions'
import type { TransformerDirectivesContext, TransformerDirectivesOptions } from './types'

export async function transformDirectives(
code: MagicString,
uno: UnoGenerator,
options: TransformerDirectivesOptions,
filename?: string,
originalCode?: string,
offset?: number,
) {
let { applyVariable } = options
const varStyle = options.varStyle
if (applyVariable === undefined) {
if (varStyle !== undefined)
applyVariable = varStyle ? [`${varStyle}apply`] : []
applyVariable = ['--at-apply', '--uno-apply', '--uno']
}
applyVariable = toArray(applyVariable || [])

const hasApply = code.original.includes('@apply') || applyVariable.some(s => code.original.includes(s))
const hasScreen = code.original.includes('@screen')
const hasThemeFn = hasThemeFunction(code.original)

if (!hasApply && !hasThemeFn && !hasScreen)
return

const ast = parse(originalCode || code.original, {
parseCustomProperty: true,
parseAtrulePrelude: false,
positions: true,
filename,
})

if (ast.type !== 'StyleSheet')
return

const stack: Promise<void>[] = []

const ctx: TransformerDirectivesContext = {
options,
applyVariable,
uno,
code,
filename,
offset,
}

const processNode = async (node: CssNode, _item: ListItem<CssNode>, _list: List<CssNode>) => {
if (hasScreen && node.type === 'Atrule')
handleScreen(ctx, node)

if (node.type === 'Function')
handleFunction(ctx, node)

if (hasApply && node.type === 'Rule')
await handleApply(ctx, node)
}

walk(ast, (...args) => stack.push(processNode(...args)))

await Promise.all(stack)
}
41 changes: 41 additions & 0 deletions packages/transformer-directives/src/types.ts
@@ -0,0 +1,41 @@
import type { SourceCodeTransformer, UnoGenerator } from '@unocss/core'
import type MagicString from 'magic-string'

export interface TransformerDirectivesOptions {
enforce?: SourceCodeTransformer['enforce']

/**
* Throw an error if utils or themes are not found.
*
* @default true
*/
throwOnMissing?: boolean

/**
* Treat CSS custom properties as @apply directives for CSS syntax compatibility.
*
* Pass `false` to disable.
*
* @default ['--at-apply', '--uno-apply', '--uno']
*/
applyVariable?: false | string | string[]

/**
* Treat CSS custom properties as directives for CSS syntax compatibility.
*
* Pass `false` to disable, or a string to use as a prefix.
*
* @deprecated use `applyVariable` to specify the full var name instead.
* @default '--at-'
*/
varStyle?: false | string
}

export interface TransformerDirectivesContext {
code: MagicString
uno: UnoGenerator
options: TransformerDirectivesOptions
applyVariable: string[]
offset?: number
filename?: string
}
22 changes: 21 additions & 1 deletion test/transformer-directives.test.ts
Expand Up @@ -2,11 +2,11 @@ import { readFile } from 'node:fs/promises'
import type { UnoGenerator } from '@unocss/core'
import { createGenerator } from '@unocss/core'
import presetUno from '@unocss/preset-uno'
import { transformDirectives } from '@unocss/transformer-directives'
import MagicString from 'magic-string'
import parserCSS from 'prettier/parser-postcss'
import prettier from 'prettier/standalone'
import { describe, expect, it } from 'vitest'
import { transformDirectives } from '../packages/transformer-directives/src/transform'

describe('transformer-directives', () => {
const uno = createGenerator({
Expand Down Expand Up @@ -311,6 +311,26 @@ describe('transformer-directives', () => {
.toMatchFileSnapshot('./assets/output/transformer-directives-var-style-class.css')
})

it('declaration for apply variable', async () => {
const result = await transform(
`nav {
--uno: b-#fff bg-black/5 fw-600 text-teal/7 'shadow-red:80';
}`,
)

expect(result).toMatchInlineSnapshot(`
"nav {
--un-border-opacity: 1;
border-color: rgb(255 255 255 / var(--un-border-opacity));
background-color: rgb(0 0 0 / 0.05);
color: rgb(45 212 191 / 0.07);
font-weight: 600;
--un-shadow-color: rgb(248 113 113 / 0.8);
}
"
`)
})

it('@screen basic', async () => {
const result = await transform(`
.grid {
Expand Down

0 comments on commit fa49753

Please sign in to comment.