@@ -23,11 +23,11 @@ export type BooleanExpression = AST.Expression | AST.MustacheStatement;
23
23
export type TemplateElement = ElementAnalysis < BooleanExpression , StringExpression , TernaryExpression > ;
24
24
export type AttrRewriteMap = { [ key : string ] : TemplateElement } ;
25
25
26
- // TODO: The state namespace should come from a config option.
27
- const STATE = / ^ s t a t e : (?: ( [ ^ . ] + ) \. ) ? ( [ ^ . ] + ) $ / ;
26
+ const NAMESPACED_ATTR = / ^ ( [ ^ : ] + ) : ( [ ^ : ] + ) $ / ;
28
27
const STYLE_IF = "style-if" ;
29
28
const STYLE_UNLESS = "style-unless" ;
30
29
const DEFAULT_BLOCK_NAME = "default" ;
30
+ const DEFAULT_BLOCK_NS = "block" ;
31
31
32
32
const debug = debugGenerator ( "css-blocks:glimmer:element-analyzer" ) ;
33
33
@@ -72,8 +72,8 @@ export class ElementAnalyzer {
72
72
let templatePath = this . cssBlocksOpts . importer . debugIdentifier ( this . template . identifier , this . cssBlocksOpts ) ;
73
73
return charInFile ( templatePath , node . loc . start ) ;
74
74
}
75
- private debugBlockPath ( ) {
76
- return this . cssBlocksOpts . importer . debugIdentifier ( this . block . identifier , this . cssBlocksOpts ) ;
75
+ private debugBlockPath ( block : Block | null = null ) {
76
+ return this . cssBlocksOpts . importer . debugIdentifier ( ( block || this . block ) . identifier , this . cssBlocksOpts ) ;
77
77
}
78
78
79
79
private newElement ( node : AnalyzableNodes , forRewrite : boolean ) : TemplateElement {
@@ -91,6 +91,16 @@ export class ElementAnalyzer {
91
91
if ( ! forRewrite ) { this . analysis . endElement ( element ) ; }
92
92
}
93
93
94
+ isAttributeAnalyzed ( attributeName : string ) : [ string , string ] | [ null , null ] {
95
+ if ( NAMESPACED_ATTR . test ( attributeName ) ) {
96
+ let namespace = RegExp . $1 ;
97
+ let attrName = RegExp . $2 ;
98
+ return [ namespace , attrName ] ;
99
+ } else {
100
+ return [ null , null ] ;
101
+ }
102
+ }
103
+
94
104
private _analyze (
95
105
node : AnalyzableNodes ,
96
106
atRootElement : boolean ,
@@ -106,21 +116,43 @@ export class ElementAnalyzer {
106
116
}
107
117
108
118
// Find the class attribute and process.
109
- if ( node . type === "ElementNode" ) {
110
- let classAttr : AST . AttrNode | undefined = node . attributes . find ( n => n . name === "class" ) ;
111
- if ( classAttr ) { this . processClass ( classAttr , element , forRewrite ) ; }
119
+ if ( isElementNode ( node ) ) {
120
+ for ( let attribute of node . attributes ) {
121
+ let [ namespace , attrName ] = this . isAttributeAnalyzed ( attribute . name ) ;
122
+ if ( namespace && attrName ) {
123
+ if ( attrName === "class" ) {
124
+ this . processClass ( namespace , attribute , element , forRewrite ) ;
125
+ } else if ( attrName === "scope" ) {
126
+ this . processScope ( namespace , attribute , element , forRewrite ) ;
127
+ }
128
+ }
129
+ }
112
130
}
113
-
114
131
else {
115
- let classAttr : AST . HashPair | undefined = node . hash . pairs . find ( n => n . key === "class" ) ;
116
- if ( classAttr ) { this . processClass ( classAttr , element , forRewrite ) ; }
132
+ for ( let pair of node . hash . pairs ) {
133
+ let [ namespace , attrName ] = this . isAttributeAnalyzed ( pair . key ) ;
134
+ if ( namespace && attrName ) {
135
+ if ( attrName === "class" ) {
136
+ this . processClass ( namespace , pair , element , forRewrite ) ;
137
+ } else if ( attrName === "scope" ) {
138
+ this . processScope ( namespace , pair , element , forRewrite ) ;
139
+ }
140
+ }
141
+ }
117
142
}
118
143
119
144
// Only ElementNodes may use states right now.
120
145
if ( isElementNode ( node ) ) {
121
146
for ( let attribute of node . attributes ) {
122
- if ( ! STATE . test ( attribute . name ) ) { continue ; }
123
- this . processState ( RegExp . $1 , RegExp . $2 , attribute , element , forRewrite ) ;
147
+ if ( attribute . name === "class" ) {
148
+ throw cssBlockError ( `The class attribute is forbidden. Did you mean block:class?` , node , this . template ) ;
149
+ }
150
+ let [ namespace , attrName ] = this . isAttributeAnalyzed ( attribute . name ) ;
151
+ if ( namespace && attrName ) {
152
+ if ( attrName !== "class" && attrName !== "scope" ) {
153
+ this . processState ( namespace , attrName , attribute , element , forRewrite ) ;
154
+ }
155
+ }
124
156
}
125
157
}
126
158
@@ -151,35 +183,36 @@ export class ElementAnalyzer {
151
183
return attrRewrites ;
152
184
}
153
185
154
- private lookupClasses ( classes : string , node : AST . Node ) : Array < BlockClass > {
186
+ private lookupClasses ( namespace : string , classes : string , node : AST . Node ) : Array < BlockClass > {
155
187
let classNames = classes . trim ( ) . split ( / \s + / ) ;
156
188
let found = new Array < BlockClass > ( ) ;
157
189
for ( let name of classNames ) {
158
- found . push ( this . lookupClass ( name , node ) ) ;
190
+ found . push ( this . lookupClass ( namespace , name , node ) ) ;
159
191
}
160
192
return found ;
161
193
}
162
194
163
- private lookupClass ( name : string , node : AST . Node ) : BlockClass {
164
- let found = this . block . externalLookup ( name ) ;
165
- if ( ! found && ! / \. / . test ( name ) ) {
166
- found = this . block . externalLookup ( "." + name ) ;
195
+ private lookupBlock ( namespace : string , node : AST . Node ) : Block {
196
+ let block = ( namespace === DEFAULT_BLOCK_NS ) ? this . block : this . block . getExportedBlock ( namespace ) ;
197
+ if ( block === null ) {
198
+ throw cssBlockError ( `No block ' ${ namespace } ' is exported from ${ this . debugBlockPath ( ) } ` , node , this . template ) ;
167
199
}
168
- if ( found ) {
169
- return < BlockClass > found ;
170
- } else {
171
- if ( / \. / . test ( name ) ) {
172
- throw cssBlockError ( `No class or block named ${ name } is referenced from ${ this . debugBlockPath ( ) } ` , node , this . template ) ;
173
- } else {
174
- throw cssBlockError ( `No class or block named ${ name } ` , node , this . template ) ;
175
- }
200
+ return block ;
201
+ }
202
+
203
+ private lookupClass ( namespace : string , name : string , node : AST . Node ) : BlockClass {
204
+ let block = this . lookupBlock ( namespace , node ) ;
205
+ let found = block . resolveClass ( name ) ;
206
+ if ( found === null ) {
207
+ throw cssBlockError ( `No class ' ${ name } ' was found in block at ${ this . debugBlockPath ( block ) } ` , node , this . template ) ;
176
208
}
209
+ return found ;
177
210
}
178
211
179
212
/**
180
213
* Adds blocks and block classes to the current node from the class attribute.
181
214
*/
182
- private processClass ( node : AST . AttrNode | AST . HashPair , element : TemplateElement , forRewrite : boolean ) : void {
215
+ private processClass ( namespace : string , node : AST . AttrNode | AST . HashPair , element : TemplateElement , forRewrite : boolean ) : void {
183
216
let statements : AST . Node [ ] ;
184
217
185
218
let value = node . value ;
@@ -193,7 +226,7 @@ export class ElementAnalyzer {
193
226
for ( let statement of statements ) {
194
227
if ( isTextNode ( statement ) || isStringLiteral ( statement ) ) {
195
228
let value = isTextNode ( statement ) ? statement . chars : statement . value ;
196
- for ( let container of this . lookupClasses ( value , statement ) ) {
229
+ for ( let container of this . lookupClasses ( namespace , value , statement ) ) {
197
230
element . addStaticClass ( container ) ;
198
231
}
199
232
}
@@ -210,7 +243,7 @@ export class ElementAnalyzer {
210
243
211
244
// Calculate the classes in the main branch of the style helper
212
245
if ( isStringLiteral ( mainBranch ) ) {
213
- let containers = this . lookupClasses ( mainBranch . value , mainBranch ) ;
246
+ let containers = this . lookupClasses ( namespace , mainBranch . value , mainBranch ) ;
214
247
if ( helperType === "style-if" ) {
215
248
whenTrue = containers ;
216
249
} else {
@@ -223,7 +256,7 @@ export class ElementAnalyzer {
223
256
// Calculate the classes in the else branch of the style helper, if it exists.
224
257
if ( elseBranch ) {
225
258
if ( isStringLiteral ( elseBranch ) ) {
226
- let containers = this . lookupClasses ( elseBranch . value , elseBranch ) ;
259
+ let containers = this . lookupClasses ( namespace , elseBranch . value , elseBranch ) ;
227
260
if ( helperType === "style-if" ) {
228
261
whenFalse = containers ;
229
262
} else {
@@ -247,21 +280,34 @@ export class ElementAnalyzer {
247
280
}
248
281
}
249
282
}
283
+ private processScope ( namespace : string , node : AST . AttrNode | AST . HashPair , element : TemplateElement , _forRewrite : boolean ) : void {
284
+ let value = node . value ;
285
+ let block = this . lookupBlock ( namespace , node ) ;
286
+
287
+ if ( isTextNode ( value ) ) {
288
+ if ( value . chars === "" ) {
289
+ element . addStaticClass ( block . rootClass ) ;
290
+ } else {
291
+ throw cssBlockError ( "String literal values are not allowed for the scope attribute" , node , this . template ) ;
292
+ }
293
+ } else if ( isBooleanLiteral ( value ) ) {
294
+ if ( value . value ) {
295
+ element . addStaticClass ( block . rootClass ) ;
296
+ }
297
+ }
298
+ }
250
299
251
300
/**
252
301
* Adds states to the current node.
253
302
*/
254
303
private processState (
255
- blockName : string | undefined ,
304
+ blockName : string ,
256
305
stateName : string ,
257
306
node : AST . AttrNode ,
258
307
element : TemplateElement ,
259
308
forRewrite : boolean ,
260
309
) : void {
261
- let stateBlock = blockName ? this . block . getExportedBlock ( blockName ) : this . block ;
262
- if ( stateBlock === null ) {
263
- throw cssBlockError ( `No block named ${ blockName } referenced from ${ this . debugBlockPath ( ) } ` , node , this . template ) ;
264
- }
310
+ let stateBlock = this . lookupBlock ( blockName , node ) ;
265
311
let containers = element . classesForBlock ( stateBlock ) ;
266
312
if ( containers . length === 0 ) {
267
313
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 ) ;
@@ -322,7 +368,7 @@ export class ElementAnalyzer {
322
368
if ( staticSubStateName ) {
323
369
errors . push ( [ `No state found named ${ stateName } with a sub-state of ${ staticSubStateName } for ${ container . asSource ( ) } in ${ blockName || "the default block" } .` , node , this . template ] ) ;
324
370
} else {
325
- errors . push ( [ `No state(s) found named ${ stateName } for ${ container . asSource ( ) } in ${ blockName || " the default block"} .` , node , this . template ] ) ;
371
+ errors . push ( [ `No state(s) found named ${ stateName } for ${ container . asSource ( ) } in ${ blockName === "block" && " the default block" || blockName } .` , node , this . template ] ) ;
326
372
}
327
373
}
328
374
}
@@ -341,6 +387,9 @@ function isConcatStatement(value: AST.Node | undefined): value is AST.ConcatStat
341
387
function isTextNode ( value : AST . Node | undefined ) : value is AST . TextNode {
342
388
return ! ! value && value . type === "TextNode" ;
343
389
}
390
+ function isBooleanLiteral ( value : AST . Node | undefined ) : value is AST . BooleanLiteral {
391
+ return ! ! value && value . type === "BooleanLiteral" ;
392
+ }
344
393
function isMustacheStatement ( value : AST . Node | undefined ) : value is AST . MustacheStatement {
345
394
return ! ! value && value . type === "MustacheStatement" ;
346
395
}
0 commit comments