mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add functional utility syntax (#15455)
This PR adds support for functional utilities constructed via CSS.
# Registering functional utilities in CSS
To register a functional utility in CSS, use the `@utility potato-*`
syntax, where the `-*` signals that this is a functional utility:
```css
@utility tab-* {
tab-size: --value(--tab-size-*);
}
```
## Resolving values
The special `--value(…)` function is used to resolve the utility value.
### Resolving against `@theme` values
To resolve the value against a set of theme keys, use
`--value(--theme-key-*)`:
```css
@theme {
--tab-size-1: 1;
--tab-size-2: 2;
--tab-size-4: 4;
--tab-size-github: 8;
}
@utility tab-* {
/* tab-1, tab-2, tab-4, tab-github */
tab-size: --value(--tab-size-*);
}
```
### Bare values
To resolve the value as a bare value, use `--value({type})`, where
`{type}` is the data type you want to validate the bare value as:
```css
@utility tab-* {
/* tab-1, tab-76, tab-971 */
tab-size: --value(integer);
}
```
### Arbitrary values
To support arbitrary values, use `--value([{type}])` (notice the square
brackets) to tell Tailwind which types are supported as an arbitrary
value:
```css
@utility tab-* {
/* tab-[1], tab-[76], tab-[971] */
tab-size: --value([integer]);
}
```
### Supporting theme values, bare values, and arbitrary values together
All three forms of the `--value(…)` function can be used within a rule
as multiple declarations, and any declarations that fail to resolve will
be omitted in the output:
```css
@theme {
--tab-size-github: 8;
}
@utility tab-* {
tab-size: --value([integer]);
tab-size: --value(integer);
tab-size: --value(--tab-size-*);
}
```
This makes it possible to treat the value differently in each case if
necessary, for example translating a bare integer to a percentage:
```css
@utility opacity-* {
opacity: --value([percentage]);
opacity: calc(--value(integer) * 1%);
opacity: --value(--opacity-*);
}
```
The `--value(…)` function can also take multiple arguments and resolve
them left to right if you don't need to treat the return value
differently in different cases:
```css
@theme {
--tab-size-github: 8;
}
@utility tab-* {
tab-size: --value(--tab-size-*, integer, [integer]);
}
@utility opacity-* {
opacity: calc(--value(integer) * 1%);
opacity: --value(--opacity-*, [percentage]);
}
```
### Negative values
To support negative values, register separate positive and negative
utilities into separate declarations:
```css
@utility inset-* {
inset: calc(--var(--spacing) * --value([percentage], [length]));
}
@utility -inset-* {
inset: calc(--var(--spacing) * --value([percentage], [length]) * -1);
}
```
## Modifiers
Modifiers are handled using the `--modifier(…)` function which works
exactly like the `--value(…)` function but operates on a modifier if
present:
```css
@utility text-* {
font-size: --value(--font-size-*, [length]);
line-height: --modifier(--line-height-*, [length], [*]);
}
```
If a modifier isn't present, any declaration depending on a modifier is
just not included in the output.
## Fractions
To handle fractions, we rely on the CSS `ratio` data type. If this is
used with `--value(…)`, it's a signal to Tailwind to treat the value +
modifier as a single value:
```css
/* The CSS `ratio` type is our signal to treat the value + modifier as a fraction */
@utility aspect-* {
/* aspect-square, aspect-3/4, aspect-[7/9] */
aspect-ratio: --value(--aspect-ratio-*, ratio, [ratio]);
}
```
This commit is contained in:
parent
d6c4e72351
commit
ee3add9d08
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Add `@tailwindcss/browser` package to run Tailwind CSS in the browser ([#15558](https://github.com/tailwindlabs/tailwindcss/pull/15558))
|
||||
- Add `@reference "…"` API as a replacement for the previous `@import "…" reference` option ([#15565](https://github.com/tailwindlabs/tailwindcss/pull/15565))
|
||||
- Add functional utility syntax ([#15455](https://github.com/tailwindlabs/tailwindcss/pull/15455))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -26,12 +26,12 @@ import { substituteFunctions } from './css-functions'
|
||||
import * as CSS from './css-parser'
|
||||
import { buildDesignSystem, type DesignSystem } from './design-system'
|
||||
import { Theme, ThemeOptions } from './theme'
|
||||
import { createCssUtility } from './utilities'
|
||||
import { segment } from './utils/segment'
|
||||
import { compoundsForSelectors } from './variants'
|
||||
export type Config = UserConfig
|
||||
|
||||
const IS_VALID_PREFIX = /^[a-z]+$/
|
||||
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
|
||||
|
||||
type CompileOptions = {
|
||||
base?: string
|
||||
@ -174,25 +174,20 @@ async function parseCss(
|
||||
throw new Error('`@utility` cannot be nested.')
|
||||
}
|
||||
|
||||
let name = node.params
|
||||
|
||||
if (!IS_VALID_UTILITY_NAME.test(name)) {
|
||||
throw new Error(
|
||||
`\`@utility ${name}\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (node.nodes.length === 0) {
|
||||
throw new Error(
|
||||
`\`@utility ${name}\` is empty. Utilities should include at least one property.`,
|
||||
`\`@utility ${node.params}\` is empty. Utilities should include at least one property.`,
|
||||
)
|
||||
}
|
||||
|
||||
customUtilities.push((designSystem) => {
|
||||
designSystem.utilities.static(name, () => structuredClone(node.nodes))
|
||||
})
|
||||
let utility = createCssUtility(node)
|
||||
if (utility === null) {
|
||||
throw new Error(
|
||||
`\`@utility ${node.params}\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.`,
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
customUtilities.push(utility)
|
||||
}
|
||||
|
||||
// Collect paths from `@source` at-rules
|
||||
|
||||
@ -437,3 +437,69 @@ test('Custom at-rule variants do not show up as a value under `group`', async ()
|
||||
expect(not.values).toContain('variant-3')
|
||||
expect(not.values).toContain('variant-4')
|
||||
})
|
||||
|
||||
test('Custom functional @utility', async () => {
|
||||
let input = css`
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@theme reference {
|
||||
--tab-size-1: 1;
|
||||
--tab-size-2: 2;
|
||||
--tab-size-4: 4;
|
||||
--tab-size-github: 8;
|
||||
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
|
||||
--leading-foo: 1.5;
|
||||
--leading-bar: 2;
|
||||
}
|
||||
|
||||
@utility tab-* {
|
||||
tab-size: --value(--tab-size);
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
font-size: --value(--text);
|
||||
line-height: --value(--text- * --line-height);
|
||||
line-height: --modifier(--leading);
|
||||
}
|
||||
|
||||
@utility -negative-* {
|
||||
margin: --value(--tab-size- *);
|
||||
}
|
||||
`
|
||||
|
||||
let design = await __unstable__loadDesignSystem(input, {
|
||||
loadStylesheet: async (_, base) => ({
|
||||
base,
|
||||
content: '@tailwind utilities;',
|
||||
}),
|
||||
})
|
||||
|
||||
let classMap = new Map(design.getClassList())
|
||||
let classNames = Array.from(classMap.keys())
|
||||
|
||||
expect(classNames).toContain('tab-1')
|
||||
expect(classNames).toContain('tab-2')
|
||||
expect(classNames).toContain('tab-4')
|
||||
expect(classNames).toContain('tab-github')
|
||||
|
||||
expect(classNames).not.toContain('-tab-1')
|
||||
expect(classNames).not.toContain('-tab-2')
|
||||
expect(classNames).not.toContain('-tab-4')
|
||||
expect(classNames).not.toContain('-tab-github')
|
||||
|
||||
expect(classNames).toContain('-negative-1')
|
||||
expect(classNames).toContain('-negative-2')
|
||||
expect(classNames).toContain('-negative-4')
|
||||
expect(classNames).toContain('-negative-github')
|
||||
|
||||
expect(classNames).not.toContain('--negative-1')
|
||||
expect(classNames).not.toContain('--negative-2')
|
||||
expect(classNames).not.toContain('--negative-4')
|
||||
expect(classNames).not.toContain('--negative-github')
|
||||
|
||||
expect(classNames).toContain('example-xs')
|
||||
expect(classMap.get('example-xs')?.modifiers).toEqual(['foo', 'bar'])
|
||||
})
|
||||
|
||||
@ -68,7 +68,7 @@ export class Theme {
|
||||
}
|
||||
}
|
||||
|
||||
keysInNamespaces(themeKeys: ThemeKey[]): string[] {
|
||||
keysInNamespaces(themeKeys: Iterable<ThemeKey>): string[] {
|
||||
let keys: string[] = []
|
||||
|
||||
for (let namespace of themeKeys) {
|
||||
|
||||
@ -17259,7 +17259,7 @@ describe('custom utilities', () => {
|
||||
test('custom utilities must use a valid name definitions', async () => {
|
||||
await expect(() =>
|
||||
compile(css`
|
||||
@utility push-* {
|
||||
@utility push-| {
|
||||
right: 100%;
|
||||
}
|
||||
`),
|
||||
@ -17438,4 +17438,754 @@ describe('custom utilities', () => {
|
||||
`[Error: You cannot \`@apply\` the \`hover:bar\` utility here because it creates a circular dependency.]`,
|
||||
)
|
||||
})
|
||||
|
||||
describe('functional utilities', () => {
|
||||
test('resolving values from `@theme`', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--tab-size-1: 1;
|
||||
--tab-size-2: 2;
|
||||
--tab-size-4: 4;
|
||||
--tab-size-github: 8;
|
||||
}
|
||||
|
||||
@utility tab-* {
|
||||
tab-size: --value(--tab-size);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".tab-1 {
|
||||
tab-size: 1;
|
||||
}
|
||||
|
||||
.tab-2 {
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.tab-4 {
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.tab-github {
|
||||
tab-size: 8;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving values from `@theme`, with `--tab-size-*` syntax', async () => {
|
||||
let input =
|
||||
// Explicitly not using the css tagged template literal so that
|
||||
// Prettier doesn't format the `value(--tab-size-*)` as
|
||||
// `value(--tab-size- *)`
|
||||
`
|
||||
@theme reference {
|
||||
--tab-size-1: 1;
|
||||
--tab-size-2: 2;
|
||||
--tab-size-4: 4;
|
||||
--tab-size-github: 8;
|
||||
}
|
||||
|
||||
@utility tab-* {
|
||||
tab-size: --value(--tab-size-*);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".tab-1 {
|
||||
tab-size: 1;
|
||||
}
|
||||
|
||||
.tab-2 {
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.tab-4 {
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.tab-github {
|
||||
tab-size: 8;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving values from `@theme`, with `--tab-size-\\*` syntax (prettier friendly)', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--tab-size-1: 1;
|
||||
--tab-size-2: 2;
|
||||
--tab-size-4: 4;
|
||||
--tab-size-github: 8;
|
||||
}
|
||||
|
||||
@utility tab-* {
|
||||
tab-size: --value(--tab-size-\*);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['tab-1', 'tab-2', 'tab-4', 'tab-github']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".tab-1 {
|
||||
tab-size: 1;
|
||||
}
|
||||
|
||||
.tab-2 {
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.tab-4 {
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.tab-github {
|
||||
tab-size: 8;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['tab-3', 'tab-gitlab'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving bare values', async () => {
|
||||
let input = css`
|
||||
@utility tab-* {
|
||||
tab-size: --value(integer);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['tab-1', 'tab-76', 'tab-971'])).toMatchInlineSnapshot(`
|
||||
".tab-1 {
|
||||
tab-size: 1;
|
||||
}
|
||||
|
||||
.tab-76 {
|
||||
tab-size: 76;
|
||||
}
|
||||
|
||||
.tab-971 {
|
||||
tab-size: 971;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['tab-foo'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving bare values with constraints for integer, percentage, and ratio', async () => {
|
||||
let input = css`
|
||||
@utility example-* {
|
||||
--value-as-number: --value(number);
|
||||
--value-as-percentage: --value(percentage);
|
||||
--value-as-ratio: --value(ratio);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['example-1', 'example-0.5', 'example-20%', 'example-2/3']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".example-0\\.5 {
|
||||
--value-as-number: .5;
|
||||
}
|
||||
|
||||
.example-1 {
|
||||
--value-as-number: 1;
|
||||
}
|
||||
|
||||
.example-2\\/3 {
|
||||
--value-as-ratio: 2 / 3;
|
||||
}
|
||||
|
||||
.example-20\\% {
|
||||
--value-as-percentage: 20%;
|
||||
}"
|
||||
`)
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'example-1.23',
|
||||
'example-12.34%',
|
||||
'example-1.2/3',
|
||||
'example-1/2.3',
|
||||
'example-1.2/3.4',
|
||||
]),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving unsupported bare values', async () => {
|
||||
let input = css`
|
||||
@utility tab-* {
|
||||
tab-size: --value(color);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['tab-#0088cc', 'tab-foo'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving arbitrary values', async () => {
|
||||
let input = css`
|
||||
@utility tab-* {
|
||||
tab-size: --value([integer]);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'tab-[1]',
|
||||
'tab-[76]',
|
||||
'tab-[971]',
|
||||
'tab-[integer:var(--my-value)]',
|
||||
'tab-(integer:my-value)',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".tab-\\[1\\] {
|
||||
tab-size: 1;
|
||||
}
|
||||
|
||||
.tab-\\[76\\] {
|
||||
tab-size: 76;
|
||||
}
|
||||
|
||||
.tab-\\[971\\] {
|
||||
tab-size: 971;
|
||||
}
|
||||
|
||||
.tab-\\[integer\\:var\\(--my-value\\)\\] {
|
||||
tab-size: var(--my-value);
|
||||
}"
|
||||
`)
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'tab-[#0088cc]',
|
||||
'tab-[1px]',
|
||||
'tab-[var(--my-value)]',
|
||||
'tab-(--my-value)',
|
||||
'tab-[color:var(--my-value)]',
|
||||
'tab-(color:--my-value)',
|
||||
]),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
test('resolving any arbitrary values', async () => {
|
||||
let input = css`
|
||||
@utility tab-* {
|
||||
tab-size: --value([ *]);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'tab-[1]',
|
||||
'tab-[76]',
|
||||
'tab-[971]',
|
||||
'tab-[var(--my-value)]',
|
||||
'tab-(--my-value)',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".tab-\\(--my-value\\) {
|
||||
tab-size: var(--my-value);
|
||||
}
|
||||
|
||||
.tab-\\[1\\] {
|
||||
tab-size: 1;
|
||||
}
|
||||
|
||||
.tab-\\[76\\] {
|
||||
tab-size: 76;
|
||||
}
|
||||
|
||||
.tab-\\[971\\] {
|
||||
tab-size: 971;
|
||||
}
|
||||
|
||||
.tab-\\[var\\(--my-value\\)\\] {
|
||||
tab-size: var(--my-value);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('resolving any arbitrary values (without space)', async () => {
|
||||
let input = `
|
||||
@utility tab-* {
|
||||
tab-size: --value([*]);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'tab-[1]',
|
||||
'tab-[76]',
|
||||
'tab-[971]',
|
||||
'tab-[var(--my-value)]',
|
||||
'tab-(--my-value)',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".tab-\\(--my-value\\) {
|
||||
tab-size: var(--my-value);
|
||||
}
|
||||
|
||||
.tab-\\[1\\] {
|
||||
tab-size: 1;
|
||||
}
|
||||
|
||||
.tab-\\[76\\] {
|
||||
tab-size: 76;
|
||||
}
|
||||
|
||||
.tab-\\[971\\] {
|
||||
tab-size: 971;
|
||||
}
|
||||
|
||||
.tab-\\[var\\(--my-value\\)\\] {
|
||||
tab-size: var(--my-value);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('resolving any arbitrary values (with escaped `*`)', async () => {
|
||||
let input = css`
|
||||
@utility tab-* {
|
||||
tab-size: --value([\*]);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'tab-[1]',
|
||||
'tab-[76]',
|
||||
'tab-[971]',
|
||||
'tab-[var(--my-value)]',
|
||||
'tab-(--my-value)',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".tab-\\(--my-value\\) {
|
||||
tab-size: var(--my-value);
|
||||
}
|
||||
|
||||
.tab-\\[1\\] {
|
||||
tab-size: 1;
|
||||
}
|
||||
|
||||
.tab-\\[76\\] {
|
||||
tab-size: 76;
|
||||
}
|
||||
|
||||
.tab-\\[971\\] {
|
||||
tab-size: 971;
|
||||
}
|
||||
|
||||
.tab-\\[var\\(--my-value\\)\\] {
|
||||
tab-size: var(--my-value);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('resolving theme, bare and arbitrary values all at once', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--tab-size-github: 8;
|
||||
}
|
||||
|
||||
@utility tab-* {
|
||||
tab-size: --value([integer]);
|
||||
tab-size: --value(integer);
|
||||
tab-size: --value(--tab-size);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['tab-github', 'tab-76', 'tab-[123]'])).toMatchInlineSnapshot(`
|
||||
".tab-76 {
|
||||
tab-size: 76;
|
||||
}
|
||||
|
||||
.tab-\\[123\\] {
|
||||
tab-size: 123;
|
||||
}
|
||||
|
||||
.tab-github {
|
||||
tab-size: 8;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['tab-[#0088cc]', 'tab-[1px]'])).toEqual('')
|
||||
})
|
||||
|
||||
test('in combination with calc to produce different data types of values', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--example-full: 100%;
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
--value: --value([percentage]);
|
||||
--value: calc(--value(integer) * 1%);
|
||||
--value: --value(--example);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['example-full', 'example-12', 'example-[20%]']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".example-12 {
|
||||
--value: calc(12 * 1%);
|
||||
}
|
||||
|
||||
.example-\\[20\\%\\] {
|
||||
--value: 20%;
|
||||
}
|
||||
|
||||
.example-full {
|
||||
--value: 100%;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['example-half', 'example-[#0088cc]'])).toEqual('')
|
||||
})
|
||||
|
||||
test('shorthand if resulting values are of the same type', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--tab-size-github: 8;
|
||||
--example-full: 100%;
|
||||
}
|
||||
|
||||
@utility tab-* {
|
||||
tab-size: --value(--tab-size, integer, [integer]);
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
--value: calc(--value(integer) * 1%);
|
||||
--value: --value(--example, [percentage]);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'tab-github',
|
||||
'tab-76',
|
||||
'tab-[123]',
|
||||
'example-37',
|
||||
'example-[50%]',
|
||||
'example-full',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".example-37 {
|
||||
--value: calc(37 * 1%);
|
||||
}
|
||||
|
||||
.example-\\[50\\%\\] {
|
||||
--value: 50%;
|
||||
}
|
||||
|
||||
.example-full {
|
||||
--value: 100%;
|
||||
}
|
||||
|
||||
.tab-76 {
|
||||
tab-size: 76;
|
||||
}
|
||||
|
||||
.tab-\\[123\\] {
|
||||
tab-size: 123;
|
||||
}
|
||||
|
||||
.tab-github {
|
||||
tab-size: 8;
|
||||
}"
|
||||
`)
|
||||
expect(
|
||||
await compileCss(input, ['tab-[#0088cc]', 'tab-[1px]', 'example-foo', 'example-[13px]']),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
test('negative values', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--example-full: 100%;
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
--value: --value(--example, [percentage], [length]);
|
||||
}
|
||||
|
||||
@utility -example-* {
|
||||
--value: calc(--value(--example, [percentage], [length]) * -1);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'example-full',
|
||||
'-example-full',
|
||||
'example-[10px]',
|
||||
'-example-[10px]',
|
||||
'example-[20%]',
|
||||
'-example-[20%]',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".-example-\\[10px\\] {
|
||||
--value: calc(10px * -1);
|
||||
}
|
||||
|
||||
.-example-\\[20\\%\\] {
|
||||
--value: calc(20% * -1);
|
||||
}
|
||||
|
||||
.-example-full {
|
||||
--value: calc(100% * -1);
|
||||
}
|
||||
|
||||
.example-\\[10px\\] {
|
||||
--value: 10px;
|
||||
}
|
||||
|
||||
.example-\\[20\\%\\] {
|
||||
--value: 20%;
|
||||
}
|
||||
|
||||
.example-full {
|
||||
--value: 100%;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['example-10'])).toEqual('')
|
||||
})
|
||||
|
||||
test('using the same value multiple times', async () => {
|
||||
let input = css`
|
||||
@utility example-* {
|
||||
--value: calc(var(--spacing) * --value(number)) calc(var(--spacing) * --value(number));
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['example-12'])).toMatchInlineSnapshot(`
|
||||
".example-12 {
|
||||
--value: calc(var(--spacing) * 12) calc(var(--spacing) * 12);
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('modifiers', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--value-sm: 14px;
|
||||
--modifier-7: 28px;
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
--value: --value(--value, [length]);
|
||||
--modifier: --modifier(--modifier, [length]);
|
||||
--modifier-with-calc: calc(--modifier(--modifier, [length]) * 2);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(
|
||||
await compileCss(input, [
|
||||
'example-sm',
|
||||
'example-sm/7',
|
||||
'example-[12px]',
|
||||
'example-[12px]/[16px]',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".example-\\[12px\\] {
|
||||
--value: 12px;
|
||||
}
|
||||
|
||||
.example-\\[12px\\]\\/\\[16px\\] {
|
||||
--value: 12px;
|
||||
--modifier: 16px;
|
||||
--modifier-with-calc: calc(16px * 2);
|
||||
}
|
||||
|
||||
.example-sm {
|
||||
--value: 14px;
|
||||
}
|
||||
|
||||
.example-sm\\/7 {
|
||||
--value: 14px;
|
||||
--modifier: 28px;
|
||||
--modifier-with-calc: calc(28px * 2);
|
||||
}"
|
||||
`)
|
||||
expect(
|
||||
await compileCss(input, ['example-foo', 'example-foo/[12px]', 'example-foo/12']),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
test('fractions', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--example-video: 16 / 9;
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
--value: --value(--example, ratio, [ratio]);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['example-video', 'example-1/1', 'example-[7/9]']))
|
||||
.toMatchInlineSnapshot(`
|
||||
".example-1\\/1 {
|
||||
--value: 1 / 1;
|
||||
}
|
||||
|
||||
.example-\\[7\\/9\\] {
|
||||
--value: 7 / 9;
|
||||
}
|
||||
|
||||
.example-video {
|
||||
--value: 16 / 9;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['example-foo'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolve theme values with sub-namespace (--text- * --line-height)', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
font-size: --value(--text);
|
||||
line-height: --value(--text- * --line-height);
|
||||
line-height: --modifier(number);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(`
|
||||
".example-xs {
|
||||
font-size: .75rem;
|
||||
line-height: 1.33333;
|
||||
}
|
||||
|
||||
.example-xs\\/6 {
|
||||
font-size: .75rem;
|
||||
line-height: 6;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolve theme values with sub-namespace (--text-\\* --line-height)', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
font-size: --value(--text);
|
||||
line-height: --value(--text-\* --line-height);
|
||||
line-height: --modifier(number);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(`
|
||||
".example-xs {
|
||||
font-size: .75rem;
|
||||
line-height: 1.33333;
|
||||
}
|
||||
|
||||
.example-xs\\/6 {
|
||||
font-size: .75rem;
|
||||
line-height: 6;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolve theme values with sub-namespace (--value(--text --line-height))', async () => {
|
||||
let input = css`
|
||||
@theme reference {
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
font-size: --value(--text);
|
||||
line-height: --value(--text --line-height);
|
||||
line-height: --modifier(number);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(`
|
||||
".example-xs {
|
||||
font-size: .75rem;
|
||||
line-height: 1.33333;
|
||||
}
|
||||
|
||||
.example-xs\\/6 {
|
||||
font-size: .75rem;
|
||||
line-height: 6;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('')
|
||||
})
|
||||
|
||||
test('resolve theme values with sub-namespace (--value(--text-*--line-height))', async () => {
|
||||
let input = `
|
||||
@theme reference {
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
}
|
||||
|
||||
@utility example-* {
|
||||
font-size: --value(--text);
|
||||
line-height: --value(--text-*--line-height);
|
||||
line-height: --modifier(number);
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
expect(await compileCss(input, ['example-xs', 'example-xs/6'])).toMatchInlineSnapshot(`
|
||||
".example-xs {
|
||||
font-size: .75rem;
|
||||
line-height: 1.33333;
|
||||
}
|
||||
|
||||
.example-xs\\/6 {
|
||||
font-size: .75rem;
|
||||
line-height: 6;
|
||||
}"
|
||||
`)
|
||||
expect(await compileCss(input, ['example-foo', 'example-xs/foo'])).toEqual('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
import { atRoot, atRule, decl, styleRule, type AstNode } from './ast'
|
||||
import {
|
||||
atRoot,
|
||||
atRule,
|
||||
decl,
|
||||
styleRule,
|
||||
walk,
|
||||
type AstNode,
|
||||
type AtRule,
|
||||
type Declaration,
|
||||
type Rule,
|
||||
} from './ast'
|
||||
import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate'
|
||||
import type { DesignSystem } from './design-system'
|
||||
import type { Theme, ThemeKey } from './theme'
|
||||
import { compareBreakpoints } from './utils/compare-breakpoints'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
@ -11,6 +22,10 @@ import {
|
||||
} from './utils/infer-data-type'
|
||||
import { replaceShadowColors } from './utils/replace-shadow-colors'
|
||||
import { segment } from './utils/segment'
|
||||
import * as ValueParser from './value-parser'
|
||||
|
||||
const IS_VALID_STATIC_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
|
||||
const IS_VALID_FUNCTIONAL_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*-\*$/
|
||||
|
||||
type CompileFn<T extends Candidate['kind']> = (
|
||||
value: Extract<Candidate, { kind: T }>,
|
||||
@ -4569,3 +4584,360 @@ export function createUtilities(theme: Theme) {
|
||||
|
||||
return utilities
|
||||
}
|
||||
|
||||
export function createCssUtility(node: AtRule) {
|
||||
let name = node.params
|
||||
|
||||
// Functional utilities. E.g.: `tab-size-*`
|
||||
if (IS_VALID_FUNCTIONAL_UTILITY_NAME.test(name)) {
|
||||
// API:
|
||||
//
|
||||
// - `--value(number)` resolves a bare value of type number
|
||||
// - `--value([number])` resolves an arbitrary value of type number
|
||||
// - `--value(--color)` resolves a theme value in the `color` namespace
|
||||
// - `--value(number, [number])` resolves a bare value of type number or an
|
||||
// arbitrary value of type number in order.
|
||||
//
|
||||
// Rules:
|
||||
//
|
||||
// - If `--value(…)` does not resolve to a valid value, the declaration is
|
||||
// removed.
|
||||
// - If a `--value(ratio)` resolves, the `--modifier(…)` cannot be used.
|
||||
// - If a candidate looks like `foo-2/3`, then the `--value(ratio)` should
|
||||
// be used OR the `--value(…)` and `--modifier(…)` must be used. But not
|
||||
// both.
|
||||
// - All parts of the candidate must resolve, otherwise it's not a valid
|
||||
// utility. E.g.:`
|
||||
// ```
|
||||
// @utility foo-* {
|
||||
// test: --value(number)
|
||||
// }
|
||||
// ```
|
||||
// If you then use `foo-1/2`, this is invalid, because the modifier is not used.
|
||||
|
||||
return (designSystem: DesignSystem) => {
|
||||
let valueThemeKeys = new Set<`--${string}`>()
|
||||
let modifierThemeKeys = new Set<`--${string}`>()
|
||||
|
||||
// Pre-process the AST to make it easier to work with.
|
||||
//
|
||||
// - Normalize theme values used in `--value(…)` and `--modifier(…)`
|
||||
// functions.
|
||||
// - Track information for suggestions
|
||||
walk(node.nodes, (child) => {
|
||||
if (child.kind !== 'declaration') return
|
||||
if (!child.value) return
|
||||
if (!child.value.includes('--value(') && !child.value.includes('--modifier(')) return
|
||||
|
||||
let declarationValueAst = ValueParser.parse(child.value)
|
||||
|
||||
// Required manipulations:
|
||||
//
|
||||
// - `--value(--spacing)` -> `--value(--spacing-*)`
|
||||
// - `--value(--spacing- *)` -> `--value(--spacing-*)`
|
||||
// - `--value(--text- * --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value(--text --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value(--text-\\* --line-height)` -> `--value(--text-*--line-height)`
|
||||
// - `--value([ *])` -> `--value([*])`
|
||||
//
|
||||
// Once Prettier / Biome handle these better (e.g.: not crashing without
|
||||
// `\\*` or not inserting whitespace) then most of these can go away.
|
||||
ValueParser.walk(declarationValueAst, (fn) => {
|
||||
if (fn.kind !== 'function') return
|
||||
if (fn.value !== '--value' && fn.value !== '--modifier') return
|
||||
|
||||
let args = segment(ValueParser.toCss(fn.nodes), ',')
|
||||
for (let [idx, arg] of args.entries()) {
|
||||
// Transform escaped `\\*` -> `*`
|
||||
arg = arg.replace(/\\\*/g, '*')
|
||||
|
||||
// Ensure `--value(--foo --bar)` becomes `--value(--foo-*--bar)`
|
||||
arg = arg.replace(/--(.*?)\s--(.*?)/g, '--$1-*--$2')
|
||||
|
||||
// Remove whitespace, e.g.: `--value([ *])` -> `--value([*])`
|
||||
arg = arg.replace(/\s+/g, '')
|
||||
|
||||
// Ensure multiple `-*` becomes a single `-*`
|
||||
arg = arg.replace(/(-\*){2,}/g, '-*')
|
||||
|
||||
// Ensure trailing `-*` exists if `-*` isn't present yet
|
||||
if (arg[0] === '-' && arg[1] === '-' && !arg.includes('-*')) {
|
||||
arg += '-*'
|
||||
}
|
||||
|
||||
args[idx] = arg
|
||||
}
|
||||
fn.nodes = ValueParser.parse(args.join(','))
|
||||
|
||||
// Track the theme keys for suggestions
|
||||
for (let node of fn.nodes) {
|
||||
if (node.kind === 'word' && node.value[0] === '-' && node.value[1] === '-') {
|
||||
let value = node.value.replace(/-\*.*$/g, '') as `--${string}`
|
||||
|
||||
if (fn.value === '--value') {
|
||||
valueThemeKeys.add(value)
|
||||
} else if (fn.value === '--modifier') {
|
||||
modifierThemeKeys.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
child.value = ValueParser.toCss(declarationValueAst)
|
||||
})
|
||||
|
||||
designSystem.utilities.functional(name.slice(0, -2), (candidate) => {
|
||||
let atRule = structuredClone(node)
|
||||
|
||||
let value = candidate.value
|
||||
let modifier = candidate.modifier
|
||||
|
||||
// A value is required for functional utilities, if you want to accept
|
||||
// just `tab-size`, you'd have to use a static utility.
|
||||
if (value === null) return
|
||||
|
||||
// Whether `--value(…)` was used
|
||||
let usedValueFn = false
|
||||
|
||||
// Whether any of the declarations successfully resolved a `--value(…)`.
|
||||
// E.g:
|
||||
// ```css
|
||||
// @utility tab-size-* {
|
||||
// tab-size: --value(integer);
|
||||
// tab-size: --value(--tab-size);
|
||||
// tab-size: --value([integer]);
|
||||
// }
|
||||
// ```
|
||||
// Any of these `tab-size` declarations have to resolve to a valid in
|
||||
// order to make the utility valid.
|
||||
let resolvedValueFn = false
|
||||
|
||||
// Whether `--modifier(…)` was used
|
||||
let usedModifierFn = false
|
||||
|
||||
// Whether any of the declarations successfully resolved a `--modifier(…)`
|
||||
let resolvedModifierFn = false
|
||||
|
||||
// A map of all declarations we replaced and their parent rules. We
|
||||
// might need to remove some later on. E.g.: remove declarations that
|
||||
// used `--value(number)` when `--value(ratio)` was resolved.
|
||||
let resolvedDeclarations = new Map<Declaration, AtRule | Rule>()
|
||||
|
||||
// Whether `--value(ratio)` was resolved
|
||||
let resolvedRatioValue = false
|
||||
|
||||
walk([atRule], (node, { parent, replaceWith: replaceDeclarationWith }) => {
|
||||
if (parent?.kind !== 'rule' && parent?.kind !== 'at-rule') return
|
||||
if (node.kind !== 'declaration') return
|
||||
if (!node.value) return
|
||||
|
||||
let valueAst = ValueParser.parse(node.value)
|
||||
let result =
|
||||
ValueParser.walk(valueAst, (valueNode, { replaceWith }) => {
|
||||
if (valueNode.kind !== 'function') return
|
||||
|
||||
// Value function, e.g.: `--value(integer)`
|
||||
if (valueNode.value === '--value') {
|
||||
usedValueFn = true
|
||||
|
||||
let resolved = resolveValueFunction(value, valueNode, designSystem)
|
||||
if (resolved) {
|
||||
resolvedValueFn = true
|
||||
if (resolved.ratio) {
|
||||
resolvedRatioValue = true
|
||||
} else {
|
||||
resolvedDeclarations.set(node, parent)
|
||||
}
|
||||
replaceWith(resolved.nodes)
|
||||
return ValueParser.ValueWalkAction.Skip
|
||||
}
|
||||
|
||||
// Drop the declaration in case we couldn't resolve the value
|
||||
usedValueFn ||= false
|
||||
replaceDeclarationWith([])
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
}
|
||||
|
||||
// Modifier function, e.g.: `--modifier(integer)`
|
||||
else if (valueNode.value === '--modifier') {
|
||||
// If there is no modifier present in the candidate, then the
|
||||
// declaration can be removed.
|
||||
if (modifier === null) {
|
||||
replaceDeclarationWith([])
|
||||
return ValueParser.ValueWalkAction.Skip
|
||||
}
|
||||
|
||||
usedModifierFn = true
|
||||
|
||||
let replacement = resolveValueFunction(modifier, valueNode, designSystem)
|
||||
if (replacement) {
|
||||
resolvedModifierFn = true
|
||||
replaceWith(replacement.nodes)
|
||||
return ValueParser.ValueWalkAction.Skip
|
||||
}
|
||||
|
||||
// Drop the declaration in case we couldn't resolve the value
|
||||
usedModifierFn ||= false
|
||||
replaceDeclarationWith([])
|
||||
return ValueParser.ValueWalkAction.Stop
|
||||
}
|
||||
}) ?? ValueParser.ValueWalkAction.Continue
|
||||
|
||||
if (result === ValueParser.ValueWalkAction.Continue) {
|
||||
node.value = ValueParser.toCss(valueAst)
|
||||
}
|
||||
})
|
||||
|
||||
// Used `--value(…)` but nothing resolved
|
||||
if (usedValueFn && !resolvedValueFn) return null
|
||||
|
||||
// Used `--modifier(…)` but nothing resolved
|
||||
if (usedModifierFn && !resolvedModifierFn) return null
|
||||
|
||||
// Resolved `--value(ratio)` and `--modifier(…)`, which is invalid
|
||||
if (resolvedRatioValue && resolvedModifierFn) return null
|
||||
|
||||
// When a candidate has a modifier, then the `--modifier(…)` must
|
||||
// resolve correctly or the `--value(ratio)` must resolve correctly.
|
||||
if (modifier && !resolvedRatioValue && !resolvedModifierFn) return null
|
||||
|
||||
// Resolved `--value(ratio)`, so all other declarations that didn't use
|
||||
// `--value(ratio)` should be removed. E.g.: `--value(number)` would
|
||||
// otherwise resolve for `foo-1/2`.
|
||||
if (resolvedRatioValue) {
|
||||
for (let [declaration, parent] of resolvedDeclarations) {
|
||||
let idx = parent.nodes.indexOf(declaration)
|
||||
if (idx !== -1) parent.nodes.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return atRule.nodes
|
||||
})
|
||||
|
||||
designSystem.utilities.suggest(name.slice(0, -2), () => {
|
||||
return [
|
||||
{
|
||||
values: designSystem.theme
|
||||
.keysInNamespaces(valueThemeKeys)
|
||||
.map((x) => x.replaceAll('_', '.')),
|
||||
modifiers: designSystem.theme
|
||||
.keysInNamespaces(modifierThemeKeys)
|
||||
.map((x) => x.replaceAll('_', '.')),
|
||||
},
|
||||
] satisfies SuggestionGroup[]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (IS_VALID_STATIC_UTILITY_NAME.test(name)) {
|
||||
return (designSystem: DesignSystem) => {
|
||||
designSystem.utilities.static(name, () => structuredClone(node.nodes))
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function resolveValueFunction(
|
||||
value: NonNullable<
|
||||
| Extract<Candidate, { kind: 'functional' }>['value']
|
||||
| Extract<Candidate, { kind: 'functional' }>['modifier']
|
||||
>,
|
||||
fn: ValueParser.ValueFunctionNode,
|
||||
designSystem: DesignSystem,
|
||||
): { nodes: ValueParser.ValueAstNode[]; ratio?: boolean } | undefined {
|
||||
for (let arg of fn.nodes) {
|
||||
// Resolving theme value, e.g.: `--value(--color)`
|
||||
if (
|
||||
value.kind === 'named' &&
|
||||
arg.kind === 'word' &&
|
||||
arg.value[0] === '-' &&
|
||||
arg.value[1] === '-'
|
||||
) {
|
||||
let resolved = designSystem.resolveThemeValue(arg.value.replace('*', value.value))
|
||||
if (resolved) return { nodes: ValueParser.parse(resolved) }
|
||||
}
|
||||
|
||||
// Bare value, e.g.: `--value(integer)`
|
||||
else if (value.kind === 'named' && arg.kind === 'word') {
|
||||
// Limit the bare value types, to prevent new syntax that we
|
||||
// don't want to support. E.g.: `text-#000` is something we
|
||||
// don't want to support, but could be built this way.
|
||||
if (
|
||||
arg.value !== 'number' &&
|
||||
arg.value !== 'integer' &&
|
||||
arg.value !== 'ratio' &&
|
||||
arg.value !== 'percentage'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
let resolved = arg.value === 'ratio' && 'fraction' in value ? value.fraction : value.value
|
||||
if (!resolved) continue
|
||||
|
||||
let type = inferDataType(resolved, [arg.value as any])
|
||||
if (type === null) continue
|
||||
|
||||
// Ratio must be a valid fraction, e.g.: <integer>/<integer>
|
||||
if (type === 'ratio') {
|
||||
let [lhs, rhs] = segment(resolved, '/')
|
||||
if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) continue
|
||||
}
|
||||
|
||||
// Non-integer numbers should be a valid multiplier,
|
||||
// e.g.: `1.5`
|
||||
else if (type === 'number' && !isValidSpacingMultiplier(resolved)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Percentages must be an integer, e.g.: `50%`
|
||||
else if (type === 'percentage' && !isPositiveInteger(resolved.slice(0, -1))) {
|
||||
continue
|
||||
}
|
||||
|
||||
return { nodes: ValueParser.parse(resolved), ratio: type === 'ratio' }
|
||||
}
|
||||
|
||||
// Arbitrary value, e.g.: `--value([integer])`
|
||||
else if (
|
||||
value.kind === 'arbitrary' &&
|
||||
arg.kind === 'word' &&
|
||||
arg.value[0] === '[' &&
|
||||
arg.value[arg.value.length - 1] === ']'
|
||||
) {
|
||||
let dataType = arg.value.slice(1, -1)
|
||||
|
||||
// Allow any data type, e.g.: `--value([*])`
|
||||
if (dataType === '*') {
|
||||
return { nodes: ValueParser.parse(value.value) }
|
||||
}
|
||||
|
||||
// The forced arbitrary value hint must match the expected
|
||||
// data type.
|
||||
//
|
||||
// ```css
|
||||
// @utility tab-* {
|
||||
// tab-size: --value([integer]);
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// Given a candidate like `tab-(color:var(--my-value))`,
|
||||
// should not match because `color` and `integer` don't
|
||||
// match.
|
||||
if ('dataType' in value && value.dataType && value.dataType !== dataType) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use the provided data type hint
|
||||
if ('dataType' in value && value.dataType) {
|
||||
return { nodes: ValueParser.parse(value.value) }
|
||||
}
|
||||
|
||||
// No data type hint provided, so we have to infer it
|
||||
let type = inferDataType(value.value, [dataType as any])
|
||||
if (type !== null) {
|
||||
return { nodes: ValueParser.parse(value.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,9 @@ type DataType =
|
||||
| 'color'
|
||||
| 'length'
|
||||
| 'percentage'
|
||||
| 'ratio'
|
||||
| 'number'
|
||||
| 'integer'
|
||||
| 'url'
|
||||
| 'position'
|
||||
| 'bg-size'
|
||||
@ -23,7 +25,9 @@ const checks: Record<DataType, (value: string) => boolean> = {
|
||||
color: isColor,
|
||||
length: isLength,
|
||||
percentage: isPercentage,
|
||||
ratio: isFraction,
|
||||
number: isNumber,
|
||||
integer: isPositiveInteger,
|
||||
url: isUrl,
|
||||
position: isBackgroundPosition,
|
||||
'bg-size': isBackgroundSize,
|
||||
@ -173,6 +177,14 @@ function isPercentage(value: string): boolean {
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
const IS_FRACTION = new RegExp(`^${HAS_NUMBER.source}\s*/\s*${HAS_NUMBER.source}$`)
|
||||
|
||||
function isFraction(value: string): boolean {
|
||||
return IS_FRACTION.test(value) || hasMathFn(value)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Please refer to MDN when updating this list:
|
||||
* @see https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user