9
9
} from "@css-blocks/core" ;
10
10
import { AST , print } from "@glimmer/syntax" ;
11
11
import { SourceLocation , SourcePosition } from "@opticss/element-analysis" ;
12
+ import { assertNever } from "@opticss/util" ;
12
13
import * as debugGenerator from "debug" ;
13
14
14
15
import { GlimmerAnalysis } from "./Analyzer" ;
@@ -20,15 +21,19 @@ import {
20
21
isConcatStatement ,
21
22
isElementNode ,
22
23
isMustacheStatement ,
24
+ isNullLiteral ,
25
+ isNumberLiteral ,
26
+ isPathExpression ,
23
27
isStringLiteral ,
24
28
isSubExpression ,
25
29
isTextNode ,
30
+ isUndefinedLiteral ,
26
31
} from "./utils" ;
27
32
28
33
// Expressions may be null when ElementAnalyzer is used in the second pass analysis
29
34
// to re-acquire analysis data for rewrites without storing AST nodes.
30
35
export type TernaryExpression = AST . Expression | AST . MustacheStatement | null ;
31
- export type StringExpression = AST . MustacheStatement | AST . ConcatStatement | null ;
36
+ export type StringExpression = AST . MustacheStatement | AST . ConcatStatement | AST . SubExpression | AST . PathExpression | null ;
32
37
export type BooleanExpression = AST . Expression | AST . MustacheStatement ;
33
38
export type TemplateElement = ElementAnalysis < BooleanExpression , StringExpression , TernaryExpression > ;
34
39
export type AttrRewriteMap = { [ key : string ] : TemplateElement } ;
@@ -41,7 +46,18 @@ const DEFAULT_BLOCK_NS = "block";
41
46
42
47
const debug = debugGenerator ( "css-blocks:glimmer:element-analyzer" ) ;
43
48
44
- type AnalyzableNodes = AST . ElementNode | AST . BlockStatement | AST . MustacheStatement ;
49
+ type AnalyzableNode = AST . ElementNode | AST . BlockStatement | AST . MustacheStatement | AST . SubExpression ;
50
+
51
+ export function isStyleOfHelper ( node : AnalyzableNode ) : node is AST . MustacheStatement | AST . SubExpression {
52
+ if ( ! isMustacheStatement ( node ) ) return false ;
53
+ let name = node . path . original ;
54
+ return typeof name === "string" && name === "style-of" ;
55
+ }
56
+
57
+ export function isAnalyzedHelper ( node : AnalyzableNode ) : node is AST . MustacheStatement | AST . BlockStatement {
58
+ if ( isElementNode ( node ) ) return false ;
59
+ return isEmberBuiltIn ( node . path . original ) || isStyleOfHelper ( node ) ;
60
+ }
45
61
46
62
export class ElementAnalyzer {
47
63
analysis : GlimmerAnalysis ;
@@ -58,15 +74,15 @@ export class ElementAnalyzer {
58
74
this . reservedClassNames = analysis . reservedClassNames ( ) ;
59
75
}
60
76
61
- analyze ( node : AnalyzableNodes , atRootElement : boolean ) : AttrRewriteMap {
77
+ analyze ( node : AnalyzableNode , atRootElement : boolean ) : AttrRewriteMap {
62
78
return this . _analyze ( node , atRootElement , false ) ;
63
79
}
64
80
65
- analyzeForRewrite ( node : AnalyzableNodes , atRootElement : boolean ) : AttrRewriteMap {
81
+ analyzeForRewrite ( node : AnalyzableNode , atRootElement : boolean ) : AttrRewriteMap {
66
82
return this . _analyze ( node , atRootElement , true ) ;
67
83
}
68
84
69
- private debugAnalysis ( node : AnalyzableNodes , atRootElement : boolean , element : TemplateElement ) {
85
+ private debugAnalysis ( node : AnalyzableNode , atRootElement : boolean , element : TemplateElement ) {
70
86
if ( ! debug . enabled ) return ;
71
87
let startTag = "" ;
72
88
if ( isElementNode ( node ) ) {
@@ -80,15 +96,15 @@ export class ElementAnalyzer {
80
96
debug ( `↳ Analyzed as: ${ element . forOptimizer ( this . cssBlocksOpts ) [ 0 ] . toString ( ) } ` ) ;
81
97
}
82
98
83
- private debugTemplateLocation ( node : AnalyzableNodes ) {
99
+ private debugTemplateLocation ( node : AnalyzableNode ) {
84
100
let templatePath = this . cssBlocksOpts . importer . debugIdentifier ( this . template . identifier , this . cssBlocksOpts ) ;
85
101
return charInFile ( templatePath , node . loc . start ) ;
86
102
}
87
103
private debugBlockPath ( block : Block | null = null ) {
88
104
return this . cssBlocksOpts . importer . debugIdentifier ( ( block || this . block ) . identifier , this . cssBlocksOpts ) ;
89
105
}
90
106
91
- private newElement ( node : AnalyzableNodes , forRewrite : boolean ) : TemplateElement {
107
+ private newElement ( node : AnalyzableNode , forRewrite : boolean ) : TemplateElement {
92
108
let label = isElementNode ( node ) ? node . tag : node . path . original as string ;
93
109
if ( forRewrite ) {
94
110
return new ElementAnalysis < BooleanExpression , StringExpression , TernaryExpression > ( nodeLocation ( node ) , this . reservedClassNames , label ) ;
@@ -117,8 +133,26 @@ export class ElementAnalyzer {
117
133
}
118
134
}
119
135
136
+ * eachAnalyzedAttribute ( node : AnalyzableNode ) : Iterable < [ string , string , AST . AttrNode | AST . HashPair ] > {
137
+ if ( isElementNode ( node ) ) {
138
+ for ( let attribute of node . attributes ) {
139
+ let [ namespace , attrName ] = this . isAttributeAnalyzed ( attribute . name ) ;
140
+ if ( namespace && attrName ) {
141
+ yield [ namespace , attrName , attribute ] ;
142
+ }
143
+ }
144
+ } else {
145
+ for ( let pair of node . hash . pairs ) {
146
+ let [ namespace , attrName ] = this . isAttributeAnalyzed ( pair . key ) ;
147
+ if ( namespace && attrName ) {
148
+ yield [ namespace , attrName , pair ] ;
149
+ }
150
+ }
151
+ }
152
+ }
153
+
120
154
private _analyze (
121
- node : AnalyzableNodes ,
155
+ node : AnalyzableNode ,
122
156
atRootElement : boolean ,
123
157
forRewrite : boolean ,
124
158
) : AttrRewriteMap {
@@ -131,53 +165,34 @@ export class ElementAnalyzer {
131
165
element . addStaticClass ( this . block . rootClass ) ;
132
166
}
133
167
134
- // Find the class attribute and process.
135
- if ( isElementNode ( node ) ) {
136
- for ( let attribute of node . attributes ) {
137
- let [ namespace , attrName ] = this . isAttributeAnalyzed ( attribute . name ) ;
138
- if ( namespace && attrName ) {
139
- if ( attrName === "class" ) {
140
- this . processClass ( namespace , attribute , element , forRewrite ) ;
141
- } else if ( attrName === "scope" ) {
142
- this . processScope ( namespace , attribute , element , forRewrite ) ;
143
- }
144
- }
145
- }
146
- }
147
- else {
148
- for ( let pair of node . hash . pairs ) {
149
- if ( pair . key === "class" ) {
150
- throw cssBlockError ( `The class attribute is forbidden. Did you mean block:class?` , node , this . template ) ;
151
- }
152
- let [ namespace , attrName ] = this . isAttributeAnalyzed ( pair . key ) ;
153
- if ( namespace && attrName ) {
154
- if ( attrName === "class" ) {
155
- this . processClass ( namespace , pair , element , forRewrite ) ;
156
- } else if ( attrName === "scope" ) {
157
- this . processScope ( namespace , pair , element , forRewrite ) ;
158
- }
159
- }
168
+ // Find the class or scope attribute and process it
169
+ for ( let [ namespace , attrName , attribute ] of this . eachAnalyzedAttribute ( node ) ) {
170
+ if ( attrName === "class" ) {
171
+ this . processClass ( namespace , attribute , element , forRewrite ) ;
172
+ } else if ( attrName === "scope" ) {
173
+ this . processScope ( namespace , attribute , element , forRewrite ) ;
160
174
}
161
175
}
162
176
163
- // Only ElementNodes may use states right now .
177
+ // validate that html elements aren't using the class attribute .
164
178
if ( isElementNode ( node ) ) {
165
179
for ( let attribute of node . attributes ) {
166
180
if ( attribute . name === "class" ) {
167
181
throw cssBlockError ( `The class attribute is forbidden. Did you mean block:class?` , node , this . template ) ;
168
182
}
169
- let [ namespace , attrName ] = this . isAttributeAnalyzed ( attribute . name ) ;
170
- if ( namespace && attrName ) {
171
- if ( attrName !== "class" && attrName !== "scope" ) {
172
- this . processState ( namespace , attrName , attribute , element , forRewrite ) ;
173
- }
174
- }
183
+ }
184
+ }
185
+
186
+ for ( let [ namespace , attrName , attribute ] of this . eachAnalyzedAttribute ( node ) ) {
187
+ if ( namespace && attrName ) {
188
+ if ( attrName === "class" || attrName === "scope" ) continue ;
189
+ this . processState ( namespace , attrName , attribute , element , forRewrite ) ;
175
190
}
176
191
}
177
192
178
193
this . finishElement ( element , forRewrite ) ;
179
194
180
- // If this is an Ember Build -In...
195
+ // If this is an Ember Built -In...
181
196
if ( ! isElementNode ( node ) && isEmberBuiltIn ( node . path . original ) ) {
182
197
this . debugAnalysis ( node , atRootElement , element ) ;
183
198
@@ -335,7 +350,7 @@ export class ElementAnalyzer {
335
350
private processState (
336
351
blockName : string ,
337
352
stateName : string ,
338
- node : AST . AttrNode ,
353
+ node : AST . AttrNode | AST . HashPair ,
339
354
element : TemplateElement ,
340
355
forRewrite : boolean ,
341
356
) : void {
@@ -345,17 +360,44 @@ export class ElementAnalyzer {
345
360
throw cssBlockError ( `No block or class from ${ blockName || "the default block" } is assigned to the element so a state from that block cannot be used.` , node , this . template ) ;
346
361
}
347
362
let staticSubStateName : string | undefined = undefined ;
348
- let dynamicSubState : AST . MustacheStatement | AST . ConcatStatement | undefined = undefined ;
349
- if ( isTextNode ( node . value ) ) {
350
- staticSubStateName = node . value . chars ;
363
+ let dynamicSubState : AST . MustacheStatement | AST . ConcatStatement | AST . SubExpression | AST . PathExpression | undefined = undefined ;
364
+ let value = node . value ;
365
+ if ( isTextNode ( value ) ) {
366
+ staticSubStateName = value . chars ;
367
+ if ( staticSubStateName === "" ) {
368
+ staticSubStateName = undefined ;
369
+ }
370
+ } else if ( isStringLiteral ( value ) ) {
371
+ staticSubStateName = value . value ;
351
372
if ( staticSubStateName === "" ) {
352
373
staticSubStateName = undefined ;
353
374
}
375
+ } else if ( isNumberLiteral ( value ) ) {
376
+ staticSubStateName = value . value . toString ( ) ;
377
+ if ( staticSubStateName === "" ) {
378
+ staticSubStateName = undefined ;
379
+ }
380
+ } else if ( isBooleanLiteral ( value ) ) {
381
+ if ( ! value . value ) {
382
+ // Setting the state explicitly to false is the same as not having the state on the element.
383
+ // So we just skip analysis of it. In the future we might want to partially analyze it to validate
384
+ // that the state name exists
385
+ return ;
386
+ // Setting it to true is the simplest way to set the state having no substates on an element when using the style-of helper.
387
+ }
388
+ } else if ( isMustacheStatement ( value ) || isConcatStatement ( value ) || isSubExpression ( value ) || isPathExpression ( value ) ) {
389
+ dynamicSubState = value ;
390
+ } else if ( isNullLiteral ( value ) || isUndefinedLiteral ( value ) ) {
391
+ // Setting the state explicitly to null or undefined is the same as not having the state on the element.
392
+ // So we just skip analysis of it. In the future we might want to partially analyze it to validate
393
+ // that the state name exists
394
+ return ;
354
395
} else {
355
- dynamicSubState = node . value ;
396
+ assertNever ( value ) ;
356
397
}
398
+
357
399
let found = false ;
358
- const errors : [ string , AST . AttrNode , ResolvedFile ] [ ] = [ ] ;
400
+ const errors : [ string , AST . AttrNode | AST . HashPair , ResolvedFile ] [ ] = [ ] ;
359
401
for ( let container of containers ) {
360
402
let stateGroup = container . resolveAttribute ( {
361
403
namespace : "state" ,
0 commit comments