import { Dictionary } from "async" ; import { postcss } from "opticss" ; import { AttributeSelector , ClassSelector , Declaration , DefinitionAST , DefinitionRoot , GlobalDeclaration , KeyCompoundSelector , Rule , ScopeSelector , Selector , builders , map as mapToDefinition , visit } from "../../BlockParser/ast" ; import { AttrValue , Block , BlockClass , Style , isAttrValue , isBlockClass } from "../../BlockTree" ; import { ResolvedConfiguration } from "../../configuration" ; import { CompiledDefinitionMapper , PathResolver } from "./CompiledDefinitionMapper" ; import { PostcssASTBuilder } from "./PostcssASTBuilder" ; export { PathResolver } from "./CompiledDefinitionMapper" ; export const INLINE_DEFINITION_FILE = Symbol ( "Inline Definition" ) ; export class BlockDefinitionCompiler { postcss : typeof postcss ; config : ResolvedConfiguration ; pathResolver : PathResolver ; constructor ( postcssImpl , pathResolver , config ) { this . postcss = postcssImpl ; this . config = config ; this . pathResolver = pathResolver ; } compile ( block , reservedClassNames ) { let ast = this . compileToAST ( block , reservedClassNames ) ; let visitor = new PostcssASTBuilder ( this . postcss ) ; visit ( visitor , ast ) ; return visitor . root ; } globalDeclarations ( block ) { let globals = new Array < GlobalDeclaration > ( ) ; for ( let attrValue of block . rootClass . allAttributeValues ( ) ) { if ( attrValue . isGlobal ) { let selector = builders . attributeSelector ( attrValue . attribute . name , attrValue . isPresenceRule ? undefined : attrValue . value ) ; globals . push ( builders . globalDeclaration ( selector ) ) ; } } return globals ; } compileToAST ( block , reservedClassNames ) { if ( ! block . blockAST ) { throw new Error ( "The block's AST is missing." ) ; } let mapper = new CompiledDefinitionMapper ( block , this . pathResolver ) ; let definitionRoot = mapToDefinition ( mapper , block . blockAST , "block" , "definition" ) ; definitionRoot . children . unshift ( ... this . globalDeclarations ( block ) ) ; definitionRoot . children . unshift ( builders . blockSyntaxVersion ( 1 ) ) ; for ( let style of block . all ( true ) ) { definitionRoot . children . push ( this . styleToRule ( style , reservedClassNames ) ) ; } definitionRoot . children . push ( ... this . complexCompositions ( block ) ) ; return definitionRoot ; } styleToRule ( style , reservedClassNames ) { let selectors = new Array < Selector < DefinitionAST > > ( ) ; let blockClass = isAttrValue ( style ) ? style . blockClass : style ; if ( isAttrValue ( style ) ) { selectors . push ( attributeSelectors ( blockClass , [ style ] ) ) ; } else { selectors . push ( elementSelector ( blockClass ) ) ; } let declarations = new Array < Declaration > ( ) ; if ( isBlockClass ( style ) && style . isRoot ) { declarations . push ( builders . declaration ( "block-id" , `"${ style . block . guid }"` ) ) ; declarations . push ( builders . declaration ( "block-name" , `"${ style . block . name }"` ) ) ; let baseName = style . block . getReferencedBlockLocalName ( style . block . base ) ; if ( baseName ) { declarations . push ( builders . declaration ( "extends" , baseName ) ) ; } let implemented = style . block . getImplementedBlocks ( ) ; if ( implemented . length > 0 ) { let names = implemented . map ( b => style . block . getReferencedBlockLocalName ( b ) ! ) ; declarations . push ( builders . declaration ( "implements" , names . join ( ", " ) ) ) ; } } let compositions = new Array < string > ( ) ; for ( let composition of blockClass . composedStyles ( ) ) { if ( composition . conditions . length === 0 && blockClass === style ) { compositions . push ( composition . path ) ; } else if ( composition . conditions . length === 1 && composition . conditions [ 0 ] === style ) { compositions . push ( composition . path ) ; } } if ( compositions . length > 0 ) { declarations . push ( builders . declaration ( "composes" , '"' + compositions . join ( '", "' ) + '"' ) ) ; } declarations . push ( builders . declaration ( "block-class" , style . cssClass ( this . config , reservedClassNames ) ) ) ; let aliasValues = new Array ( ... style . getStyleAliases ( ) ) ; if ( aliasValues . length ) { declarations . push ( builders . declaration ( "block-alias" , aliasValues . join ( " " ) ) ) ; } return builders . rule ( selectors , declarations ) ; } /**
     * The complex compositions which apply to the intersection of more than one
     * attribute require the generation of ruleset that targets all of those
     * attributes together.
     *
     * Note: Simple compositions (which apply to a single block class or
     * attribute) are processed when we generate the rule for that style.
     */ complexCompositions ( block ) { let complexCompositions = { } ; for ( let blockClass of block . classes ) { for ( let composition of blockClass . composedStyles ( ) ) { if ( composition . conditions . length > 1 ) { let key = composition . conditions . map ( c => c . asSource ( ) ) . sort ( ) . join ( " " ) ; if ( ! complexCompositions [ key ] ) { complexCompositions [ key ] = { blockClass , attributes : composition . conditions , paths : [ composition . path ] , } ; } else { complexCompositions [ key ] . paths . push ( composition . path ) ; } } } } let rules = new Array < Rule < DefinitionAST > > ( ) ; for ( let key of Object . keys ( complexCompositions ) ) { let composition = complexCompositions [ key ] ; let selector = attributeSelectors ( composition . blockClass , composition . attributes ) ; let declarations = new Array < Declaration > ( ) ; declarations . push ( builders . declaration ( "composes" , composition . paths . join ( ", " ) ) ) ; rules . push ( builders . rule ( [ selector ] , declarations ) ) ; } return rules ; } insertBlockDefinitionURLComment ( css , definitionPath ) { let comment = this . postcss . comment ( { text : `#blockDefinitionURL=${ definitionPath }` } ) ; Object . assign ( comment . raws , { left : "" , right : "" } ) ; css . append ( comment ) ; } insertInlineBlockDefinitionURLComment ( css , definition ) { let cssText = definition . toResult ( ) . css ; let base64EncodedText = Buffer . from ( cssText ) . toString ( "base64" ) ; let url = `data:text/css;base64,${ base64EncodedText }` ; this . insertBlockDefinitionURLComment ( css , url ) ; } } function elementSelector ( blockClass ) { if ( blockClass . isRoot ) { return builders . scopeSelector ( ) ; } else { return builders . classSelector ( blockClass . name ) ; } } function attributeSelectors ( blockClass , attributes ) { let attributeSelectors = new Array < AttributeSelector > ( ) ; for ( let style of attributes ) { if ( style . isPresenceRule ) { attributeSelectors . push ( builders . attributeSelector ( style . attribute . name ) ) ; } else { attributeSelectors . push ( builders . attributeSelector ( style . attribute . name , style . value ) ) ; } } return builders . keyCompoundSelector ( elementSelector ( blockClass ) , attributeSelectors ) ; }