@@ -23,11 +23,11 @@ export type BooleanExpression = AST.Expression | AST.MustacheStatement;
2323export type TemplateElement = ElementAnalysis < BooleanExpression , StringExpression , TernaryExpression > ;
2424export type AttrRewriteMap = { [ key : string ] : TemplateElement } ;
2525
26- // TODO: The state namespace should come from a config option.
27- const STATE = / ^ s t a t e : (?: ( [ ^ . ] + ) \. ) ? ( [ ^ . ] + ) $ / ;
26+ const NAMESPACED_ATTR = / ^ ( [ ^ : ] + ) : ( [ ^ : ] + ) $ / ;
2827const STYLE_IF = "style-if" ;
2928const STYLE_UNLESS = "style-unless" ;
3029const DEFAULT_BLOCK_NAME = "default" ;
30+ const DEFAULT_BLOCK_NS = "block" ;
3131
3232const debug = debugGenerator ( "css-blocks:glimmer:element-analyzer" ) ;
3333
@@ -72,8 +72,8 @@ export class ElementAnalyzer {
7272 let templatePath = this . cssBlocksOpts . importer . debugIdentifier ( this . template . identifier , this . cssBlocksOpts ) ;
7373 return charInFile ( templatePath , node . loc . start ) ;
7474 }
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 ) ;
7777 }
7878
7979 private newElement ( node : AnalyzableNodes , forRewrite : boolean ) : TemplateElement {
@@ -91,6 +91,16 @@ export class ElementAnalyzer {
9191 if ( ! forRewrite ) { this . analysis . endElement ( element ) ; }
9292 }
9393
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+
94104 private _analyze (
95105 node : AnalyzableNodes ,
96106 atRootElement : boolean ,
@@ -106,21 +116,43 @@ export class ElementAnalyzer {
106116 }
107117
108118 // 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+ }
112130 }
113-
114131 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+ }
117142 }
118143
119144 // Only ElementNodes may use states right now.
120145 if ( isElementNode ( node ) ) {
121146 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+ }
124156 }
125157 }
126158
@@ -151,35 +183,36 @@ export class ElementAnalyzer {
151183 return attrRewrites ;
152184 }
153185
154- private lookupClasses ( classes : string , node : AST . Node ) : Array < BlockClass > {
186+ private lookupClasses ( namespace : string , classes : string , node : AST . Node ) : Array < BlockClass > {
155187 let classNames = classes . trim ( ) . split ( / \s + / ) ;
156188 let found = new Array < BlockClass > ( ) ;
157189 for ( let name of classNames ) {
158- found . push ( this . lookupClass ( name , node ) ) ;
190+ found . push ( this . lookupClass ( namespace , name , node ) ) ;
159191 }
160192 return found ;
161193 }
162194
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 ) ;
167199 }
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 ) ;
176208 }
209+ return found ;
177210 }
178211
179212 /**
180213 * Adds blocks and block classes to the current node from the class attribute.
181214 */
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 {
183216 let statements : AST . Node [ ] ;
184217
185218 let value = node . value ;
@@ -193,7 +226,7 @@ export class ElementAnalyzer {
193226 for ( let statement of statements ) {
194227 if ( isTextNode ( statement ) || isStringLiteral ( statement ) ) {
195228 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 ) ) {
197230 element . addStaticClass ( container ) ;
198231 }
199232 }
@@ -210,7 +243,7 @@ export class ElementAnalyzer {
210243
211244 // Calculate the classes in the main branch of the style helper
212245 if ( isStringLiteral ( mainBranch ) ) {
213- let containers = this . lookupClasses ( mainBranch . value , mainBranch ) ;
246+ let containers = this . lookupClasses ( namespace , mainBranch . value , mainBranch ) ;
214247 if ( helperType === "style-if" ) {
215248 whenTrue = containers ;
216249 } else {
@@ -223,7 +256,7 @@ export class ElementAnalyzer {
223256 // Calculate the classes in the else branch of the style helper, if it exists.
224257 if ( elseBranch ) {
225258 if ( isStringLiteral ( elseBranch ) ) {
226- let containers = this . lookupClasses ( elseBranch . value , elseBranch ) ;
259+ let containers = this . lookupClasses ( namespace , elseBranch . value , elseBranch ) ;
227260 if ( helperType === "style-if" ) {
228261 whenFalse = containers ;
229262 } else {
@@ -247,21 +280,34 @@ export class ElementAnalyzer {
247280 }
248281 }
249282 }
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+ }
250299
251300 /**
252301 * Adds states to the current node.
253302 */
254303 private processState (
255- blockName : string | undefined ,
304+ blockName : string ,
256305 stateName : string ,
257306 node : AST . AttrNode ,
258307 element : TemplateElement ,
259308 forRewrite : boolean ,
260309 ) : 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 ) ;
265311 let containers = element . classesForBlock ( stateBlock ) ;
266312 if ( containers . length === 0 ) {
267313 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 {
322368 if ( staticSubStateName ) {
323369 errors . push ( [ `No state found named ${ stateName } with a sub-state of ${ staticSubStateName } for ${ container . asSource ( ) } in ${ blockName || "the default block" } .` , node , this . template ] ) ;
324370 } 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 ] ) ;
326372 }
327373 }
328374 }
@@ -341,6 +387,9 @@ function isConcatStatement(value: AST.Node | undefined): value is AST.ConcatStat
341387function isTextNode ( value : AST . Node | undefined ) : value is AST . TextNode {
342388 return ! ! value && value . type === "TextNode" ;
343389}
390+ function isBooleanLiteral ( value : AST . Node | undefined ) : value is AST . BooleanLiteral {
391+ return ! ! value && value . type === "BooleanLiteral" ;
392+ }
344393function isMustacheStatement ( value : AST . Node | undefined ) : value is AST . MustacheStatement {
345394 return ! ! value && value . type === "MustacheStatement" ;
346395}
0 commit comments