Skip to content

Commit

Permalink
feat(transformer-directives): Implemented @screen directive (#1434)
Browse files Browse the repository at this point in the history
Co-authored-by: kortykotropina <kortykotropina@gmail.com>
Co-authored-by: chris-zhu <1633711653@qq.com>
  • Loading branch information
3 people committed Aug 21, 2022
1 parent 3406a48 commit 01e0657
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 27 deletions.
2 changes: 1 addition & 1 deletion packages/preset-mini/src/variants/breakpoints.ts
Expand Up @@ -4,7 +4,7 @@ import type { Theme } from '../theme'

const regexCache: Record<string, RegExp> = {}

const calcMaxWidthBySize = (size: string) => {
export const calcMaxWidthBySize = (size: string) => {
const value = size.match(/^-?[0-9]+\.?[0-9]*/)?.[0] || ''
const unit = size.slice(value.length)
const maxWidth = (parseFloat(value) - 0.1)
Expand Down
68 changes: 66 additions & 2 deletions packages/transformer-directives/src/index.ts
@@ -1,8 +1,10 @@
import { cssIdRE, expandVariantGroup, notNull, regexScopePlaceholder } from '@unocss/core'
import type { SourceCodeTransformer, StringifiedUtil, UnoGenerator } from '@unocss/core'
import type { CssNode, Declaration, List, ListItem, Rule, Selector, SelectorList } from 'css-tree'
import type { Atrule, CssNode, Declaration, List, ListItem, Rule, Selector, SelectorList } from 'css-tree'
import { clone, generate, parse, walk } from 'css-tree'
import type MagicString from 'magic-string'
import { calcMaxWidthBySize } from '@unocss/preset-mini/variants'
import type { Theme } from '@unocss/preset-mini'

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

Expand Down Expand Up @@ -37,6 +39,7 @@ export default function transformerDirectives(options: TransformerDirectivesOpti
}

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

export async function transformDirectives(
code: MagicString,
Expand All @@ -52,9 +55,10 @@ export async function transformDirectives(
} = options

const isApply = code.original.includes('@apply') || (varStyle !== false && code.original.includes(varStyle))
const isScreen = code.original.includes('@screen')
const hasThemeFn = code.original.match(themeFnRE)

if (!isApply && !hasThemeFn)
if (!isApply && !hasThemeFn && !isScreen)
return

const ast = parse(originalCode || code.original, {
Expand Down Expand Up @@ -187,9 +191,69 @@ export async function transformDirectives(
}
}

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 surpported: ${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>) => {
if (isScreen && node.type === 'Atrule')
handleScreen(node)

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

Expand Down
205 changes: 181 additions & 24 deletions test/transformer-directives.test.ts
Expand Up @@ -18,6 +18,16 @@ describe('transformer-directives', () => {
shortcuts: {
btn: 'px-2 py-3 md:px-4 bg-blue-500 text-white rounded',
},
theme: {
breakpoints: {
xs: '320px',
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
xxl: '1536px',
},
},
})

async function transform(code: string, _uno: UnoGenerator = uno) {
Expand Down Expand Up @@ -296,26 +306,7 @@ describe('transformer-directives', () => {
})

test('custom breakpoints', async () => {
const customUno = createGenerator({
presets: [
presetUno(),
],
theme: {
breakpoints: {
'xs': '320px',
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
'xxl': '1536px',
},
},
})
const result = await transform(
'.grid { @apply grid grid-cols-2 xs:grid-cols-1 xxl:grid-cols-15 xl:grid-cols-10 sm:grid-cols-7 md:grid-cols-3 lg:grid-cols-4 }',
customUno,
)
const result = await transform('.grid { @apply grid grid-cols-2 xs:grid-cols-1 xxl:grid-cols-15 xl:grid-cols-10 sm:grid-cols-7 md:grid-cols-3 lg:grid-cols-4 }')
expect(result)
.toMatchInlineSnapshot(`
".grid {
Expand Down Expand Up @@ -396,10 +387,176 @@ describe('transformer-directives', () => {
`)
})

test('@screen basic', async () => {
const result = await transform(`
.grid {
@apply grid grid-cols-2;
}
@screen xs {
.grid {
@apply grid-cols-1;
}
}
@screen sm {
.grid {
@apply grid-cols-3;
}
}
@screen md {
.grid {
@apply grid-cols-4;
}
}
@screen lg {
.grid {
@apply grid-cols-5;
}
}
@screen xl {
.grid {
@apply grid-cols-6;
}
}
@screen xxl {
.grid {
@apply grid-cols-7;
}
}
`)
expect(result)
.toMatchInlineSnapshot(`
".grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 320px) {
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
@media (min-width: 640px) {
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 768px) {
.grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
@media (min-width: 1280px) {
.grid {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
}
@media (min-width: 1536px) {
.grid {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
}
"
`)
})

test('@screen lt variant', async () => {
const result = await transform(`
.grid {
@apply grid grid-cols-2;
}
@screen lt-xs {
.grid {
@apply grid-cols-1;
}
}
@screen lt-sm {
.grid {
@apply grid-cols-3;
}
}
@screen lt-md {
.grid {
@apply grid-cols-4;
}
}
`)
expect(result).toMatchInlineSnapshot(`
".grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 319.9px) {
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
@media (max-width: 639.9px) {
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 767.9px) {
.grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
"
`)
})

test('@screen at variant', async () => {
const result = await transform(`
.grid {
@apply grid grid-cols-2;
}
@screen at-xs {
.grid {
@apply grid-cols-1;
}
}
@screen at-xl {
.grid {
@apply grid-cols-3;
}
}
@screen at-xxl {
.grid {
@apply grid-cols-4;
}
}
`)
expect(result).toMatchInlineSnapshot(`
".grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 320px) and (max-width: 639.9px) {
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
@media (min-width: 1280px) and (max-width: 1535.9px) {
.grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 1536px) {
.grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
"
`)
})

describe('theme()', () => {
test('basic', async () => {
const result = await transform(
`.btn {
`.btn {
background-color: theme("colors.blue.500");
padding: theme("spacing.xs") theme("spacing.sm");
}
Expand All @@ -422,14 +579,14 @@ describe('transformer-directives', () => {

test('non-exist', async () => {
expect(async () => await transform(
`.btn {
`.btn {
color: theme("color.none.500");
}`,
)).rejects
.toMatchInlineSnapshot('[Error: theme of "color.none.500" did not found]')

expect(async () => await transform(
`.btn {
`.btn {
font-size: theme("size.lg");
}`,
)).rejects
Expand All @@ -438,7 +595,7 @@ describe('transformer-directives', () => {

test('args', async () => {
expect(async () => await transform(
`.btn {
`.btn {
color: theme();
}`,
)).rejects
Expand Down

0 comments on commit 01e0657

Please sign in to comment.