1
+ import { ObjectDictionary , objectValues } from '@opticss/util' ;
1
2
import * as debugGenerator from 'debug' ;
2
- import { BlockObject , Block , BlockClass , State , StyleMapping } from 'css-blocks' ;
3
+ import { Block , BlockClass , State , isBlockClass } from 'css-blocks' ;
3
4
import { Node } from 'babel-traverse' ;
4
5
import {
5
6
isCallExpression ,
@@ -10,10 +11,13 @@ import {
10
11
isBooleanLiteral ,
11
12
isIdentifier ,
12
13
isNumericLiteral ,
13
- StringLiteral
14
+ StringLiteral ,
15
+ NumericLiteral ,
16
+ BooleanLiteral ,
17
+ Expression ,
18
+ CallExpression ,
14
19
} from 'babel-types' ;
15
20
16
- import Analysis from './Analysis' ;
17
21
import { MalformedBlockPath , ErrorLocation } from '../utils/Errors' ;
18
22
19
23
const debug = debugGenerator ( 'css-blocks:jsx' ) ;
@@ -23,66 +27,101 @@ const isValidSegment = /^[a-z|A-Z|_|$][a-z|A-Z|_|$|1-9]*$/;
23
27
const PATH_START = Symbol ( 'path-start' ) ;
24
28
const PATH_END = Symbol ( 'path-end' ) ;
25
29
const CALL_START = Symbol ( 'call-start' ) ;
26
- const CALL_END = Symbol ( 'call-start ' ) ;
30
+ const CALL_END = Symbol ( 'call-end ' ) ;
27
31
28
32
export const DYNAMIC_STATE_ID = '*' ;
29
33
30
34
export type PathExpression = ( string | symbol ) [ ] ;
31
35
32
- function isLiteralPart ( part : Node ) {
33
- return isStringLiteral ( part ) || isNumericLiteral ( part ) || isBooleanLiteral ( part ) ;
36
+ function isLiteral ( node : Node ) : node is StringLiteral | NumericLiteral | BooleanLiteral {
37
+ return isStringLiteral ( node ) || isNumericLiteral ( node ) || isBooleanLiteral ( node ) ;
38
+ }
39
+
40
+ function hasLiteralArguments ( args : Array < Node > , length : number ) : boolean {
41
+ return args . length === length && args . every ( a => isLiteral ( a ) ) ;
42
+ }
43
+
44
+ export type BlockClassResult = {
45
+ block : Block ;
46
+ blockClass ?: BlockClass ;
47
+ } ;
48
+ export type BlockStateResult = BlockClassResult & {
49
+ state : State ;
50
+ } ;
51
+ export type BlockStateGroupResult = BlockClassResult & {
52
+ stateGroup : ObjectDictionary < State > ;
53
+ dynamicStateExpression : Expression ;
54
+ } ;
55
+ export type BlockExpressionResult = BlockClassResult
56
+ | BlockStateResult
57
+ | BlockStateGroupResult ;
58
+
59
+ export function isBlockStateResult ( result : BlockExpressionResult ) : result is BlockStateResult {
60
+ return ! ! ( ( < BlockStateResult > result ) . state ) ;
61
+ }
62
+ export function isBlockStateGroupResult ( result : BlockExpressionResult ) : result is BlockStateGroupResult {
63
+ return ! ! ( ( < BlockStateGroupResult > result ) . stateGroup ) ;
34
64
}
35
65
36
66
export class ExpressionReader {
37
- private expression : PathExpression ;
38
- private index = 0 ;
67
+ private pathExpression : PathExpression ;
68
+ private callExpression : CallExpression | undefined ;
39
69
40
70
isBlockExpression : boolean ;
41
71
block : string | undefined ;
42
72
class : string | undefined ;
43
73
state : string | undefined ;
44
74
subState : string | undefined ;
45
75
isDynamic : boolean ;
46
- concerns : BlockObject [ ] = [ ] ;
47
76
err : null | string = null ;
77
+ loc : ErrorLocation ;
48
78
49
- constructor ( expression : Node , analysis : Analysis | StyleMapping ) {
79
+ constructor ( expression : Node , filename : string ) {
50
80
51
81
// Expression location info object for error reporting.
52
- let loc : ErrorLocation = {
53
- filename : 'TODO' ,
82
+ this . loc = {
83
+ filename,
54
84
line : expression . loc . start . line ,
55
- column : expression . loc . start . line
85
+ column : expression . loc . start . column
56
86
} ;
57
87
58
- this . expression = getExpressionParts ( expression , loc ) ;
88
+ this . pathExpression = parsePathExpression ( expression , this . loc ) ;
59
89
60
90
// Register if this expression's sub-state is dynamic or static.
61
- if ( isCallExpression ( expression ) && expression . arguments [ 0 ] && ! isLiteralPart ( expression . arguments [ 0 ] ) ) {
62
- this . isDynamic = true ;
63
- }
64
- else {
65
- this . isDynamic = false ;
91
+ if ( isCallExpression ( expression ) ) {
92
+ this . callExpression = expression ;
93
+ this . isDynamic = ! hasLiteralArguments ( expression . arguments , 1 ) ;
94
+ if ( expression . arguments . length > 1 ) {
95
+ this . isBlockExpression = false ;
96
+ this . isDynamic = false ;
97
+ this . err = 'Only one argument can be supplied to a dynamic state' ;
98
+ return ;
99
+ }
66
100
}
67
101
68
- let len = this . expression . length ;
102
+ if ( this . pathExpression . length < 3 ) {
103
+ this . isBlockExpression = false ;
104
+ return ;
105
+ }
69
106
70
107
// Discover block expression identifiers of the form `block[.class][.state([subState])]`
71
- for ( let i = 0 ; i < len ; i ++ ) {
108
+ for ( let i = 0 ; i < this . pathExpression . length ; i ++ ) {
72
109
73
110
if ( this . err ) {
74
111
this . block = this . class = this . state = this . subState = undefined ;
75
112
break ;
76
113
}
77
114
78
- let token = this . expression [ i ] ;
79
- let next = this . expression [ i + 1 ] ;
115
+ let token = this . pathExpression [ i ] ;
116
+ let next = this . pathExpression [ i + 1 ] ;
80
117
81
118
if ( token === PATH_START && this . block ) {
119
+ // XXX This err appears to be completely swallowed?
82
120
debug ( `Discovered invalid block expression ${ this . toString ( ) } in objstr` ) ;
83
121
this . err = 'Nested expressions are not allowed in block expressions.' ;
84
122
}
85
123
else if ( token === CALL_START && ! this . state ) {
124
+ // XXX This err appears to be completely swallowed?
86
125
debug ( `Discovered invalid block expression ${ this . toString ( ) } in objstr` ) ;
87
126
this . err = 'Can not select state without a block or class.' ;
88
127
}
@@ -95,88 +134,73 @@ export class ExpressionReader {
95
134
}
96
135
}
97
136
98
- this . isBlockExpression = ! ! len && ! this . err && ! ! this . block ;
137
+ this . isBlockExpression = ! this . err && ! ! this . block ;
138
+ }
99
139
100
- // Fetch the specified block. If no block found, fail silently.
101
- if ( ! this . block ) { return ; }
102
- let blockObj : Block | BlockClass = analysis . blocks [ this . block ] ;
103
- if ( ! blockObj ) {
104
- debug ( `Discovered Block ${ this . block } from expression ${ this . toString ( ) } ` ) ;
105
- return ;
140
+ getResult ( blocks : ObjectDictionary < Block > ) : BlockExpressionResult {
141
+ if ( ! this . isBlockExpression ) {
142
+ if ( this . err ) {
143
+ throw new MalformedBlockPath ( this . err , this . loc ) ;
144
+ } else {
145
+ throw new MalformedBlockPath ( 'No block name specified.' , this . loc ) ;
146
+ }
147
+ }
148
+ let block = blocks [ this . block ! ] ;
149
+ let blockClass : BlockClass | undefined = undefined ;
150
+ if ( ! block ) {
151
+ throw new MalformedBlockPath ( `No block named ${ this . block } exists in this scope.` , this . loc ) ;
106
152
}
107
153
108
154
// Fetch the class referenced in this selector, if it exists.
109
155
if ( this . class && this . class !== 'root' ) {
110
- let classObj : BlockClass | undefined ;
111
- classObj = ( blockObj as Block ) . getClass ( this . class ) ;
112
- if ( ! classObj ) {
113
- throw new MalformedBlockPath ( `No class named "${ this . class } " found on block "${ this . block } "` , loc ) ;
156
+ blockClass = block . lookup ( `.${ this . class } ` ) as BlockClass | undefined ;
157
+ if ( ! blockClass ) {
158
+ let knownClasses = block . all ( false ) . filter ( s => isBlockClass ( s ) ) . map ( c => c . asSource ( ) ) ;
159
+ throw new MalformedBlockPath ( `No class named "${ this . class } " found on block "${ this . block } ". ` +
160
+ `Did you mean one of: ${ knownClasses . join ( ', ' ) } ` , this . loc ) ;
114
161
}
115
- blockObj = classObj ;
116
162
}
117
163
118
164
// If no state, we're done!
119
165
if ( ! this . state ) {
120
166
debug ( `Discovered BlockClass ${ this . class } from expression ${ this . toString ( ) } ` ) ;
121
- this . concerns . push ( blockObj ) ;
122
- return ;
123
- }
124
-
125
- // Throw an error if this state expects a sub-state and nothing has been provided.
126
- let states = blockObj . states . resolveGroup ( this . state ) || { } ;
127
- if ( Object . keys ( states ) . length > 1 && this . subState === undefined ) {
128
- throw new MalformedBlockPath ( `State ${ this . toString ( ) } expects a sub-state.` , loc ) ;
167
+ return { block, blockClass } ;
129
168
}
169
+ let statesContainer = ( blockClass || block ) . states ;
130
170
131
171
// Fetch all matching state objects.
132
- let stateObjects = blockObj . states . resolveGroup ( this . state , this . subState !== DYNAMIC_STATE_ID ? this . subState : undefined ) || { } ;
172
+ let stateGroup = statesContainer . resolveGroup ( this . state , this . subState !== DYNAMIC_STATE_ID ? this . subState : undefined ) || { } ;
173
+ let stateNames = Object . keys ( stateGroup ) ;
174
+ if ( stateNames . length > 1 && this . subState !== DYNAMIC_STATE_ID ) {
175
+ throw new MalformedBlockPath ( `State ${ this . toString ( ) } expects a sub-state.` , this . loc ) ;
176
+ }
133
177
134
178
// Throw a helpful error if this state / sub-state does not exist.
135
- if ( ! Object . keys ( stateObjects ) . length ) {
136
- let knownStates : State [ ] | undefined ;
137
- let allSubStates = blockObj . states . resolveGroup ( this . state ) || { } ;
138
- if ( allSubStates ) {
139
- let ass = allSubStates ;
140
- knownStates = Object . keys ( allSubStates ) . map ( k => ass [ k ] ) ;
141
- }
179
+ if ( stateNames . length === 0 ) {
180
+ let allSubStates = statesContainer . resolveGroup ( this . state ) || { } ;
181
+ let knownStates = objectValues ( allSubStates ) ;
142
182
let message = `No state [state|${ this . state } ${ this . subState ? '=' + this . subState : '' } ] found on block "${ this . block } ".` ;
143
- if ( knownStates ) {
144
- if ( knownStates . length === 1 ) {
145
- message += `\n Did you mean: ${ knownStates [ 0 ] . asSource ( ) } ?` ;
146
- } else {
147
- message += `\n Did you mean one of: ${ knownStates . map ( s => s . asSource ( ) ) . join ( ', ' ) } ?` ;
148
- }
183
+ if ( knownStates . length === 1 ) {
184
+ message += `\n Did you mean: ${ knownStates [ 0 ] . asSource ( ) } ?` ;
185
+ } else if ( knownStates . length > 0 ) {
186
+ message += `\n Did you mean one of: ${ knownStates . map ( s => s . asSource ( ) ) . join ( ', ' ) } ?` ;
149
187
}
150
- throw new MalformedBlockPath ( message , loc ) ;
188
+ throw new MalformedBlockPath ( message , this . loc ) ;
151
189
}
152
190
153
191
debug ( `Discovered ${ this . class ? 'class-level' : 'block-level' } state ${ this . state } from expression ${ this . toString ( ) } ` ) ;
154
192
155
- // Push all discovered state / sub-state objects to BlockObject concerns list.
156
- ( [ ] ) . push . apply ( this . concerns , ( < any > Object ) . values ( stateObjects ) ) ;
157
- }
158
-
159
- get length ( ) {
160
- return this . expression . length ;
161
- }
162
-
163
- next ( ) : string | undefined {
164
- let next = this . expression [ this . index ++ ] ;
165
- if ( next === PATH_START ) return this . next ( ) ;
166
- if ( next === PATH_END ) return this . next ( ) ;
167
- if ( next === CALL_START ) return this . next ( ) ;
168
- if ( next === CALL_END ) return this . next ( ) ;
169
- return < string > next ;
170
- }
171
-
172
- reset ( ) : void {
173
- this . index = 0 ;
193
+ if ( this . subState === DYNAMIC_STATE_ID ) {
194
+ return { block, blockClass, stateGroup, dynamicStateExpression : this . callExpression ! . arguments [ 0 ] } ;
195
+ } else {
196
+ return { block, blockClass, state : objectValues ( stateGroup ) [ 0 ] } ;
197
+ }
174
198
}
175
199
176
200
toString ( ) {
177
201
let out = '' ;
178
- let len = this . expression . length ;
179
- this . expression . forEach ( ( part , idx ) => {
202
+ let len = this . pathExpression . length ;
203
+ this . pathExpression . forEach ( ( part , idx ) => {
180
204
181
205
// If the first or last character, skip. These will always be path start/end symbols.
182
206
if ( idx === 0 || idx === len - 1 ) { return ; }
@@ -206,14 +230,13 @@ export class ExpressionReader {
206
230
/**
207
231
* Given a `MemberExpression`, `Identifier`, or `CallExpression`, return an array
208
232
* of all expression identifiers.
209
- * Ex: `foo.bar['baz']` => [' foo', 'bar', 'baz']
210
- * EX: `foo.bar[biz.baz].bar` => [' foo', 'bar', [' biz', 'baz'], ' bar']
233
+ * Ex: `foo.bar['baz']` => [Symbol('path-start'), ' foo', 'bar', 'baz', Symbol('path-end') ]
234
+ * EX: `foo.bar[biz.baz].bar` => [Symbol('path-start'), ' foo', 'bar', Symbol('path-start'), ' biz', 'baz', Symbol('path-end'), ' bar', Symbol('path-end ']
211
235
* Return empty array if input is invalid nested expression.
212
- * @param expression The expression in question. Yes, any. We're about to do some
213
- * very explicit type checking here.
236
+ * @param expression The expression node to be parsed
214
237
* @returns An array of strings representing the expression parts.
215
238
*/
216
- function getExpressionParts ( expression : Node , loc : ErrorLocation ) : PathExpression {
239
+ function parsePathExpression ( expression : Node , loc : ErrorLocation ) : PathExpression {
217
240
218
241
let parts : PathExpression = [ ] ;
219
242
let args : Node [ ] | undefined ;
@@ -246,7 +269,7 @@ function getExpressionParts(expression: Node, loc: ErrorLocation): PathExpressio
246
269
}
247
270
248
271
// If we encounter another member expression (Ex: foo[bar.baz])
249
- // Because Typescript has issues with recursively nested types, we use booleans
272
+ // Because Typescript has issues with recursively nested types, we use symbols
250
273
// to denote the boundaries between nested expressions.
251
274
else if ( expression . computed && (
252
275
isCallExpression ( prop ) ||
@@ -255,7 +278,7 @@ function getExpressionParts(expression: Node, loc: ErrorLocation): PathExpressio
255
278
isJSXIdentifier ( prop ) ||
256
279
isIdentifier ( prop )
257
280
) ) {
258
- parts . unshift . apply ( parts , getExpressionParts ( prop , loc ) ) ;
281
+ parts . unshift ( ... parsePathExpression ( prop , loc ) ) ;
259
282
}
260
283
261
284
else {
@@ -276,7 +299,7 @@ function getExpressionParts(expression: Node, loc: ErrorLocation): PathExpressio
276
299
if ( args ) {
277
300
parts . push ( CALL_START ) ;
278
301
args . forEach ( ( part ) => {
279
- if ( isLiteralPart ( part ) ) {
302
+ if ( isLiteral ( part ) ) {
280
303
parts . push ( String ( ( part as StringLiteral ) . value ) ) ;
281
304
}
282
305
else {
0 commit comments