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(transformer-directives): Implemented @screen directive #1434

Merged
merged 3 commits into from Aug 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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