From 4af961fb50391665be3e987c7e0134220619fd63 Mon Sep 17 00:00:00 2001 From: Kamil Beda Date: Thu, 18 Aug 2022 21:39:25 +0200 Subject: [PATCH 1/3] Implemented @screen directive known from windi.css https://windicss.org/features/directives.html#screen --- packages/transformer-directives/src/index.ts | 38 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/transformer-directives/src/index.ts b/packages/transformer-directives/src/index.ts index 3431ed1a83..ed0311a71e 100644 --- a/packages/transformer-directives/src/index.ts +++ b/packages/transformer-directives/src/index.ts @@ -1,6 +1,6 @@ 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' @@ -37,6 +37,7 @@ export default function transformerDirectives(options: TransformerDirectivesOpti } const themeFnRE = /theme\((.*?)\)/g +const screenRuleRE = /(@screen) (.+) /g export async function transformDirectives( code: MagicString, @@ -52,9 +53,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, { @@ -187,9 +189,41 @@ export async function transformDirectives( } } + const handleScreen = (node: Atrule) => { + let breakpointName + if (node.name === 'screen' && node.prelude && node.prelude.type === 'Raw') + breakpointName = node.prelude.value.trim() + + if (!breakpointName) + return + + // @ts-expect-error breakpoints aren't always available + const breakpointPx = uno.config.theme.breakpoints ? uno.config.theme.breakpoints[breakpointName] : null + if (!breakpointPx) + 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, + `@media (min-width: ${breakpointPx}) `, + ) + } + } + const stack: Promise[] = [] const processNode = async (node: CssNode, _item: ListItem, _list: List) => { + if (isScreen && node.type === 'Atrule') + handleScreen(node) + if (hasThemeFn && node.type === 'Declaration') handleThemeFn(node) From fae8669feeaee1f873fc10af68d06e0e2fa0f978 Mon Sep 17 00:00:00 2001 From: kortykotropina Date: Fri, 19 Aug 2022 16:44:49 +0200 Subject: [PATCH 2/3] feature: Added basic test case --- test/transformer-directives.test.ts | 100 ++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 4 deletions(-) diff --git a/test/transformer-directives.test.ts b/test/transformer-directives.test.ts index 9eba468348..7e326b2d50 100644 --- a/test/transformer-directives.test.ts +++ b/test/transformer-directives.test.ts @@ -396,10 +396,102 @@ describe('transformer-directives', () => { `) }) + test('basic', async () => { + const customUno = createGenerator({ + presets: [ + presetUno(), + ], + theme: { + breakpoints: { + xs: '320px', + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + xxl: '1536px', + }, + }, + }) + 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; + } + }`, + customUno, + ) + 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)); + } + } + " + `) + }) + describe('theme()', () => { test('basic', async () => { const result = await transform( - `.btn { + `.btn { background-color: theme("colors.blue.500"); padding: theme("spacing.xs") theme("spacing.sm"); } @@ -422,14 +514,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 @@ -438,7 +530,7 @@ describe('transformer-directives', () => { test('args', async () => { expect(async () => await transform( - `.btn { + `.btn { color: theme(); }`, )).rejects From 0a182e4c54f83a7ea2eacc23df6198048b81850b Mon Sep 17 00:00:00 2001 From: chris-zhu <1633711653@qq.com> Date: Sun, 21 Aug 2022 15:46:08 +0800 Subject: [PATCH 3/3] feat(transformer-directives): support `lt` & `at` --- .../preset-mini/src/variants/breakpoints.ts | 2 +- packages/transformer-directives/src/index.ts | 42 ++- test/transformer-directives.test.ts | 239 +++++++++++------- 3 files changed, 189 insertions(+), 94 deletions(-) diff --git a/packages/preset-mini/src/variants/breakpoints.ts b/packages/preset-mini/src/variants/breakpoints.ts index e865e0592b..a2e208fb87 100644 --- a/packages/preset-mini/src/variants/breakpoints.ts +++ b/packages/preset-mini/src/variants/breakpoints.ts @@ -4,7 +4,7 @@ import type { Theme } from '../theme' const regexCache: Record = {} -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) diff --git a/packages/transformer-directives/src/index.ts b/packages/transformer-directives/src/index.ts index ed0311a71e..939343acee 100644 --- a/packages/transformer-directives/src/index.ts +++ b/packages/transformer-directives/src/index.ts @@ -3,6 +3,8 @@ import type { SourceCodeTransformer, StringifiedUtil, UnoGenerator } from '@unoc 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 = { -readonly [P in keyof T]: T[P] } @@ -190,16 +192,44 @@ export async function transformDirectives( } const handleScreen = (node: Atrule) => { - let breakpointName - if (node.name === 'screen' && node.prelude && node.prelude.type === 'Raw') + let breakpointName = ''; let prefix + if (node.name === 'screen' && node.prelude?.type === 'Raw') breakpointName = node.prelude.value.trim() if (!breakpointName) return - // @ts-expect-error breakpoints aren't always available - const breakpointPx = uno.config.theme.breakpoints ? uno.config.theme.breakpoints[breakpointName] : null - if (!breakpointPx) + const match = breakpointName.match(/^(?:(lt|at)-)?(\w+)$/) + if (match) { + prefix = match[1] + breakpointName = match[2] + } + + const resolveBreakpoints = () => { + let breakpoints: Record | 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 @@ -213,7 +243,7 @@ export async function transformDirectives( code.overwrite( offset + match.index!, offset + match.index! + match[0].length, - `@media (min-width: ${breakpointPx}) `, + `${generateMediaQuery(breakpointName, prefix)} `, ) } } diff --git a/test/transformer-directives.test.ts b/test/transformer-directives.test.ts index 7e326b2d50..9eb15f6a67 100644 --- a/test/transformer-directives.test.ts +++ b/test/transformer-directives.test.ts @@ -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) { @@ -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 { @@ -396,96 +387,170 @@ describe('transformer-directives', () => { `) }) - test('basic', async () => { - const customUno = createGenerator({ - presets: [ - presetUno(), - ], - theme: { - breakpoints: { - xs: '320px', - sm: '640px', - md: '768px', - lg: '1024px', - xl: '1280px', - xxl: '1536px', - }, - }, - }) - 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; - } - }`, - customUno, - ) + 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 (min-width: 320px) { + @media (max-width: 319.9px) { .grid { grid-template-columns: repeat(1, minmax(0, 1fr)); } } - @media (min-width: 640px) { + @media (max-width: 639.9px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } - @media (min-width: 768px) { + @media (max-width: 767.9px) { .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } } - @media (min-width: 1024px) { + " + `) + }) + + test('@screen at variant', async () => { + const result = await transform(` .grid { - grid-template-columns: repeat(5, minmax(0, 1fr)); + @apply grid grid-cols-2; } - } - @media (min-width: 1280px) { - .grid { - grid-template-columns: repeat(6, minmax(0, 1fr)); + @screen at-xs { + .grid { + @apply grid-cols-1; + } } - } - @media (min-width: 1536px) { - .grid { - grid-template-columns: repeat(7, minmax(0, 1fr)); + @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()', () => {