import { Block , Options as CSSBlocksOptions , ResolvedConfiguration as CSSBlocksConfiguration , StyleMapping , resolveConfiguration , } from "@css-blocks/core" ; import { DEFAULT_EXPORT } from "@css-blocks/core/dist/src/BlockSyntax" ; import type { AST , ASTPlugin , NodeVisitor , Syntax , } from "@glimmer/syntax" ; import { TemplateTypes } from "@opticss/template-api" ; import * as debugGenerator from "debug" ; import { GlimmerAnalysis } from "./Analyzer" ; import { classnamesHelper , classnamesSubexpr } from "./ClassnamesHelperGenerator" ; import { ElementAnalyzer , TemplateElement , isStyleOfHelper } from "./ElementAnalyzer" ; import { getEmberBuiltInStates , isEmberBuiltIn , isEmberBuiltInNode } from "./EmberBuiltins" ; import { CONCAT_HELPER_NAME } from "./helpers" ; import { TEMPLATE_TYPE } from "./Template" ; import { cssBlockError , isTextNode , pathString } from "./utils" ; const DEBUG = debugGenerator ( "css-blocks:glimmer:rewriter" ) ; export type GlimmerStyleMapping = StyleMapping < TEMPLATE_TYPE > ; interface ASTPluginWithDeps extends ASTPlugin { /**
     * If this method exists, it is called with the relative path to the current
     * file just before processing starts. Use this method to reset the
     * dependency tracking state associated with the file.
     */ resetDependencies ? ( relativePath ) : void ; param relativePath return dependencies ( relativePath ) : string [ ] ; } export class GlimmerRewriter implements ASTPluginWithDeps { template : TemplateTypes [ TEMPLATE_TYPE ] & { identifier : string ; path ? : string ; relativePath ? : string ; fullPath ? : string ; } ; templatePath : string ; analysis : GlimmerAnalysis ; elementCount : number ; syntax : Syntax ; block : Block ; styleMapping : GlimmerStyleMapping ; cssBlocksOpts : CSSBlocksConfiguration ; visitor : NodeVisitor ; visitors : NodeVisitor ; private elementAnalyzer : ElementAnalyzer < TEMPLATE_TYPE > ; constructor ( syntax , styleMapping , analysis , cssBlocksOpts ) { this . syntax = syntax ; this . analysis = analysis ; this . template = analysis . template ; this . templatePath = this . template . path || this . template . relativePath || this . template . fullPath || this . template . identifier ; this . block = analysis . getBlock ( DEFAULT_EXPORT ) ! ; this . styleMapping = styleMapping ; this . cssBlocksOpts = resolveConfiguration ( cssBlocksOpts ) ; this . elementCount = 0 ; this . elementAnalyzer = new ElementAnalyzer ( this . analysis , this . cssBlocksOpts ) ; if ( this . block ) { this . visitor = { ElementNode : this . ElementNode . bind ( this ) , SubExpression : this . HelperStatement . bind ( this ) , MustacheStatement : this . HelperStatement . bind ( this ) , BlockStatement : this . HelperStatement . bind ( this ) , } ; } else { this . visitor = { } ; } this . visitors = this . visitor ; } debug ( message , ... args ) { DEBUG ( `${ this . template . path }: ${ message }` , ... args ) ; } get name ( ) : string { return this . block ? "css-blocks-glimmer-rewriter" : "css-blocks-noop" ; } param _relativePath returns dependencies ( _relativePath ) { this . debug ( "Getting dependencies for" , _relativePath ) ; let deps = new Set ( ) ; for ( let block of this . analysis . transitiveBlockDependencies ( ) ) { let blockFile = block . identifier ; this . debug ( "block file path is" , blockFile ) ; if ( blockFile ) { deps . add ( blockFile ) ; } for ( let additionalDep of block . dependencies ) { deps . add ( additionalDep ) ; } } let depArray = new Array ( ... deps ) ; return depArray ; } HelperStatement ( node ) { if ( isEmberBuiltInNode ( node ) ) { this . BuiltInStatement ( node ) ; } else if ( isStyleOfHelper ( node ) ) { this . StyleOfHelper ( node ) ; } } BuiltInStatement ( node ) { this . elementCount ++ ; let name = pathString ( node ) ; if ( ! isEmberBuiltIn ( name ) ) return ; const space = this . syntax . builders . string ( " " ) ; const attrToStateMap = getEmberBuiltInStates ( name ) ; let atRootElement = ( this . elementCount === 1 ) ; const attrMap = this . elementAnalyzer . reanalyze ( node , atRootElement ) ; node . hash . pairs = node . hash . pairs . filter ( a => this . elementAnalyzer . isAttributeAnalyzed ( a . key ) [ 0 ] === null ) . filter ( a => ! attrToStateMap [ a . key ] ) ; for ( let attr of Object . keys ( attrMap ) ) { let element = attrMap [ attr ] ; let rewrite = this . styleMapping . simpleRewriteMapping ( element ) ; this . debug ( element . forOptimizer ( this . cssBlocksOpts ) [ 0 ] . toString ( ) ) ; let staticValue = null ; let dynamicValue = null ; if ( rewrite . staticClasses . length ) { staticValue = this . syntax . builders . string ( rewrite . staticClasses . join ( " " ) ) ; } if ( rewrite . dynamicClasses . length ) { dynamicValue = classnamesSubexpr ( this . syntax . builders , rewrite , element ) ; } if ( ! staticValue && ! dynamicValue ) { return ; } const attrValue = ( staticValue && dynamicValue ) ? this . syntax . builders . sexpr ( this . syntax . builders . path ( CONCAT_HELPER_NAME ) , [ staticValue , space , dynamicValue ] ) : ( staticValue || dynamicValue ) ; let hash = this . syntax . builders . pair ( attr , attrValue ! ) ; node . hash . pairs . push ( hash ) ; } } buildStringValue ( subexpression , value ) ; buildStringValue ( subexpression , value ) ; buildStringValue ( subexpression , value ) ; buildStringValue ( subexpression , value ) { if ( subexpression ) { return this . syntax . builders . string ( value ) ; } else { return this . syntax . builders . text ( value ) ; } } buildStringConcatExpr ( strings ) { return this . syntax . builders . sexpr ( this . syntax . builders . path ( "-css-blocks-concat" ) , strings . map ( s => { if ( typeof s === "string" ) { return this . syntax . builders . string ( s ) ; } else { return s ; } } ) ) ; } buildStringConcatStmnt ( strings ) { return this . syntax . builders . concat ( strings . map ( s => { if ( typeof s === "string" ) { return this . syntax . builders . text ( s ) ; } else { return s ; } } ) ) ; } buildClassValue ( subexpression , analysis ) ; buildClassValue ( subexpression , analysis ) ; buildClassValue ( subexpression , analysis ) { let sExp = null ; let stmnt = null ; let rewrite = this . styleMapping . simpleRewriteMapping ( analysis ) ; this . debug ( analysis . forOptimizer ( this . cssBlocksOpts ) [ 0 ] . toString ( ) ) ; if ( rewrite . staticClasses . length ) { if ( subexpression ) { sExp = this . buildStringValue ( true , rewrite . staticClasses . join ( " " ) ) ; } else { stmnt = this . buildStringValue ( false , rewrite . staticClasses . join ( " " ) ) ; } } if ( rewrite . dynamicClasses . length ) { if ( subexpression ) { let dynamicValue = classnamesSubexpr ( this . syntax . builders , rewrite , analysis ) ; if ( sExp ) { sExp = this . buildStringConcatExpr ( [ sExp , " " , dynamicValue ] ) ; } else { sExp = dynamicValue ; } } else { let dynamicValue = classnamesHelper ( this . syntax . builders , rewrite , analysis ) ; if ( stmnt ) { stmnt = this . buildStringConcatStmnt ( [ stmnt , " " , dynamicValue ] ) ; } else { stmnt = dynamicValue ; } } } return subexpression ? sExp : stmnt ; } StyleOfHelper ( node ) { this . elementCount ++ ; let atRootElement = ( this . elementCount === 1 ) ; let attrMap = this . elementAnalyzer . reanalyze ( node , atRootElement ) ; let attrNames = Object . keys ( attrMap ) ; if ( attrNames . length !== 1 || attrNames [ 0 ] !== "class" ) { const names = attrNames . filter ( name => name !== "class" ) ; throw cssBlockError ( `Unexpected attribute(s) [${ names . join ( ", " ) }] in rewrite for style-of helper.` , node , this . templatePath ) ; } node . path = this . syntax . builders . path ( "-css-blocks-concat" ) ; let attrValue = this . buildClassValue ( true , attrMap [ "class" ] ) ; if ( attrValue ) { node . params = [ attrValue ] ; } else { node . params = [ ] ; } node . hash . pairs = [ ] ; } ElementNode ( node ) { this . elementCount ++ ; let atRootElement = ( this . elementCount === 1 ) ; let attrMap = this . elementAnalyzer . reanalyze ( node , atRootElement ) ; node . attributes = node . attributes . filter ( a => this . elementAnalyzer . isAttributeAnalyzed ( a . name ) [ 0 ] === null ) ; for ( let attrName of Object . keys ( attrMap ) ) { let analysis = attrMap [ attrName ] ; let attrValue = this . buildClassValue ( false , analysis ) ; if ( ! attrValue ) continue ; if ( isTextNode ( attrValue ) && attrValue . chars === "" ) continue ; let attr = this . syntax . builders . attr ( attrName , attrValue ) ; node . attributes . push ( attr ) ; } } }