Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions packages/tailwindcss/src/expand-declaration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { describe, expect, test } from 'vitest'
import { toCss } from './ast'
import { parse } from './css-parser'
import { expandDeclaration } from './expand-declaration'
import { SignatureFeatures } from './signatures'
import { walk, WalkAction } from './walk'

const css = String.raw

function expand(input: string, options: SignatureFeatures): string {
let ast = parse(input)

walk(ast, (node) => {
if (node.kind === 'declaration') {
let result = expandDeclaration(node, options)
if (result) return WalkAction.ReplaceSkip(result)
}
})

return toCss(ast)
}

describe('expand declarations', () => {
let options = SignatureFeatures.ExpandProperties

test('expand to 4 properties', () => {
let input = css`
.one {
inset: 10px;
}

.two {
inset: 10px 20px;
}

.three {
inset: 10px 20px 30px;
}

.four {
inset: 10px 20px 30px 40px;
}
`

expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
top: 10px;
right: 10px;
bottom: 10px;
left: 10px;
}
.two {
top: 10px;
right: 20px;
bottom: 10px;
left: 20px;
}
.three {
top: 10px;
right: 20px;
bottom: 30px;
left: 20px;
}
.four {
top: 10px;
right: 20px;
bottom: 30px;
left: 40px;
}
"
`)
})

test('expand to 2 properties', () => {
let input = css`
.one {
gap: 10px;
}

.two {
gap: 10px 20px;
}
`

expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
row-gap: 10px;
column-gap: 10px;
}
.two {
row-gap: 10px;
column-gap: 20px;
}
"
`)
})

test('expansion with `!important`', () => {
let input = css`
.one {
inset: 10px;
}

.two {
inset: 10px 20px;
}

.three {
inset: 10px 20px 30px !important;
}

.four {
inset: 10px 20px 30px 40px;
}
`

expect(expand(input, options)).toMatchInlineSnapshot(`
".one {
top: 10px;
right: 10px;
bottom: 10px;
left: 10px;
}
.two {
top: 10px;
right: 20px;
bottom: 10px;
left: 20px;
}
.three {
top: 10px !important;
right: 20px !important;
bottom: 30px !important;
left: 20px !important;
}
.four {
top: 10px;
right: 20px;
bottom: 30px;
left: 40px;
}
"
`)
})
})

describe('expand logical properties', () => {
let options = SignatureFeatures.ExpandProperties | SignatureFeatures.LogicalToPhysical

test('margin-block', () => {
let input = css`
.example {
margin-block: 10px 20px;
}
`

expect(expand(input, options)).toMatchInlineSnapshot(`
".example {
margin-top: 10px;
margin-bottom: 20px;
}
"
`)
})
})
93 changes: 93 additions & 0 deletions packages/tailwindcss/src/expand-declaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { decl, type AstNode } from './ast'
import { SignatureFeatures } from './signatures'
import { segment } from './utils/segment'

function createPrefixedQuad(
prefix: string,
t = 'top',
r = 'right',
b = 'bottom',
l = 'left',
): Record<number, [prop: string, index: number][]> {
return createBareQuad(`${prefix}-${t}`, `${prefix}-${r}`, `${prefix}-${b}`, `${prefix}-${l}`)
}

// prettier-ignore
function createBareQuad(t = 'top', r = 'right', b = 'bottom', l = 'left'): Record<number, [prop: string, index: number][]> {
return {
1: [[t, 0], [r, 0], [b, 0], [l, 0]],
2: [[t, 0], [r, 1], [b, 0], [l, 1]],
3: [[t, 0], [r, 1], [b, 2], [l, 1]],
4: [[t, 0], [r, 1], [b, 2], [l, 3]],
} as const;
}

// prettier-ignore
function createPair(lhs: string, rhs: string): Record<number, [prop: string, index: number][]> {
return {
1: [[lhs, 0], [rhs, 0]],
2: [[lhs, 0], [rhs, 1]],
} as const;
}

// Depending on the length of the value, map to different properties
let VARIADIC_EXPANSION_MAP: Record<string, Record<number, [prop: string, index: number][]>> = {
inset: createBareQuad(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These depend on the amount of arguments inset: 10px will set top, right, bottom, left to 10px. But 10px 20px will set top and bottom to 10px and left and right to 20px

margin: createPrefixedQuad('margin'),
padding: createPrefixedQuad('padding'),
gap: createPair('row-gap', 'column-gap'),
}

// Depending on the length of the value, map to different properties
let VARIADIC_LOGICAL_EXPANSION_MAP: Record<
string,
Record<number, [prop: string, index: number][]>
> = {
'inset-block': createPair('top', 'bottom'),
'inset-inline': createPair('left', 'right'),
'margin-block': createPair('margin-top', 'margin-bottom'),
'margin-inline': createPair('margin-left', 'margin-right'),
'padding-block': createPair('padding-top', 'padding-bottom'),
'padding-inline': createPair('padding-left', 'padding-right'),
}

// The entire value is mapped to each property
let LOGICAL_EXPANSION_MAP: Record<string, string[]> = {
'border-block': ['border-bottom', 'border-top'],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These on the other hand map to the physical properties as-is.

border-block: 1px solid red;

Would map to:

border-top: 1px solid red;
border-bottom: 1px solid red;

'border-block-color': ['border-bottom-color', 'border-top-color'],
'border-block-style': ['border-bottom-style', 'border-top-style'],
'border-block-width': ['border-bottom-width', 'border-top-width'],
'border-inline': ['border-left', 'border-right'],
'border-inline-color': ['border-left-color', 'border-right-color'],
'border-inline-style': ['border-left-style', 'border-right-style'],
'border-inline-width': ['border-left-width', 'border-right-width'],
}

export function expandDeclaration(
node: Extract<AstNode, { kind: 'declaration' }>,
options: SignatureFeatures,
): AstNode[] | null {
if (options & SignatureFeatures.LogicalToPhysical) {
if (node.property in VARIADIC_LOGICAL_EXPANSION_MAP) {
let args = segment(node.value!, ' ')
return VARIADIC_LOGICAL_EXPANSION_MAP[node.property][args.length]?.map(([prop, index]) => {
return decl(prop, args[index], node.important)
})
}

if (node.property in LOGICAL_EXPANSION_MAP) {
return LOGICAL_EXPANSION_MAP[node.property]?.map((prop) => {
return decl(prop, node.value!, node.important)
})
}
}

if (node.property in VARIADIC_EXPANSION_MAP) {
let args = segment(node.value!, ' ')
return VARIADIC_EXPANSION_MAP[node.property][args.length]?.map(([prop, index]) => {
return decl(prop, args[index], node.important)
})
}

return null
}