import { POSITION_UNKNOWN , SourceLocation , SourcePosition , isSourcePosition , } from "@opticss/element-analysis" ; import { SerializedTemplateInfo , TemplateAnalysis as OptimizationTemplateAnalysis , TemplateInfoFactory , TemplateTypes , } from "@opticss/template-api" ; import { ObjectDictionary , objectValues } from "@opticss/util" ; import { IdentGenerator } from "opticss" ; import { BlockFactory } from "../BlockParser" ; import { AttrValue , Block , BlockClass , Style } from "../BlockTree" ; import { ResolvedConfiguration } from "../configuration" ; import { allDone } from "../util" ; import { Analyzer } from "./Analyzer" ; import { ElementAnalysis , SerializedElementAnalysis , SerializedElementSourceAnalysis } from "./ElementAnalysis" ; import { TemplateValidator , TemplateValidatorOptions } from "./validations" ; /**
 * This interface defines a JSON friendly serialization
 * of an {Analysis}.
 */ export interface SerializedAnalysis < K extends keyof TemplateTypes > { template : SerializedTemplateInfo < K > ; blocks : ObjectDictionary < string > ; stylesFound : string [ ] ; elements : ObjectDictionary < SerializedElementAnalysis > ; } /**
 * This interface defines a JSON friendly serialization
 * of an {Analysis}.
 */ export interface SerializedSourceAnalysis < K extends keyof TemplateTypes > { template : SerializedTemplateInfo < K > ; blocks : ObjectDictionary < string > ; stylesFound : string [ ] ; elements : ObjectDictionary < SerializedElementSourceAnalysis > ; } type ElementAnalyzedCallback < BooleanExpression , StringExpression , TernaryExpression > = ( element : ElementAnalysis < BooleanExpression , StringExpression , TernaryExpression > ) => void ; /**
 * An Analysis performs book keeping and ensures internal consistency of the block objects referenced
 * within a single template. It is designed to be used as part of an AST walk over a template.
 *
 * 1. Call [[startElement startElement()]] at the beginning of an new html element.
 * 2. Call [[addStyle addStyle(style, isDynamic)]] for all the styles used on the current html element.
 * 2. Call [[addExclusiveStyle addExclusiveStyle(alwaysPresent, ...style)]] for all the styles used that are mutually exclusive on the current html element.
 * 3. Call [[endElement endElement()]] when done adding styles for the current element.
 */ export class Analysis < K extends keyof TemplateTypes > { idGenerator : IdentGenerator ; template : TemplateTypes [ K ] ; /**
     * A per-element correlation of styles used. The current correlation is added
     * to this list when [[endElement]] is called.
     */ elements : Map < string , ElementAnalysis < any , any , any > > ; /**
     * A map from a local name for the block to the [[Block]].
     * The local name must be a legal CSS ident/class name but this is not validated here.
     * See [[CLASS_NAME_IDENT]] for help validating a legal class name.
     */ private blocks : ObjectDictionary < Block > ; /**
     * The current element, created when calling [[startElement]].
     * The current element is unset after calling [[endElement]].
     */ private currentElement : ElementAnalysis < any , any , any > | undefined ; /**
     * Template validator instance to verify blocks applied to an element.
     */ private validator : TemplateValidator ; /**
     * Callback when an element is done being analyzed.
     * The element analysis will be sealed.
     */ onElementAnalyzed ? : ElementAnalyzedCallback < any , any , any > ; param template constructor ( template , options ? , onElementAnalyzed ? ) { this . idGenerator = new IdentGenerator ( ) ; this . template = template ; this . blocks = { } ; this . elements = new Map ( ) ; this . validator = new TemplateValidator ( options ) ; this . onElementAnalyzed = onElementAnalyzed ; } /**
     * Return the number of blocks discovered in this Template.
    */ blockCount ( ) { return Object . keys ( this . blocks ) . length ; } /**
     * Convenience setter for adding a block to the template scope.
     */ addBlock ( name , block ) { return this . blocks [ name ] = block ; } /**
     * Convenience getter for fetching a block from the template scope.
     */ getBlock ( name ) { return this . blocks [ name ] ; } /**
     * Return the number of elements discovered in this Analysis.
     */ elementCount ( ) { return this . elements . size ; } /**
     * Get the nth element discovered in this Analysis.
     */ getElement < BooleanExpression , StringExpression , TernaryExpression > ( idx ) { let mapIter = this . elements . entries ( ) ; let el = mapIter . next ( ) . value ; for ( let i = 0 ; i < idx ; i ++ ) { el = mapIter . next ( ) . value ; } return el [ 1 ] ; } /**
     * Return the number of styles discovered in this Analysis' Template.
     * TODO: This is slow. We can pre-compute this as elements are added.
     */ styleCount ( ) { let c = 0 ; for ( let el of this . elements . values ( ) ) { for ( let _s of el . attributesFound ( ) ) { c ++ ; } for ( let _s of el . classesFound ( ) ) { c ++ ; } } return c ; } /**
     * Get an Element by ID.
     */ getElementById < BooleanExpression , StringExpression , TernaryExpression > ( id ) { return this . elements . get ( id ) ; } param block return getBlockName ( block ) { for ( let name of Object . keys ( this . blocks ) ) { let searchBlock = this . blocks [ name ] ; let blockName = this . _searchForBlock ( block , searchBlock , name ) ; if ( blockName !== null ) { return blockName ; } } return null ; } _searchForBlock ( blockToFind , block , parentPath ) { if ( block === blockToFind ) { return parentPath ; } let blockRefs = new Array < [ string , Block ] > ( ) ; block . eachBlockReference ( ( name , refBlock ) => { blockRefs . push ( [ name , refBlock ] ) ; } ) ; for ( let [ name , refBlock ] of blockRefs ) { let currentSearchPath = `${ parentPath }>${ name }` ; let rv = this . _searchForBlock ( blockToFind , refBlock , currentSearchPath ) ; if ( rv !== null ) { return rv ; } } return null ; } param location param tagName returns startElement < BooleanExpression , StringExpression , TernaryExpression > ( location , tagName ? ) { if ( isSourcePosition ( location ) ) { location = { start : location } ; } if ( this . currentElement ) { throw new Error ( "Internal Error: failure to call endElement before previous call to startElement." ) ; } this . currentElement = new ElementAnalysis < BooleanExpression , StringExpression , TernaryExpression > ( location , this . reservedClassNames ( ) , tagName , this . idGenerator . nextIdent ( ) ) ; return this . currentElement ; } param element param endPosition endElement < BooleanExpression , StringExpression , TernaryExpression > ( element , endPosition ? ) { if ( this . currentElement !== element ) { throw new Error ( "Element is not the current element." ) ; } if ( endPosition ) { element . sourceLocation . end = endPosition ; } if ( ! element . id ) { element . id = this . idGenerator . nextIdent ( ) ; } if ( this . elements . get ( element . id ) ) { throw new Error ( `Internal Error: an element with id = ${ element . id } already exists in this analysis` ) ; } this . ensureFilename ( element . sourceLocation . start ) ; this . ensureFilename ( element . sourceLocation . end ) ; if ( ! element . sealed ) { element . seal ( ) ; } this . validator . validate ( this , element ) ; this . elements . set ( element . id , element ) ; if ( this . onElementAnalyzed ) { this . onElementAnalyzed ( element ) ; } this . currentElement = undefined ; } param post private ensureFilename ( pos ) { if ( pos && ! pos . filename ) { pos . filename = this . template . identifier ; } } return serializedName ( o ) { let blockName = this . getBlockName ( o . block ) ; if ( blockName === null ) { throw new Error ( `Block ${ o . block . identifier } is not registered in the dependency graph for this analysis.` ) ; } return `${ blockName }${ o . asSource ( ) }` ; } /**
     * All the blocks referenced by this analysis.
     */ referencedBlocks ( ) { return objectValues ( this . blocks ) ; } /**
     * For now, returns all aliases referenced by this block and all the blocks they
     * reference recursively. We can add more to this set in future
     */ reservedClassNames ( ) { let aliases = new Set < string > ( ) ; let blocks = this . transitiveBlockDependencies ( ) ; blocks . forEach ( block => { block . getAllStyleAliases ( ) . forEach ( alias => aliases . add ( alias ) ) ; } ) ; return aliases ; } /**
     * All the blocks referenced by this block and all the blocks they reference recursively.
     */ transitiveBlockDependencies ( ) { let deps = new Set < Block > ( ) ; this . referencedBlocks ( ) . forEach ( ( block ) => { deps . add ( block ) ; let moreDeps = block . transitiveBlockDependencies ( ) ; if ( moreDeps . size > 0 ) { deps = new Set ( [ ... deps , ... moreDeps ] ) ; } } ) ; return deps ; } /**
     * All bhe blocks this block depends on. Same as referenced blocks except for the return type.
     */ blockDependencies ( ) { return new Set < Block > ( this . referencedBlocks ( ) ) ; } * stylesFound ( dynamic ? ) { let found = new Set < Style > ( ) ; for ( let el of this . elements . values ( ) ) { for ( let s of el . classesFound ( dynamic ) ) { if ( found . has ( s ) ) continue ; found . add ( s ) ; yield s ; } for ( let s of el . attributesFound ( dynamic ) ) { if ( found . has ( s ) ) continue ; found . add ( s ) ; yield s ; } } } serializeSource ( blockPaths ? ) { let elements = { } ; let { template , blocks , stylesFound , styleIndexes } = this . _serializeSetup ( blockPaths ) ; this . elements . forEach ( ( el , key ) => { elements [ key ] = el . serializeSourceAnalysis ( styleIndexes ) ; } ) ; return { template , blocks , stylesFound , elements } ; } /**
     * Generates a [[SerializedTemplateAnalysis]] for this analysis.
     */ serialize ( blockPaths ? ) { let elements = { } ; let { template , blocks , stylesFound , styleIndexes } = this . _serializeSetup ( blockPaths ) ; this . elements . forEach ( ( el , key ) => { elements [ key ] = el . serialize ( styleIndexes ) ; } ) ; return { template , blocks , stylesFound , elements } ; } _serializeSetup ( blockPaths ? ) { let blocks = { } ; let stylesFound = [ ] ; let template = this . template . serialize ( ) as SerializedTemplateInfo < K > ; let styleNameMap = new Map < Style , string > ( ) ; let styleIndexes = new Map < Style , number > ( ) ; let blocksUsed = new Set < Block > ( ) ; let styles = [ ... this . stylesFound ( ) ] ; for ( let found of styles ) { blocksUsed . add ( found . block ) ; styleNameMap . set ( found , this . serializedName ( found ) ) ; } styles . sort ( ( a , b ) => { return styleNameMap . get ( a ) ! > styleNameMap . get ( b ) ! ? 1 : - 1 ; } ) ; styles . forEach ( ( s , idx ) => { stylesFound . push ( styleNameMap . get ( s ) ! ) ; styleIndexes . set ( s , idx ) ; } ) ; Object . keys ( this . blocks ) . forEach ( ( localName ) => { let block = this . blocks [ localName ] ; if ( blocksUsed . has ( block ) ) { blocks [ localName ] = blockPaths && blockPaths . get ( block ) || block . identifier ; } } ) ; return { template , blocks , stylesFound , styleIndexes } ; } param serializedAnalysis param blockFactory param parent static async deserializeSource ( serializedAnalysis , blockFactory , parent ) { let blockNames = Object . keys ( serializedAnalysis . blocks ) ; let info = TemplateInfoFactory . deserialize < keyof TemplateTypes > ( serializedAnalysis . template ) ; let analysis = parent . newAnalysis ( info ) ; let blockPromises = new Array < Promise < { name : string ; block : Block ; } > > ( ) ; blockNames . forEach ( n => { let blockIdentifier = serializedAnalysis . blocks [ n ] ; let promise = blockFactory . getBlock ( blockIdentifier ) . then ( block => { return { name : n , block : block } ; } ) ; blockPromises . push ( promise ) ; } ) ; let values = await allDone ( blockPromises ) ; let localScope = new Block ( "analysis-block" , "tmp" , "analysis-block" ) ; values . forEach ( o => { analysis . blocks [ o . name ] = o . block ; localScope . addBlockReference ( o . name , o . block ) ; } ) ; let styles = new Array < Style > ( ) ; serializedAnalysis . stylesFound . forEach ( s => { let style = localScope . find ( s ) ; if ( style ) { styles . push ( style ) ; } else { throw new Error ( `Cannot resolve ${ s } to a block style.` ) ; } } ) ; let styleRef = ( index ) => { let s = styles [ index ] ; if ( ! s ) { throw new Error ( "[internal error] Style index out of bounds!" ) ; } return s ; } ; let classRef = ( index ) => { let s = styleRef ( index ) ; if ( ! ( s instanceof BlockClass ) ) { throw new Error ( "[internal error] Block class expected." ) ; } return s ; } ; let attrValueRef = ( index ) => { let s = styleRef ( index ) ; if ( ! ( s instanceof AttrValue ) ) { throw new Error ( "[internal error] attribute value expected." ) ; } return s ; } ; let elementNames = Object . keys ( serializedAnalysis . elements ) ; elementNames . forEach ( ( elID ) => { let data = serializedAnalysis . elements [ elID ] ; let element = new ElementAnalysis < null , null , null > ( data . sourceLocation || { start : POSITION_UNKNOWN } , parent . reservedClassNames ( ) , data . tagName , elID ) ; for ( let analyzedStyle of data . analyzedStyles ) { ElementAnalysis . deserializeAnalyzedStyle ( element , analyzedStyle , styleRef , classRef , attrValueRef ) ; } element . seal ( ) ; analysis . elements . set ( elID , element ) ; if ( analysis . onElementAnalyzed ) analysis . onElementAnalyzed ( element ) ; } ) ; return analysis ; } param serializedAnalysis param blockFactory param parent static async deserialize ( serializedAnalysis , blockFactory , parent ) { let blockNames = Object . keys ( serializedAnalysis . blocks ) ; let info = TemplateInfoFactory . deserialize < keyof TemplateTypes > ( serializedAnalysis . template ) ; let analysis = parent . newAnalysis ( info ) ; let blockPromises = new Array < Promise < { name : string ; block : Block ; } > > ( ) ; blockNames . forEach ( n => { let blockIdentifier = serializedAnalysis . blocks [ n ] ; let promise = blockFactory . getBlock ( blockIdentifier ) . then ( block => { return { name : n , block : block } ; } ) ; blockPromises . push ( promise ) ; } ) ; let values = await allDone ( blockPromises ) ; let localScope = new Block ( "analysis-block" , "tmp" , "analysis-block" ) ; values . forEach ( o => { analysis . blocks [ o . name ] = o . block ; localScope . addBlockReference ( o . name , o . block ) ; } ) ; let objects = new Array < Style > ( ) ; serializedAnalysis . stylesFound . forEach ( s => { let style = localScope . find ( s ) ; if ( style ) { objects . push ( style ) ; } else { throw new Error ( `Cannot resolve ${ s } to a block style.` ) ; } } ) ; let elementNames = Object . keys ( serializedAnalysis . elements ) ; elementNames . forEach ( ( elID ) => { let data = serializedAnalysis . elements [ elID ] ; let element = new ElementAnalysis < null , null , null > ( data . sourceLocation || { start : POSITION_UNKNOWN } , parent . reservedClassNames ( ) , data . tagName , elID ) ; element . seal ( ) ; analysis . elements . set ( elID , element ) ; } ) ; return analysis ; } forOptimizer ( config ) { let optAnalysis = new OptimizationTemplateAnalysis < K > ( this . template ) ; for ( let element of this . elements . values ( ) ) { let result = element . forOptimizer ( config ) ; optAnalysis . elements . push ( result [ 0 ] ) ; } return optAnalysis ; } } export interface IAnalysis < K extends keyof TemplateTypes > extends Analysis < K > { }