Skip to content

Commit

Permalink
chore(transformer-directives): deconstruct index.ts (#1724)
Browse files Browse the repository at this point in the history
Co-authored-by: hanlee <me@hanlee.co>
  • Loading branch information
zyyv and hannoeru committed Oct 16, 2022
1 parent eeed70e commit 2a5289d
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 210 deletions.
103 changes: 103 additions & 0 deletions packages/transformer-directives/src/apply.ts
@@ -0,0 +1,103 @@
import type { StringifiedUtil } from '@unocss/core'
import { expandVariantGroup, notNull, regexScopePlaceholder } from '@unocss/core'
import type { CssNode, Rule, Selector, SelectorList } from 'css-tree'
import { clone, generate, parse } from 'css-tree'
import type { TransformerDirectivesContext } from '.'
import { transformDirectives } from '.'

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

export async function handleApply(ctx: TransformerDirectivesContext, node: Rule) {
const { code, uno, options, filename, offset } = ctx
const calcOffset = (pos: number) => offset ? pos + offset : pos

await Promise.all(
node.block.children.map(async (childNode) => {
if (childNode.type === 'Raw')
return transformDirectives(code, uno, options, filename, childNode.value, calcOffset(childNode.loc!.start.offset))
await parseApply(ctx, node, childNode)
}).toArray(),
)
}

export async function parseApply({ code, uno, options, offset }: TransformerDirectivesContext, node: Rule, childNode: CssNode) {
const { varStyle = '--at-' } = options
const calcOffset = (pos: number) => offset ? pos + offset : pos

let body: string | undefined
if (childNode.type === 'Atrule' && childNode.name === 'apply' && childNode.prelude && childNode.prelude.type === 'Raw') {
body = childNode.prelude.value.trim()
}
else if (varStyle !== false && childNode!.type === 'Declaration' && childNode.property === `${varStyle}apply` && childNode.value.type === 'Raw') {
body = childNode.value.value.trim()
// remove quotes
if (body.match(/^(['"]).*\1$/))
body = body.slice(1, -1)
}

if (!body)
return

const classNames = expandVariantGroup(body)
.split(/\s+/g)
.map(className => className.trim().replace(/\\/, ''))

const utils = (
await Promise.all(
classNames.map(i => uno.parseToken(i, '-')),
))
.filter(notNull).flat()
.sort((a, b) => a[0] - b[0])
.sort((a, b) => (a[3] ? uno.parentOrders.get(a[3]) ?? 0 : 0) - (b[3] ? uno.parentOrders.get(b[3]) ?? 0 : 0))
.reduce((acc, item) => {
const target = acc.find(i => i[1] === item[1] && i[3] === item[3])
if (target)
target[2] += item[2]
else
// use spread operator to prevent reassign to uno internal cache
acc.push([...item] as Writeable<StringifiedUtil>)
return acc
}, [] as Writeable<StringifiedUtil>[])

if (!utils.length)
return

for (const i of utils) {
const [, _selector, body, parent] = i
const selector = _selector?.replace(regexScopePlaceholder, ' ') || _selector

if (parent || (selector && selector !== '.\\-')) {
let newSelector = generate(node.prelude)
if (selector && selector !== '.\\-') {
const selectorAST = parse(selector, {
context: 'selector',
}) as Selector

const prelude = clone(node.prelude) as SelectorList

prelude.children.forEach((child) => {
const parentSelectorAst = clone(selectorAST) as Selector
parentSelectorAst.children.forEach((i) => {
if (i.type === 'ClassSelector' && i.name === '\\-')
Object.assign(i, clone(child))
})
Object.assign(child, parentSelectorAst)
})
newSelector = generate(prelude)
}

let css = `${newSelector}{${body}}`
if (parent)
css = `${parent}{${css}}`

code.appendLeft(calcOffset(node.loc!.end.offset), css)
}
else {
code.appendRight(calcOffset(childNode!.loc!.end.offset), body)
}
}
code.remove(
calcOffset(childNode!.loc!.start.offset),
calcOffset(childNode!.loc!.end.offset),
)
}
232 changes: 22 additions & 210 deletions packages/transformer-directives/src/index.ts
@@ -1,11 +1,11 @@
import { cssIdRE, expandVariantGroup, notNull, regexScopePlaceholder } from '@unocss/core'
import type { SourceCodeTransformer, StringifiedUtil, UnoGenerator } from '@unocss/core'
import type { Atrule, CssNode, Declaration, List, ListItem, Rule, Selector, SelectorList } from 'css-tree'
import { clone, generate, parse, walk } from 'css-tree'
import { cssIdRE } 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 type { Theme } from '@unocss/preset-mini'

type Writeable<T> = { -readonly [P in keyof T]: T[P] }
import { handleThemeFn, themeFnRE } from './theme'
import { handleScreen } from './screen'
import { handleApply } from './apply'

export interface TransformerDirectivesOptions {
enforce?: SourceCodeTransformer['enforce']
Expand All @@ -26,6 +26,14 @@ export interface TransformerDirectivesOptions {
throwOnMissing?: boolean
}

export interface TransformerDirectivesContext {
code: MagicString
uno: UnoGenerator
options: TransformerDirectivesOptions
offset?: number
filename?: string
}

export default function transformerDirectives(options: TransformerDirectivesOptions = {}): SourceCodeTransformer {
return {
name: 'css-directive',
Expand All @@ -37,9 +45,6 @@ export default function transformerDirectives(options: TransformerDirectivesOpti
}
}

const themeFnRE = /theme\((.*?)\)/g
const screenRuleRE = /(@screen) (.+) /g

export async function transformDirectives(
code: MagicString,
uno: UnoGenerator,
Expand All @@ -48,10 +53,7 @@ export async function transformDirectives(
originalCode?: string,
offset?: number,
) {
const {
varStyle = '--at-',
throwOnMissing = true,
} = options
const { varStyle = '--at-' } = options

const isApply = code.original.includes('@apply') || (varStyle !== false && code.original.includes(varStyle))
const isScreen = code.original.includes('@screen')
Expand All @@ -69,212 +71,22 @@ export async function transformDirectives(
if (ast.type !== 'StyleSheet')
return

const calcOffset = (pos: number) => offset ? pos + offset : pos

const handleApply = async (node: Rule, childNode: CssNode) => {
let body: string | undefined
if (childNode.type === 'Atrule' && childNode.name === 'apply' && childNode.prelude && childNode.prelude.type === 'Raw') {
body = childNode.prelude.value.trim()
}
else if (varStyle !== false && childNode.type === 'Declaration' && childNode.property === `${varStyle}apply` && childNode.value.type === 'Raw') {
body = childNode.value.value.trim()
// remove quotes
if (body.match(/^(['"]).*\1$/))
body = body.slice(1, -1)
}

if (!body)
return

const classNames = expandVariantGroup(body)
.split(/\s+/g)
.map(className => className.trim().replace(/\\/, ''))

const utils = (
await Promise.all(
classNames.map(i => uno.parseToken(i, '-')),
))
.filter(notNull).flat()
.sort((a, b) => a[0] - b[0])
.sort((a, b) => (a[3] ? uno.parentOrders.get(a[3]) ?? 0 : 0) - (b[3] ? uno.parentOrders.get(b[3]) ?? 0 : 0))
.reduce((acc, item) => {
const target = acc.find(i => i[1] === item[1] && i[3] === item[3])
if (target)
target[2] += item[2]
else
// use spread operator to prevent reassign to uno internal cache
acc.push([...item] as Writeable<StringifiedUtil>)
return acc
}, [] as Writeable<StringifiedUtil>[])

if (!utils.length)
return

for (const i of utils) {
const [, _selector, body, parent] = i
const selector = _selector?.replace(regexScopePlaceholder, ' ') || _selector

if (parent || (selector && selector !== '.\\-')) {
let newSelector = generate(node.prelude)
if (selector && selector !== '.\\-') {
const selectorAST = parse(selector, {
context: 'selector',
}) as Selector

const prelude = clone(node.prelude) as SelectorList

prelude.children.forEach((child) => {
const parentSelectorAst = clone(selectorAST) as Selector
parentSelectorAst.children.forEach((i) => {
if (i.type === 'ClassSelector' && i.name === '\\-')
Object.assign(i, clone(child))
})
Object.assign(child, parentSelectorAst)
})
newSelector = generate(prelude)
}

let css = `${newSelector}{${body}}`
if (parent)
css = `${parent}{${css}}`

code.appendLeft(calcOffset(node.loc!.end.offset), css)
}
else {
code.appendRight(calcOffset(childNode.loc!.end.offset), body)
}
}
code.remove(
calcOffset(childNode.loc!.start.offset),
calcOffset(childNode.loc!.end.offset),
)
}

const handleThemeFn = (node: Declaration) => {
const value = node.value
const offset = value.loc!.start.offset
const str = code.original.slice(offset, value.loc!.end.offset)
const matches = Array.from(str.matchAll(themeFnRE))

if (!matches.length)
return

for (const match of matches) {
const rawArg = match[1].trim()
if (!rawArg)
throw new Error('theme() expect exact one argument, but got 0')

let value: any = uno.config.theme
const keys = rawArg.slice(1, -1).split('.')

keys.every((key) => {
if (value[key] != null)
value = value[key]
else if (value[+key] != null)
value = value[+key]
else
return false
return true
})

if (typeof value === 'string') {
code.overwrite(
offset + match.index!,
offset + match.index! + match[0].length,
value,
)
}
else if (throwOnMissing) {
throw new Error(`theme of "${rawArg.slice(1, -1)}" did not found`)
}
}
}

const handleScreen = (node: Atrule) => {
let breakpointName = ''; let prefix
if (node.name === 'screen' && node.prelude?.type === 'Raw')
breakpointName = node.prelude.value.trim()

if (!breakpointName)
return

const match = breakpointName.match(/^(?:(lt|at)-)?(\w+)$/)
if (match) {
prefix = match[1]
breakpointName = match[2]
}

const resolveBreakpoints = () => {
let breakpoints: Record<string, string> | undefined
if (uno.userConfig && uno.userConfig.theme)
breakpoints = (uno.userConfig.theme as Theme).breakpoints

if (!breakpoints)
breakpoints = (uno.config.theme as Theme).breakpoints

return breakpoints
}
const variantEntries: Array<[string, string, number]> = Object.entries(resolveBreakpoints() ?? {}).map(([point, size], idx) => [point, size, idx])
const generateMediaQuery = (breakpointName: string, prefix?: string) => {
const [, size, idx] = variantEntries.find(i => i[0] === breakpointName)!
if (prefix) {
if (prefix === 'lt')
return `@media (max-width: ${calcMaxWidthBySize(size)})`
else if (prefix === 'at')
return `@media (min-width: ${size})${variantEntries[idx + 1] ? ` and (max-width: ${calcMaxWidthBySize(variantEntries[idx + 1][1])})` : ''}`

else throw new Error(`breakpoint variant not supported: ${prefix}`)
}
return `@media (min-width: ${size})`
}

if (!variantEntries.find(i => i[0] === breakpointName))
throw new Error(`breakpoint ${breakpointName} not found`)

const offset = node.loc!.start.offset
const str = code.original.slice(offset, node.loc!.end.offset)
const matches = Array.from(str.matchAll(screenRuleRE))

if (!matches.length)
return

for (const match of matches) {
code.overwrite(
offset + match.index!,
offset + match.index! + match[0].length,
`${generateMediaQuery(breakpointName, prefix)} `,
)
}
}

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

const processNode = async (node: CssNode, _item: ListItem<CssNode>, _list: List<CssNode>) => {
const ctx: TransformerDirectivesContext = { options, uno, code, filename, offset }

if (isScreen && node.type === 'Atrule')
handleScreen(node)
handleScreen(ctx, node)

if (hasThemeFn && node.type === 'Declaration')
handleThemeFn(node)
handleThemeFn(ctx, node)

if (isApply && node.type === 'Rule') {
await Promise.all(
node.block.children.map(async (childNode, _childItem) => {
if (childNode.type === 'Raw')
return transformDirectives(code, uno, options, filename, childNode.value, calcOffset(childNode.loc!.start.offset))
await handleApply(node, childNode)
}).toArray(),
)
}
if (isApply && node.type === 'Rule')
await handleApply(ctx, node)
}

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

await Promise.all(stack)
}

function calcMaxWidthBySize(size: string) {
const value = size.match(/^-?[0-9]+\.?[0-9]*/)?.[0] || ''
const unit = size.slice(value.length)
const maxWidth = (parseFloat(value) - 0.1)
return Number.isNaN(maxWidth) ? size : `${maxWidth}${unit}`
}

0 comments on commit 2a5289d

Please sign in to comment.