@wix/zero-config-implementation 1.61.0 → 1.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"registry": "https://registry.npmjs.org/",
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.62.0",
|
|
8
8
|
"description": "Core library for extracting component manifests from JS and CSS files",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "dist/index.js",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
]
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
|
-
"falconPackageHash": "
|
|
83
|
+
"falconPackageHash": "bd7035914229aba9dd44c1b5098381d537a4c3b4e7c58e03bd0c5e84"
|
|
84
84
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { resolveLonghand, splitAxis, splitBox, splitCorners, splitTopLevelTokens } from './css-longhand-resolver'
|
|
3
|
+
|
|
4
|
+
describe('splitTopLevelTokens', () => {
|
|
5
|
+
it('splits a single token', () => {
|
|
6
|
+
expect(splitTopLevelTokens('1rem')).toEqual(['1rem'])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('splits multiple plain tokens', () => {
|
|
10
|
+
expect(splitTopLevelTokens('1px solid red')).toEqual(['1px', 'solid', 'red'])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('keeps calc() intact even with embedded spaces', () => {
|
|
14
|
+
expect(splitTopLevelTokens('calc(1rem + 2px) 4px')).toEqual(['calc(1rem + 2px)', '4px'])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('keeps var() intact', () => {
|
|
18
|
+
expect(splitTopLevelTokens('var(--x) var(--y)')).toEqual(['var(--x)', 'var(--y)'])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('keeps nested parens intact', () => {
|
|
22
|
+
expect(splitTopLevelTokens('rgb(0 0 0) calc(1px + var(--y))')).toEqual(['rgb(0 0 0)', 'calc(1px + var(--y))'])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('handles leading and trailing whitespace', () => {
|
|
26
|
+
expect(splitTopLevelTokens(' 1px 2px ')).toEqual(['1px', '2px'])
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('splitBox', () => {
|
|
31
|
+
it('one value: applies to all sides', () => {
|
|
32
|
+
expect(splitBox('1rem')).toEqual({ top: '1rem', right: '1rem', bottom: '1rem', left: '1rem' })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('two values: block / inline', () => {
|
|
36
|
+
expect(splitBox('1rem 2rem')).toEqual({ top: '1rem', right: '2rem', bottom: '1rem', left: '2rem' })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('three values: top / horizontal / bottom', () => {
|
|
40
|
+
expect(splitBox('1px 2px 3px')).toEqual({ top: '1px', right: '2px', bottom: '3px', left: '2px' })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('four values: top / right / bottom / left', () => {
|
|
44
|
+
expect(splitBox('1px 2px 3px 4px')).toEqual({ top: '1px', right: '2px', bottom: '3px', left: '4px' })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('preserves calc() as a single token', () => {
|
|
48
|
+
expect(splitBox('calc(10px + 1rem) 2rem')).toEqual({
|
|
49
|
+
top: 'calc(10px + 1rem)',
|
|
50
|
+
right: '2rem',
|
|
51
|
+
bottom: 'calc(10px + 1rem)',
|
|
52
|
+
left: '2rem',
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('preserves var() tokens', () => {
|
|
57
|
+
expect(splitBox('var(--x) var(--y)')).toEqual({
|
|
58
|
+
top: 'var(--x)',
|
|
59
|
+
right: 'var(--y)',
|
|
60
|
+
bottom: 'var(--x)',
|
|
61
|
+
left: 'var(--y)',
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('returns undefined for unsupported token counts', () => {
|
|
66
|
+
expect(splitBox('')).toBeUndefined()
|
|
67
|
+
expect(splitBox('1px 2px 3px 4px 5px')).toBeUndefined()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('splitAxis', () => {
|
|
72
|
+
it('one value: both ends', () => {
|
|
73
|
+
expect(splitAxis('1rem')).toEqual({ start: '1rem', end: '1rem' })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('two values: start / end', () => {
|
|
77
|
+
expect(splitAxis('4px 8px')).toEqual({ start: '4px', end: '8px' })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('returns undefined for zero or > 2 tokens', () => {
|
|
81
|
+
expect(splitAxis('')).toBeUndefined()
|
|
82
|
+
expect(splitAxis('1px 2px 3px')).toBeUndefined()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('splitCorners', () => {
|
|
87
|
+
it('one value: applies to all corners', () => {
|
|
88
|
+
expect(splitCorners('4px')).toEqual({ tl: '4px', tr: '4px', br: '4px', bl: '4px' })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('two values: tl/br and tr/bl', () => {
|
|
92
|
+
expect(splitCorners('4px 8px')).toEqual({ tl: '4px', tr: '8px', br: '4px', bl: '8px' })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('three values: tl / (tr & bl) / br', () => {
|
|
96
|
+
expect(splitCorners('4px 8px 12px')).toEqual({ tl: '4px', tr: '8px', br: '12px', bl: '8px' })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('four values: tl / tr / br / bl', () => {
|
|
100
|
+
expect(splitCorners('4px 8px 12px 16px')).toEqual({ tl: '4px', tr: '8px', br: '12px', bl: '16px' })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('drops elliptical second radii after /', () => {
|
|
104
|
+
expect(splitCorners('4px 8px 12px 16px / 1px 2px 3px 4px')).toEqual({
|
|
105
|
+
tl: '4px',
|
|
106
|
+
tr: '8px',
|
|
107
|
+
br: '12px',
|
|
108
|
+
bl: '16px',
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('returns undefined for unsupported token counts', () => {
|
|
113
|
+
expect(splitCorners('')).toBeUndefined()
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('resolveLonghand', () => {
|
|
118
|
+
it('returns direct hits without consulting shorthand rules', () => {
|
|
119
|
+
const values = new Map([
|
|
120
|
+
['paddingTop', '5px'],
|
|
121
|
+
['padding', '10px'],
|
|
122
|
+
])
|
|
123
|
+
expect(resolveLonghand('paddingTop', values)).toBe('5px')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('returns undefined when target is unknown and absent', () => {
|
|
127
|
+
expect(resolveLonghand('marginTop', new Map())).toBeUndefined()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('returns undefined when no rule produces a value', () => {
|
|
131
|
+
expect(resolveLonghand('paddingTop', new Map())).toBeUndefined()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('padding family', () => {
|
|
135
|
+
it('paddingTop falls back through paddingBlockStart → paddingBlock → padding', () => {
|
|
136
|
+
expect(resolveLonghand('paddingTop', new Map([['paddingBlockStart', '1px']]))).toBe('1px')
|
|
137
|
+
expect(resolveLonghand('paddingTop', new Map([['paddingBlock', '4px 8px']]))).toBe('4px')
|
|
138
|
+
expect(resolveLonghand('paddingTop', new Map([['padding', '1rem 2rem']]))).toBe('1rem')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('paddingBottom falls back through paddingBlockEnd → paddingBlock → padding', () => {
|
|
142
|
+
expect(resolveLonghand('paddingBottom', new Map([['paddingBlockEnd', '2px']]))).toBe('2px')
|
|
143
|
+
expect(resolveLonghand('paddingBottom', new Map([['paddingBlock', '4px 8px']]))).toBe('8px')
|
|
144
|
+
expect(resolveLonghand('paddingBottom', new Map([['padding', '1rem 2rem']]))).toBe('1rem')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('paddingInlineStart falls back through paddingLeft → paddingInline → padding', () => {
|
|
148
|
+
expect(resolveLonghand('paddingInlineStart', new Map([['paddingLeft', '7px']]))).toBe('7px')
|
|
149
|
+
expect(resolveLonghand('paddingInlineStart', new Map([['paddingInline', '12px']]))).toBe('12px')
|
|
150
|
+
expect(resolveLonghand('paddingInlineStart', new Map([['paddingInline', '12px 24px']]))).toBe('12px')
|
|
151
|
+
expect(resolveLonghand('paddingInlineStart', new Map([['padding', '1px 2px 3px 4px']]))).toBe('4px')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('paddingInlineEnd falls back through paddingRight → paddingInline → padding', () => {
|
|
155
|
+
expect(resolveLonghand('paddingInlineEnd', new Map([['paddingRight', '9px']]))).toBe('9px')
|
|
156
|
+
expect(resolveLonghand('paddingInlineEnd', new Map([['paddingInline', '12px 24px']]))).toBe('24px')
|
|
157
|
+
expect(resolveLonghand('paddingInlineEnd', new Map([['padding', '1px 2px 3px 4px']]))).toBe('2px')
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('border-radius corners', () => {
|
|
162
|
+
it('maps borderTopLeftRadius to borderStartStartRadius (LTR / horizontal-tb)', () => {
|
|
163
|
+
expect(resolveLonghand('borderStartStartRadius', new Map([['borderTopLeftRadius', '5px']]))).toBe('5px')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('falls back to borderRadius corner split', () => {
|
|
167
|
+
const values = new Map([['borderRadius', '4px 8px 12px 16px']])
|
|
168
|
+
expect(resolveLonghand('borderStartStartRadius', values)).toBe('4px')
|
|
169
|
+
expect(resolveLonghand('borderStartEndRadius', values)).toBe('8px')
|
|
170
|
+
expect(resolveLonghand('borderEndEndRadius', values)).toBe('12px')
|
|
171
|
+
expect(resolveLonghand('borderEndStartRadius', values)).toBe('16px')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('border sides', () => {
|
|
176
|
+
it('borderTop falls back through borderBlockStart → border', () => {
|
|
177
|
+
expect(resolveLonghand('borderTop', new Map([['borderBlockStart', '2px dashed blue']]))).toBe('2px dashed blue')
|
|
178
|
+
expect(resolveLonghand('borderTop', new Map([['border', '1px solid red']]))).toBe('1px solid red')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('borderInlineStart falls back through borderLeft → borderInline → border', () => {
|
|
182
|
+
expect(resolveLonghand('borderInlineStart', new Map([['borderLeft', '3px dotted black']]))).toBe(
|
|
183
|
+
'3px dotted black',
|
|
184
|
+
)
|
|
185
|
+
expect(resolveLonghand('borderInlineStart', new Map([['borderInline', '1px solid grey']]))).toBe('1px solid grey')
|
|
186
|
+
expect(resolveLonghand('borderInlineStart', new Map([['border', '1px solid red']]))).toBe('1px solid red')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('background', () => {
|
|
191
|
+
it('falls back to backgroundColor when background is absent', () => {
|
|
192
|
+
expect(resolveLonghand('background', new Map([['backgroundColor', '#abcdef']]))).toBe('#abcdef')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('prefers literal background over backgroundColor', () => {
|
|
196
|
+
const values = new Map([
|
|
197
|
+
['background', '#001'],
|
|
198
|
+
['backgroundColor', '#abcdef'],
|
|
199
|
+
])
|
|
200
|
+
expect(resolveLonghand('background', values)).toBe('#001')
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
})
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { CSS_PROPERTIES } from '@wix/react-component-schema'
|
|
2
|
+
import { type Value, generate, parse } from 'css-tree'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a CSS longhand to its default value by reading the developer's
|
|
6
|
+
* parsed CSS map. Accepts whatever shorthand the developer happened to write
|
|
7
|
+
* (`padding`, `padding-block`, `padding-block-start`, …) and splits it into
|
|
8
|
+
* the longhand piece the schema asked for. Returns `undefined` when no
|
|
9
|
+
* shorthand can satisfy the target.
|
|
10
|
+
*
|
|
11
|
+
* Example. Developer wrote `padding: 10px 20px`. Schema asks for `paddingTop`.
|
|
12
|
+
* 1. Direct hit on `paddingTop` — miss.
|
|
13
|
+
* 2. Walk FALLBACKS['paddingTop'] in order:
|
|
14
|
+
* fromKey('paddingBlockStart') → miss
|
|
15
|
+
* fromAxis('paddingBlock', 'start') → miss
|
|
16
|
+
* fromBox('padding', 'top') → splitBox('10px 20px').top = '10px' ✅
|
|
17
|
+
*
|
|
18
|
+
* Writing-mode assumption: `horizontal-tb` + LTR (CSS default). The logical-
|
|
19
|
+
* to-physical equivalences this file relies on (e.g. `padding-block-start` ↔
|
|
20
|
+
* `padding-top`, `padding-left` ↔ `padding-inline-start`) only hold under
|
|
21
|
+
* that mode. RTL and vertical writing modes are out of scope.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Types
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Every property name the editor's schema knows about — the values of
|
|
30
|
+
* `CSS_PROPERTIES.CSS_PROPERTY_TYPE`. Used to type the FALLBACKS table's
|
|
31
|
+
* target keys so a typo or rename in the schema fails compilation here.
|
|
32
|
+
*
|
|
33
|
+
* Note: source-side names in factory calls (`paddingBlock`, `borderBlockStart`,
|
|
34
|
+
* etc.) stay plain strings because the developer can write any valid CSS
|
|
35
|
+
* property, including names the schema doesn't have controls for.
|
|
36
|
+
*/
|
|
37
|
+
type CssPropertyName = (typeof CSS_PROPERTIES.CSS_PROPERTY_TYPE)[keyof typeof CSS_PROPERTIES.CSS_PROPERTY_TYPE]
|
|
38
|
+
|
|
39
|
+
type BoxValues = { top: string; right: string; bottom: string; left: string }
|
|
40
|
+
type AxisValues = { start: string; end: string }
|
|
41
|
+
type CornerValues = { tl: string; tr: string; br: string; bl: string }
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Tokenizer
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Splits a CSS value on top-level whitespace, keeping function calls intact.
|
|
49
|
+
* `1px solid red` → ['1px', 'solid', 'red']
|
|
50
|
+
* `calc(1rem + 2px) 4px` → ['calc(1rem + 2px)', '4px']
|
|
51
|
+
* `rgb(0 0 0) var(--y)` → ['rgb(0 0 0)', 'var(--y)']
|
|
52
|
+
*
|
|
53
|
+
* Backed by css-tree's value AST so function calls (calc/var/rgb/…) stay as
|
|
54
|
+
* one opaque `Function` node — no manual paren tracking. Whitespace and
|
|
55
|
+
* operators are filtered out; operator-aware callers (like splitCorners,
|
|
56
|
+
* which stops at `/`) handle the AST themselves.
|
|
57
|
+
*/
|
|
58
|
+
export function splitTopLevelTokens(value: string): string[] {
|
|
59
|
+
const tokens: string[] = []
|
|
60
|
+
try {
|
|
61
|
+
const valueAst = parse(value, { context: 'value' }) as Value
|
|
62
|
+
valueAst.children.forEach((child) => {
|
|
63
|
+
if (child.type === 'WhiteSpace' || child.type === 'Operator') return
|
|
64
|
+
tokens.push(generate(child))
|
|
65
|
+
})
|
|
66
|
+
} catch {
|
|
67
|
+
// Malformed value — let callers see the empty result and fall back conservatively.
|
|
68
|
+
}
|
|
69
|
+
return tokens
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Splitters — parse a shorthand value into named parts
|
|
74
|
+
//
|
|
75
|
+
// Each splitter applies the CSS spec's 1-to-N-value expansion rule for its
|
|
76
|
+
// shorthand family. Unsupported token counts return `undefined` so callers
|
|
77
|
+
// fall back conservatively instead of guessing.
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `padding` / `margin` / `inset` — the four-sided box.
|
|
82
|
+
* 1 value: `padding: 10px` → all four sides 10px
|
|
83
|
+
* 2 values: `padding: 10px 20px` → block (top/bottom) 10px, inline (right/left) 20px
|
|
84
|
+
* 3 values: `padding: 10px 20px 30px` → top 10px, right & left 20px, bottom 30px
|
|
85
|
+
* 4 values: `padding: 10px 20px 30px 40px` → clockwise from top
|
|
86
|
+
*/
|
|
87
|
+
export function splitBox(value: string): BoxValues | undefined {
|
|
88
|
+
const tokens = splitTopLevelTokens(value)
|
|
89
|
+
switch (tokens.length) {
|
|
90
|
+
case 1:
|
|
91
|
+
return { top: tokens[0], right: tokens[0], bottom: tokens[0], left: tokens[0] }
|
|
92
|
+
case 2:
|
|
93
|
+
return { top: tokens[0], right: tokens[1], bottom: tokens[0], left: tokens[1] }
|
|
94
|
+
case 3:
|
|
95
|
+
return { top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[1] }
|
|
96
|
+
case 4:
|
|
97
|
+
return { top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[3] }
|
|
98
|
+
default:
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `padding-block`, `padding-inline`, `margin-block`, `margin-inline` — a single axis.
|
|
105
|
+
* 1 value: `padding-block: 10px` → start 10px, end 10px
|
|
106
|
+
* 2 values: `padding-block: 4px 8px` → start 4px, end 8px
|
|
107
|
+
*/
|
|
108
|
+
export function splitAxis(value: string): AxisValues | undefined {
|
|
109
|
+
const tokens = splitTopLevelTokens(value)
|
|
110
|
+
switch (tokens.length) {
|
|
111
|
+
case 1:
|
|
112
|
+
return { start: tokens[0], end: tokens[0] }
|
|
113
|
+
case 2:
|
|
114
|
+
return { start: tokens[0], end: tokens[1] }
|
|
115
|
+
default:
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `border-radius` — the four corners. Keys: tl, tr, br, bl.
|
|
122
|
+
* Anything after `/` describes elliptical second radii and is dropped (v1).
|
|
123
|
+
* 1 corner: `border-radius: 4px` → all four corners 4px
|
|
124
|
+
* 2 corners: `border-radius: 4px 8px` → tl & br 4px, tr & bl 8px
|
|
125
|
+
* 3 corners: `border-radius: 4px 8px 12px` → tl 4px, tr & bl 8px, br 12px
|
|
126
|
+
* 4 corners: `border-radius: 4px 8px 12px 16px` → clockwise from top-left
|
|
127
|
+
*/
|
|
128
|
+
export function splitCorners(value: string): CornerValues | undefined {
|
|
129
|
+
const tokens: string[] = []
|
|
130
|
+
try {
|
|
131
|
+
const valueAst = parse(value, { context: 'value' }) as Value
|
|
132
|
+
for (const child of valueAst.children) {
|
|
133
|
+
if (child.type === 'WhiteSpace') continue
|
|
134
|
+
// `/` separates the corner radii from the elliptical second radii (v1: drop them).
|
|
135
|
+
if (child.type === 'Operator' && child.value === '/') break
|
|
136
|
+
tokens.push(generate(child))
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
switch (tokens.length) {
|
|
142
|
+
case 1:
|
|
143
|
+
return { tl: tokens[0], tr: tokens[0], br: tokens[0], bl: tokens[0] }
|
|
144
|
+
case 2:
|
|
145
|
+
return { tl: tokens[0], tr: tokens[1], br: tokens[0], bl: tokens[1] }
|
|
146
|
+
case 3:
|
|
147
|
+
return { tl: tokens[0], tr: tokens[1], br: tokens[2], bl: tokens[1] }
|
|
148
|
+
case 4:
|
|
149
|
+
return { tl: tokens[0], tr: tokens[1], br: tokens[2], bl: tokens[3] }
|
|
150
|
+
default:
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
156
|
+
// Fallback factories — used to build the FALLBACKS table declaratively
|
|
157
|
+
//
|
|
158
|
+
// A Fallback is a function: given the developer's CSS map, produce a default
|
|
159
|
+
// value (or `undefined` when its source isn't present). Four flavors, each
|
|
160
|
+
// named after what it pulls out:
|
|
161
|
+
//
|
|
162
|
+
// fromKey('paddingBlockStart') → look up that key, return its value as-is
|
|
163
|
+
// fromBox('padding', 'top') → look up 'padding', split it, return the top piece
|
|
164
|
+
// fromAxis('paddingBlock', 'start') → look up 'paddingBlock', split it, return the start piece
|
|
165
|
+
// fromCorner('borderRadius', 'tl') → look up 'borderRadius', split it, return the top-left corner
|
|
166
|
+
//
|
|
167
|
+
// This is the layer that lets FALLBACKS read like a sentence per row, instead
|
|
168
|
+
// of a wall of inline arrow functions.
|
|
169
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
type Fallback = (values: Map<string, string>) => string | undefined
|
|
172
|
+
|
|
173
|
+
function fromKey(from: string): Fallback {
|
|
174
|
+
return (values) => values.get(from)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function fromBox(from: string, side: keyof BoxValues): Fallback {
|
|
178
|
+
return (values) => {
|
|
179
|
+
const value = values.get(from)
|
|
180
|
+
return value === undefined ? undefined : splitBox(value)?.[side]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function fromAxis(from: string, end: keyof AxisValues): Fallback {
|
|
185
|
+
return (values) => {
|
|
186
|
+
const value = values.get(from)
|
|
187
|
+
return value === undefined ? undefined : splitAxis(value)?.[end]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fromCorner(from: string, corner: keyof CornerValues): Fallback {
|
|
192
|
+
return (values) => {
|
|
193
|
+
const value = values.get(from)
|
|
194
|
+
return value === undefined ? undefined : splitCorners(value)?.[corner]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
// Resolution table
|
|
200
|
+
//
|
|
201
|
+
// For each schema longhand, an ordered list of fallbacks to try when the
|
|
202
|
+
// developer didn't write that longhand literally. The resolver walks them
|
|
203
|
+
// top-to-bottom and returns the first non-undefined result.
|
|
204
|
+
//
|
|
205
|
+
// Order matters: most specific source first, broadest source last — this
|
|
206
|
+
// mirrors CSS's own "more specific wins" cascade behavior.
|
|
207
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
|
|
210
|
+
// Padding — block axis is physical (top/bottom), inline axis is logical (start/end).
|
|
211
|
+
paddingTop: [fromKey('paddingBlockStart'), fromAxis('paddingBlock', 'start'), fromBox('padding', 'top')],
|
|
212
|
+
paddingBottom: [fromKey('paddingBlockEnd'), fromAxis('paddingBlock', 'end'), fromBox('padding', 'bottom')],
|
|
213
|
+
paddingInlineStart: [fromKey('paddingLeft'), fromAxis('paddingInline', 'start'), fromBox('padding', 'left')],
|
|
214
|
+
paddingInlineEnd: [fromKey('paddingRight'), fromAxis('paddingInline', 'end'), fromBox('padding', 'right')],
|
|
215
|
+
|
|
216
|
+
// Border radius — schema uses all-logical corner names; physical names map under LTR/horizontal-tb.
|
|
217
|
+
borderStartStartRadius: [fromKey('borderTopLeftRadius'), fromCorner('borderRadius', 'tl')],
|
|
218
|
+
borderStartEndRadius: [fromKey('borderTopRightRadius'), fromCorner('borderRadius', 'tr')],
|
|
219
|
+
borderEndStartRadius: [fromKey('borderBottomLeftRadius'), fromCorner('borderRadius', 'bl')],
|
|
220
|
+
borderEndEndRadius: [fromKey('borderBottomRightRadius'), fromCorner('borderRadius', 'br')],
|
|
221
|
+
|
|
222
|
+
// Border sides — these are themselves shorthands (<width> || <style> || <color>).
|
|
223
|
+
// The schema treats them as one opaque value, so `border: 1px solid red` flows
|
|
224
|
+
// through verbatim to every side.
|
|
225
|
+
borderTop: [fromKey('borderBlockStart'), fromKey('border')],
|
|
226
|
+
borderBottom: [fromKey('borderBlockEnd'), fromKey('border')],
|
|
227
|
+
borderInlineStart: [fromKey('borderLeft'), fromKey('borderInline'), fromKey('border')],
|
|
228
|
+
borderInlineEnd: [fromKey('borderRight'), fromKey('borderInline'), fromKey('border')],
|
|
229
|
+
|
|
230
|
+
// Background — populate the schema's `background` from `background-color` when
|
|
231
|
+
// the developer only wrote the color.
|
|
232
|
+
background: [fromKey('backgroundColor')],
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
236
|
+
// Public entry point
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Try the literal longhand first (covers the case where the developer wrote
|
|
241
|
+
* the exact key the schema is asking for). On a miss, walk the target's
|
|
242
|
+
* fallbacks and return the first non-undefined result. Returns `undefined`
|
|
243
|
+
* when nothing matches — the caller then emits an empty `cssProperties.<name>`
|
|
244
|
+
* entry, matching pre-resolver behavior for unknown cases.
|
|
245
|
+
*/
|
|
246
|
+
export function resolveLonghand(target: string, values: Map<string, string>): string | undefined {
|
|
247
|
+
const literal = values.get(target)
|
|
248
|
+
if (literal !== undefined) return literal
|
|
249
|
+
|
|
250
|
+
const fallbacks = FALLBACKS[target as CssPropertyName]
|
|
251
|
+
if (!fallbacks) return undefined
|
|
252
|
+
|
|
253
|
+
for (const fallback of fallbacks) {
|
|
254
|
+
const resolved = fallback(values)
|
|
255
|
+
if (resolved !== undefined) return resolved
|
|
256
|
+
}
|
|
257
|
+
return undefined
|
|
258
|
+
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ElementItem,
|
|
9
9
|
} from '@wix/react-component-schema'
|
|
10
10
|
import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
|
|
11
|
+
import { camelCase } from 'case-anything'
|
|
11
12
|
import type { ComponentInfoWithCss } from '../index'
|
|
12
13
|
import type { MatchedCssData } from '../information-extractors/css/types'
|
|
13
14
|
import type {
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
} from '../information-extractors/react'
|
|
20
21
|
import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
|
|
21
22
|
import { findPreferredSemanticClass } from '../utils/css-class'
|
|
23
|
+
import { resolveLonghand } from './css-longhand-resolver'
|
|
22
24
|
import { buildDataItem } from './data-item-builder'
|
|
23
25
|
import { formatDisplayName } from './utils'
|
|
24
26
|
|
|
@@ -305,7 +307,7 @@ function getMatchedPropertyValues(element: ExtractedElement): Map<string, string
|
|
|
305
307
|
const values = new Map<string, string>()
|
|
306
308
|
for (const match of matches) {
|
|
307
309
|
for (const prop of match.properties) {
|
|
308
|
-
values.set(prop.name, prop.value)
|
|
310
|
+
values.set(camelCase(prop.name), prop.value)
|
|
309
311
|
}
|
|
310
312
|
}
|
|
311
313
|
return values
|
|
@@ -340,7 +342,7 @@ function buildCssProperties(element: ExtractedElement | undefined): Record<strin
|
|
|
340
342
|
result[propName] = buildDisplayProperty(element)
|
|
341
343
|
continue
|
|
342
344
|
}
|
|
343
|
-
const defaultValue =
|
|
345
|
+
const defaultValue = resolveLonghand(propName, cssPropertyValues)
|
|
344
346
|
result[propName] = {
|
|
345
347
|
...(defaultValue !== undefined && { defaultValue }),
|
|
346
348
|
}
|