@cssxjs/css-to-react-native 3.2.0-0 → 3.2.0-1
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/index.js +808 -279
- package/package.json +1 -1
- package/src/__tests__/animation.js +417 -0
- package/src/__tests__/boxModel.js +6 -1
- package/src/__tests__/boxShadow.js +31 -74
- package/src/__tests__/index.js +10 -2
- package/src/__tests__/transition.js +186 -0
- package/src/__tests__/units.js +1 -4
- package/src/index.js +135 -7
- package/src/tokenTypes.js +21 -0
- package/src/transforms/animation.js +336 -0
- package/src/transforms/boxShadow.js +5 -6
- package/src/transforms/index.js +30 -0
- package/src/transforms/transition.js +238 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { SPACE, COMMA, IDENT, TIME, NUMBER, NONE } from '../tokenTypes'
|
|
2
|
+
|
|
3
|
+
// Timing function keywords
|
|
4
|
+
const timingFunctionKeywords = [
|
|
5
|
+
'ease',
|
|
6
|
+
'linear',
|
|
7
|
+
'ease-in',
|
|
8
|
+
'ease-out',
|
|
9
|
+
'ease-in-out',
|
|
10
|
+
'step-start',
|
|
11
|
+
'step-end',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
// Direction keywords
|
|
15
|
+
const directionKeywords = [
|
|
16
|
+
'normal',
|
|
17
|
+
'reverse',
|
|
18
|
+
'alternate',
|
|
19
|
+
'alternate-reverse',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
// Fill mode keywords
|
|
23
|
+
const fillModeKeywords = ['none', 'forwards', 'backwards', 'both']
|
|
24
|
+
|
|
25
|
+
// Play state keywords
|
|
26
|
+
const playStateKeywords = ['running', 'paused']
|
|
27
|
+
|
|
28
|
+
const isTimingFunction = value =>
|
|
29
|
+
timingFunctionKeywords.includes(value.toLowerCase())
|
|
30
|
+
const isDirection = value => directionKeywords.includes(value.toLowerCase())
|
|
31
|
+
const isFillMode = value => fillModeKeywords.includes(value.toLowerCase())
|
|
32
|
+
const isPlayState = value => playStateKeywords.includes(value.toLowerCase())
|
|
33
|
+
const isTime = value => /^[+-]?(?:\d*\.)?\d+(?:ms|s)$/i.test(value)
|
|
34
|
+
const isIterationCount = value =>
|
|
35
|
+
value.toLowerCase() === 'infinite' || /^[+-]?(?:\d*\.)?\d+$/.test(value)
|
|
36
|
+
|
|
37
|
+
// Helper to parse comma-separated values
|
|
38
|
+
// Returns single value if only one, array if multiple
|
|
39
|
+
const parseCommaSeparatedValues = (tokenStream, parseValue) => {
|
|
40
|
+
const values = []
|
|
41
|
+
let parsingFirst = true
|
|
42
|
+
|
|
43
|
+
while (tokenStream.hasTokens()) {
|
|
44
|
+
if (!parsingFirst) {
|
|
45
|
+
tokenStream.expect(COMMA)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Skip leading/trailing spaces
|
|
49
|
+
if (tokenStream.matches(SPACE)) {
|
|
50
|
+
// continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const value = parseValue(tokenStream)
|
|
54
|
+
values.push(value)
|
|
55
|
+
|
|
56
|
+
// Skip trailing spaces
|
|
57
|
+
if (tokenStream.matches(SPACE)) {
|
|
58
|
+
// continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
parsingFirst = false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return values.length === 1 ? values[0] : values
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Transform for animation-name property
|
|
68
|
+
export const animationName = tokenStream => {
|
|
69
|
+
const names = parseCommaSeparatedValues(tokenStream, ts =>
|
|
70
|
+
ts.expect(IDENT, NONE)
|
|
71
|
+
)
|
|
72
|
+
return { animationName: names }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Transform for animation-duration property
|
|
76
|
+
export const animationDuration = tokenStream => {
|
|
77
|
+
const durations = parseCommaSeparatedValues(tokenStream, ts =>
|
|
78
|
+
ts.expect(TIME)
|
|
79
|
+
)
|
|
80
|
+
return { animationDuration: durations }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Transform for animation-timing-function property
|
|
84
|
+
export const animationTimingFunction = tokenStream => {
|
|
85
|
+
const timingFunctions = parseCommaSeparatedValues(tokenStream, ts => {
|
|
86
|
+
// Check for function (cubic-bezier or steps)
|
|
87
|
+
const funcStream = ts.matchesFunction()
|
|
88
|
+
if (funcStream) {
|
|
89
|
+
const funcName = funcStream.functionName
|
|
90
|
+
const args = []
|
|
91
|
+
while (funcStream.hasTokens()) {
|
|
92
|
+
if (funcStream.matches(SPACE) || funcStream.matches(COMMA)) {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
const val = funcStream.expect(IDENT, TIME, NUMBER, node => {
|
|
96
|
+
if (node.type === 'word') return node.value
|
|
97
|
+
return null
|
|
98
|
+
})
|
|
99
|
+
args.push(val)
|
|
100
|
+
}
|
|
101
|
+
return `${funcName}(${args.join(', ')})`
|
|
102
|
+
}
|
|
103
|
+
return ts.expect(IDENT)
|
|
104
|
+
})
|
|
105
|
+
return { animationTimingFunction: timingFunctions }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Transform for animation-delay property
|
|
109
|
+
export const animationDelay = tokenStream => {
|
|
110
|
+
const delays = parseCommaSeparatedValues(tokenStream, ts => ts.expect(TIME))
|
|
111
|
+
return { animationDelay: delays }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Transform for animation-iteration-count property
|
|
115
|
+
export const animationIterationCount = tokenStream => {
|
|
116
|
+
const counts = parseCommaSeparatedValues(tokenStream, ts => {
|
|
117
|
+
if (ts.matches(IDENT)) {
|
|
118
|
+
const value = ts.lastValue
|
|
119
|
+
return value.toLowerCase() === 'infinite' ? 'infinite' : Number(value)
|
|
120
|
+
}
|
|
121
|
+
if (ts.matches(NUMBER)) {
|
|
122
|
+
return ts.lastValue
|
|
123
|
+
}
|
|
124
|
+
const word = ts.expect(node => {
|
|
125
|
+
if (node.type === 'word') return node.value
|
|
126
|
+
return null
|
|
127
|
+
})
|
|
128
|
+
return word.toLowerCase() === 'infinite' ? 'infinite' : Number(word)
|
|
129
|
+
})
|
|
130
|
+
return { animationIterationCount: counts }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Transform for animation-direction property
|
|
134
|
+
export const animationDirection = tokenStream => {
|
|
135
|
+
const directions = parseCommaSeparatedValues(tokenStream, ts =>
|
|
136
|
+
ts.expect(IDENT)
|
|
137
|
+
)
|
|
138
|
+
return { animationDirection: directions }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Transform for animation-fill-mode property
|
|
142
|
+
export const animationFillMode = tokenStream => {
|
|
143
|
+
const fillModes = parseCommaSeparatedValues(tokenStream, ts =>
|
|
144
|
+
ts.expect(IDENT)
|
|
145
|
+
)
|
|
146
|
+
return { animationFillMode: fillModes }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Transform for animation-play-state property
|
|
150
|
+
export const animationPlayState = tokenStream => {
|
|
151
|
+
const playStates = parseCommaSeparatedValues(tokenStream, ts =>
|
|
152
|
+
ts.expect(IDENT)
|
|
153
|
+
)
|
|
154
|
+
return { animationPlayState: playStates }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default tokenStream => {
|
|
158
|
+
// Handle 'none'
|
|
159
|
+
if (tokenStream.matches(NONE)) {
|
|
160
|
+
tokenStream.expectEmpty()
|
|
161
|
+
return {
|
|
162
|
+
animationName: 'none',
|
|
163
|
+
animationDuration: '0s',
|
|
164
|
+
animationTimingFunction: 'ease',
|
|
165
|
+
animationDelay: '0s',
|
|
166
|
+
animationIterationCount: 1,
|
|
167
|
+
animationDirection: 'normal',
|
|
168
|
+
animationFillMode: 'none',
|
|
169
|
+
animationPlayState: 'running',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const names = []
|
|
174
|
+
const durations = []
|
|
175
|
+
const timingFunctions = []
|
|
176
|
+
const delays = []
|
|
177
|
+
const iterationCounts = []
|
|
178
|
+
const directions = []
|
|
179
|
+
const fillModes = []
|
|
180
|
+
const playStates = []
|
|
181
|
+
|
|
182
|
+
let parsingFirst = true
|
|
183
|
+
|
|
184
|
+
while (tokenStream.hasTokens()) {
|
|
185
|
+
if (!parsingFirst) {
|
|
186
|
+
tokenStream.expect(COMMA)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Parse single animation
|
|
190
|
+
let name = null
|
|
191
|
+
let duration = null
|
|
192
|
+
let timingFunction = null
|
|
193
|
+
let delay = null
|
|
194
|
+
let iterationCount = null
|
|
195
|
+
let direction = null
|
|
196
|
+
let fillMode = null
|
|
197
|
+
let playState = null
|
|
198
|
+
|
|
199
|
+
// Skip leading space
|
|
200
|
+
if (tokenStream.matches(SPACE)) {
|
|
201
|
+
// continue
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Parse tokens for this animation
|
|
205
|
+
while (tokenStream.hasTokens()) {
|
|
206
|
+
// Check for comma (next animation)
|
|
207
|
+
tokenStream.saveRewindPoint()
|
|
208
|
+
if (tokenStream.matches(SPACE)) {
|
|
209
|
+
if (tokenStream.matches(COMMA)) {
|
|
210
|
+
tokenStream.rewind()
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (tokenStream.matches(COMMA)) {
|
|
215
|
+
tokenStream.rewind()
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
tokenStream.rewind()
|
|
219
|
+
|
|
220
|
+
// Skip spaces
|
|
221
|
+
if (tokenStream.matches(SPACE)) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check for timing function (cubic-bezier or steps)
|
|
226
|
+
const funcStream = tokenStream.matchesFunction()
|
|
227
|
+
if (funcStream) {
|
|
228
|
+
const funcName = funcStream.functionName
|
|
229
|
+
const args = []
|
|
230
|
+
while (funcStream.hasTokens()) {
|
|
231
|
+
if (funcStream.matches(SPACE) || funcStream.matches(COMMA)) {
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
const val = funcStream.expect(IDENT, TIME, NUMBER, node => {
|
|
235
|
+
if (node.type === 'word') return node.value
|
|
236
|
+
return null
|
|
237
|
+
})
|
|
238
|
+
args.push(val)
|
|
239
|
+
}
|
|
240
|
+
timingFunction = `${funcName}(${args.join(', ')})`
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Match number (iteration count)
|
|
245
|
+
if (tokenStream.matches(NUMBER)) {
|
|
246
|
+
iterationCount = tokenStream.lastValue
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Match time
|
|
251
|
+
if (tokenStream.matches(TIME)) {
|
|
252
|
+
const value = tokenStream.lastValue
|
|
253
|
+
if (duration === null) {
|
|
254
|
+
duration = value
|
|
255
|
+
} else {
|
|
256
|
+
delay = value
|
|
257
|
+
}
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Match identifier
|
|
262
|
+
if (tokenStream.matches(IDENT)) {
|
|
263
|
+
const value = tokenStream.lastValue
|
|
264
|
+
if (isTimingFunction(value)) {
|
|
265
|
+
timingFunction = value
|
|
266
|
+
} else if (isDirection(value)) {
|
|
267
|
+
direction = value
|
|
268
|
+
} else if (isFillMode(value)) {
|
|
269
|
+
fillMode = value
|
|
270
|
+
} else if (isPlayState(value)) {
|
|
271
|
+
playState = value
|
|
272
|
+
} else if (value.toLowerCase() === 'infinite') {
|
|
273
|
+
iterationCount = 'infinite'
|
|
274
|
+
} else {
|
|
275
|
+
// It's the animation name
|
|
276
|
+
name = value
|
|
277
|
+
}
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Try to match as generic word
|
|
282
|
+
const wordMatch = tokenStream.expect(node => {
|
|
283
|
+
if (node.type === 'word') return node.value
|
|
284
|
+
return null
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
if (isTime(wordMatch)) {
|
|
288
|
+
if (duration === null) {
|
|
289
|
+
duration = wordMatch
|
|
290
|
+
} else {
|
|
291
|
+
delay = wordMatch
|
|
292
|
+
}
|
|
293
|
+
} else if (isTimingFunction(wordMatch)) {
|
|
294
|
+
timingFunction = wordMatch
|
|
295
|
+
} else if (isDirection(wordMatch)) {
|
|
296
|
+
direction = wordMatch
|
|
297
|
+
} else if (isFillMode(wordMatch)) {
|
|
298
|
+
fillMode = wordMatch
|
|
299
|
+
} else if (isPlayState(wordMatch)) {
|
|
300
|
+
playState = wordMatch
|
|
301
|
+
} else if (isIterationCount(wordMatch)) {
|
|
302
|
+
iterationCount =
|
|
303
|
+
wordMatch.toLowerCase() === 'infinite'
|
|
304
|
+
? 'infinite'
|
|
305
|
+
: Number(wordMatch)
|
|
306
|
+
} else {
|
|
307
|
+
name = wordMatch
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Apply defaults and push
|
|
312
|
+
names.push(name || 'none')
|
|
313
|
+
durations.push(duration || '0s')
|
|
314
|
+
timingFunctions.push(timingFunction || 'ease')
|
|
315
|
+
delays.push(delay || '0s')
|
|
316
|
+
iterationCounts.push(iterationCount !== null ? iterationCount : 1)
|
|
317
|
+
directions.push(direction || 'normal')
|
|
318
|
+
fillModes.push(fillMode || 'none')
|
|
319
|
+
playStates.push(playState || 'running')
|
|
320
|
+
|
|
321
|
+
parsingFirst = false
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Return single values if only one animation, arrays if multiple
|
|
325
|
+
const isSingle = names.length === 1
|
|
326
|
+
return {
|
|
327
|
+
animationName: isSingle ? names[0] : names,
|
|
328
|
+
animationDuration: isSingle ? durations[0] : durations,
|
|
329
|
+
animationTimingFunction: isSingle ? timingFunctions[0] : timingFunctions,
|
|
330
|
+
animationDelay: isSingle ? delays[0] : delays,
|
|
331
|
+
animationIterationCount: isSingle ? iterationCounts[0] : iterationCounts,
|
|
332
|
+
animationDirection: isSingle ? directions[0] : directions,
|
|
333
|
+
animationFillMode: isSingle ? fillModes[0] : fillModes,
|
|
334
|
+
animationPlayState: isSingle ? playStates[0] : playStates,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stringify } from 'postcss-value-parser'
|
|
2
2
|
|
|
3
3
|
export default tokenStream => {
|
|
4
|
-
|
|
4
|
+
// React Native now supports web-style box-shadow format directly
|
|
5
|
+
// Pass through the original CSS value as boxShadow
|
|
6
|
+
const value = stringify(tokenStream.nodes)
|
|
5
7
|
return {
|
|
6
|
-
|
|
7
|
-
shadowRadius: radius,
|
|
8
|
-
shadowColor: color,
|
|
9
|
-
shadowOpacity: 1,
|
|
8
|
+
boxShadow: value,
|
|
10
9
|
}
|
|
11
10
|
}
|
package/src/transforms/index.js
CHANGED
|
@@ -7,6 +7,16 @@ import {
|
|
|
7
7
|
WORD,
|
|
8
8
|
VARIABLE,
|
|
9
9
|
} from '../tokenTypes'
|
|
10
|
+
import animation, {
|
|
11
|
+
animationName,
|
|
12
|
+
animationDuration,
|
|
13
|
+
animationTimingFunction,
|
|
14
|
+
animationDelay,
|
|
15
|
+
animationIterationCount,
|
|
16
|
+
animationDirection,
|
|
17
|
+
animationFillMode,
|
|
18
|
+
animationPlayState,
|
|
19
|
+
} from './animation'
|
|
10
20
|
import aspectRatio from './aspectRatio'
|
|
11
21
|
import border from './border'
|
|
12
22
|
import boxShadow from './boxShadow'
|
|
@@ -20,6 +30,12 @@ import textDecoration from './textDecoration'
|
|
|
20
30
|
import textDecorationLine from './textDecorationLine'
|
|
21
31
|
import textShadow from './textShadow'
|
|
22
32
|
import transform from './transform'
|
|
33
|
+
import transition, {
|
|
34
|
+
transitionProperty,
|
|
35
|
+
transitionDuration,
|
|
36
|
+
transitionTimingFunction,
|
|
37
|
+
transitionDelay,
|
|
38
|
+
} from './transition'
|
|
23
39
|
import { directionFactory, parseShadowOffset } from './util'
|
|
24
40
|
|
|
25
41
|
const background = tokenStream => ({
|
|
@@ -53,6 +69,15 @@ const textShadowOffset = tokenStream => ({
|
|
|
53
69
|
})
|
|
54
70
|
|
|
55
71
|
export default {
|
|
72
|
+
animation,
|
|
73
|
+
animationName,
|
|
74
|
+
animationDuration,
|
|
75
|
+
animationTimingFunction,
|
|
76
|
+
animationDelay,
|
|
77
|
+
animationIterationCount,
|
|
78
|
+
animationDirection,
|
|
79
|
+
animationFillMode,
|
|
80
|
+
animationPlayState,
|
|
56
81
|
aspectRatio,
|
|
57
82
|
background,
|
|
58
83
|
border,
|
|
@@ -75,4 +100,9 @@ export default {
|
|
|
75
100
|
textDecoration,
|
|
76
101
|
textDecorationLine,
|
|
77
102
|
transform,
|
|
103
|
+
transition,
|
|
104
|
+
transitionProperty,
|
|
105
|
+
transitionDuration,
|
|
106
|
+
transitionTimingFunction,
|
|
107
|
+
transitionDelay,
|
|
78
108
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import camelizeStyleName from 'camelize'
|
|
2
|
+
import { SPACE, COMMA, IDENT, TIME, NONE } from '../tokenTypes'
|
|
3
|
+
|
|
4
|
+
// Timing function keywords
|
|
5
|
+
const timingFunctionKeywords = [
|
|
6
|
+
'ease',
|
|
7
|
+
'linear',
|
|
8
|
+
'ease-in',
|
|
9
|
+
'ease-out',
|
|
10
|
+
'ease-in-out',
|
|
11
|
+
'step-start',
|
|
12
|
+
'step-end',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
const isTimingFunction = value => {
|
|
16
|
+
if (timingFunctionKeywords.includes(value.toLowerCase())) {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
// cubic-bezier and steps are handled as functions
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isTime = value => /^[+-]?(?:\d*\.)?\d+(?:ms|s)$/i.test(value)
|
|
24
|
+
|
|
25
|
+
// Helper to parse comma-separated values
|
|
26
|
+
// Returns single value if only one, array if multiple
|
|
27
|
+
const parseCommaSeparatedValues = (tokenStream, parseValue) => {
|
|
28
|
+
const values = []
|
|
29
|
+
let parsingFirst = true
|
|
30
|
+
|
|
31
|
+
while (tokenStream.hasTokens()) {
|
|
32
|
+
if (!parsingFirst) {
|
|
33
|
+
tokenStream.expect(COMMA)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Skip leading/trailing spaces
|
|
37
|
+
if (tokenStream.matches(SPACE)) {
|
|
38
|
+
// continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const value = parseValue(tokenStream)
|
|
42
|
+
values.push(value)
|
|
43
|
+
|
|
44
|
+
// Skip trailing spaces
|
|
45
|
+
if (tokenStream.matches(SPACE)) {
|
|
46
|
+
// continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
parsingFirst = false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return values.length === 1 ? values[0] : values
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Transform for transition-property
|
|
56
|
+
export const transitionProperty = tokenStream => {
|
|
57
|
+
const properties = parseCommaSeparatedValues(tokenStream, ts => {
|
|
58
|
+
const prop = ts.expect(IDENT, NONE)
|
|
59
|
+
// Don't camelize special values like 'all' and 'none'
|
|
60
|
+
return prop === 'all' || prop === 'none' ? prop : camelizeStyleName(prop)
|
|
61
|
+
})
|
|
62
|
+
return { transitionProperty: properties }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Transform for transition-duration
|
|
66
|
+
export const transitionDuration = tokenStream => {
|
|
67
|
+
const durations = parseCommaSeparatedValues(tokenStream, ts =>
|
|
68
|
+
ts.expect(TIME)
|
|
69
|
+
)
|
|
70
|
+
return { transitionDuration: durations }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Transform for transition-timing-function
|
|
74
|
+
export const transitionTimingFunction = tokenStream => {
|
|
75
|
+
const timingFunctions = parseCommaSeparatedValues(tokenStream, ts => {
|
|
76
|
+
// Check for function (cubic-bezier or steps)
|
|
77
|
+
const funcStream = ts.matchesFunction()
|
|
78
|
+
if (funcStream) {
|
|
79
|
+
const funcName = funcStream.functionName
|
|
80
|
+
const args = []
|
|
81
|
+
while (funcStream.hasTokens()) {
|
|
82
|
+
if (funcStream.matches(SPACE) || funcStream.matches(COMMA)) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
const val = funcStream.expect(IDENT, TIME, node => {
|
|
86
|
+
if (node.type === 'word') return node.value
|
|
87
|
+
return null
|
|
88
|
+
})
|
|
89
|
+
args.push(val)
|
|
90
|
+
}
|
|
91
|
+
return `${funcName}(${args.join(', ')})`
|
|
92
|
+
}
|
|
93
|
+
return ts.expect(IDENT)
|
|
94
|
+
})
|
|
95
|
+
return { transitionTimingFunction: timingFunctions }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Transform for transition-delay
|
|
99
|
+
export const transitionDelay = tokenStream => {
|
|
100
|
+
const delays = parseCommaSeparatedValues(tokenStream, ts => ts.expect(TIME))
|
|
101
|
+
return { transitionDelay: delays }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default tokenStream => {
|
|
105
|
+
// Handle 'none'
|
|
106
|
+
if (tokenStream.matches(NONE)) {
|
|
107
|
+
tokenStream.expectEmpty()
|
|
108
|
+
return {
|
|
109
|
+
transitionProperty: 'none',
|
|
110
|
+
transitionDuration: '0s',
|
|
111
|
+
transitionTimingFunction: 'ease',
|
|
112
|
+
transitionDelay: '0s',
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const properties = []
|
|
117
|
+
const durations = []
|
|
118
|
+
const timingFunctions = []
|
|
119
|
+
const delays = []
|
|
120
|
+
|
|
121
|
+
let parsingFirst = true
|
|
122
|
+
|
|
123
|
+
while (tokenStream.hasTokens()) {
|
|
124
|
+
if (!parsingFirst) {
|
|
125
|
+
tokenStream.expect(COMMA)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parse single transition: property duration timing-function delay
|
|
129
|
+
// Order can vary, but typically: property duration [timing-function] [delay]
|
|
130
|
+
let property = null
|
|
131
|
+
let duration = null
|
|
132
|
+
let timingFunction = null
|
|
133
|
+
let delay = null
|
|
134
|
+
|
|
135
|
+
// Skip leading space
|
|
136
|
+
if (tokenStream.matches(SPACE)) {
|
|
137
|
+
// continue
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Parse tokens for this transition
|
|
141
|
+
while (tokenStream.hasTokens()) {
|
|
142
|
+
// Check for comma (next transition)
|
|
143
|
+
tokenStream.saveRewindPoint()
|
|
144
|
+
if (tokenStream.matches(SPACE)) {
|
|
145
|
+
if (tokenStream.matches(COMMA)) {
|
|
146
|
+
tokenStream.rewind()
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (tokenStream.matches(COMMA)) {
|
|
151
|
+
tokenStream.rewind()
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
tokenStream.rewind()
|
|
155
|
+
|
|
156
|
+
// Try to match different token types
|
|
157
|
+
if (tokenStream.matches(SPACE)) {
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check for timing function (cubic-bezier or steps)
|
|
162
|
+
const funcStream = tokenStream.matchesFunction()
|
|
163
|
+
if (funcStream) {
|
|
164
|
+
// It's a function like cubic-bezier() or steps()
|
|
165
|
+
const funcName = funcStream.functionName
|
|
166
|
+
const args = []
|
|
167
|
+
while (funcStream.hasTokens()) {
|
|
168
|
+
if (funcStream.matches(SPACE) || funcStream.matches(COMMA)) {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
const val = funcStream.expect(IDENT, TIME, node => {
|
|
172
|
+
if (node.type === 'word') return node.value
|
|
173
|
+
return null
|
|
174
|
+
})
|
|
175
|
+
args.push(val)
|
|
176
|
+
}
|
|
177
|
+
timingFunction = `${funcName}(${args.join(', ')})`
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Match word or time
|
|
182
|
+
if (tokenStream.matches(IDENT)) {
|
|
183
|
+
const value = tokenStream.lastValue
|
|
184
|
+
if (isTimingFunction(value)) {
|
|
185
|
+
timingFunction = value
|
|
186
|
+
} else {
|
|
187
|
+
property = value
|
|
188
|
+
}
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (tokenStream.matches(TIME)) {
|
|
193
|
+
const value = tokenStream.lastValue
|
|
194
|
+
if (duration === null) {
|
|
195
|
+
duration = value
|
|
196
|
+
} else {
|
|
197
|
+
delay = value
|
|
198
|
+
}
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Try to match as word for property names like 'all', 'opacity', etc.
|
|
203
|
+
const wordMatch = tokenStream.expect(node => {
|
|
204
|
+
if (node.type === 'word') return node.value
|
|
205
|
+
return null
|
|
206
|
+
})
|
|
207
|
+
if (isTime(wordMatch)) {
|
|
208
|
+
if (duration === null) {
|
|
209
|
+
duration = wordMatch
|
|
210
|
+
} else {
|
|
211
|
+
delay = wordMatch
|
|
212
|
+
}
|
|
213
|
+
} else if (isTimingFunction(wordMatch)) {
|
|
214
|
+
timingFunction = wordMatch
|
|
215
|
+
} else {
|
|
216
|
+
property = wordMatch
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Apply defaults and camelize property name
|
|
221
|
+
const propName = property || 'all'
|
|
222
|
+
properties.push(propName === 'all' ? 'all' : camelizeStyleName(propName))
|
|
223
|
+
durations.push(duration || '0s')
|
|
224
|
+
timingFunctions.push(timingFunction || 'ease')
|
|
225
|
+
delays.push(delay || '0s')
|
|
226
|
+
|
|
227
|
+
parsingFirst = false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Return single values if only one transition, arrays if multiple
|
|
231
|
+
const isSingle = properties.length === 1
|
|
232
|
+
return {
|
|
233
|
+
transitionProperty: isSingle ? properties[0] : properties,
|
|
234
|
+
transitionDuration: isSingle ? durations[0] : durations,
|
|
235
|
+
transitionTimingFunction: isSingle ? timingFunctions[0] : timingFunctions,
|
|
236
|
+
transitionDelay: isSingle ? delays[0] : delays,
|
|
237
|
+
}
|
|
238
|
+
}
|