import * as Handlebars from "handlebars" ; import * as fs from "graceful-fs" ; import * as path from "path" ; import { promisify } from "util" ; import * as globSync from "glob" ; import type { UnknownObject , FunctionObject , ConfigOptions , Engine , TemplateSpecificationObject , TemplateDelegateObject , FsCache , PartialTemplateOptions , PartialsDirObject , RenderOptions , RenderViewOptions , RenderCallback , HandlebarsImport , CompiledCache , PrecompiledCache , RenameFunction , } from "../types" ; const glob = promisify ( globSync ) ; const readFile = promisify ( fs . readFile ) ; const defaultConfig = { handlebars : Handlebars , extname : ".handlebars" , encoding : "utf8" , layoutsDir : undefined , partialsDir : undefined , defaultLayout : "main" , helpers : undefined , compilerOptions : undefined , runtimeOptions : undefined , } ; export default class ExpressHandlebars { config : ConfigOptions ; engine : Engine ; encoding : BufferEncoding ; layoutsDir : string ; extname : string ; compiled : CompiledCache ; precompiled : PrecompiledCache ; _fsCache : FsCache ; partialsDir : string | PartialsDirObject | ( string | PartialsDirObject ) [ ] ; compilerOptions : CompileOptions ; runtimeOptions : RuntimeOptions ; helpers : FunctionObject ; defaultLayout : string ; handlebars : HandlebarsImport ; constructor ( config = { } ) { Object . assign ( this , defaultConfig , config ) ; this . config = config ; this . engine = this . renderView . bind ( this ) ; if ( this . extname . charAt ( 0 ) !== "." ) { this . extname = "." + this . extname ; } this . compiled = { } ; this . precompiled = { } ; this . _fsCache = { } ; } async getPartials ( options = { } ) { if ( typeof this . partialsDir === "undefined" ) { return { } ; } const partialsDirs = Array . isArray ( this . partialsDir ) ? this . partialsDir : [ this . partialsDir ] ; const dirs = await Promise . all ( partialsDirs . map ( async ( dir ) => { let dirPath ; let dirTemplates ; let dirNamespace ; let dirRename ; if ( typeof dir === "string" ) { dirPath = dir ; } else if ( typeof dir === "object" ) { dirTemplates = dir . templates ; dirNamespace = dir . namespace ; dirRename = dir . rename ; dirPath = dir . dir ; } if ( ! dirPath && ! dirTemplates ) { throw new Error ( "A partials dir must be a string or config object" ) ; } const templates = dirTemplates || await this . getTemplates ( dirPath , options ) ; return { templates : templates as HandlebarsTemplateDelegate | TemplateSpecification , namespace : dirNamespace , rename : dirRename , } ; } ) ) ; const partials = { } ; for ( const dir of dirs ) { const { templates , namespace , rename } = dir ; const filePaths = Object . keys ( templates ) ; const getTemplateNameFn = typeof rename === "function" ? rename : this . _getTemplateName . bind ( this ) ; for ( const filePath of filePaths ) { const partialName = getTemplateNameFn ( filePath , namespace ) ; partials [ partialName ] = templates [ filePath ] ; } } return partials ; } async getTemplate ( filePath , options = { } ) { filePath = path . resolve ( filePath ) ; const encoding = options . encoding || this . encoding ; const cache = options . precompiled ? this . precompiled : this . compiled ; const template = options . cache && cache [ filePath ] ; if ( template ) { return template ; } try { cache [ filePath ] = this . _getFile ( filePath , { cache : options . cache , encoding } ) . then ( ( file ) => { const compileTemplate = ( options . precompiled ? this . _precompileTemplate : this . _compileTemplate ) . bind ( this ) ; return compileTemplate ( file , this . compilerOptions ) ; } ) ; return await cache [ filePath ] ; } catch ( err ) { delete cache [ filePath ] ; throw err ; } } async getTemplates ( dirPath , options = { } ) { const cache = options . cache ; const filePaths = await this . _getDir ( dirPath , { cache } ) ; const templates = await Promise . all ( filePaths . map ( filePath => { return this . getTemplate ( path . join ( dirPath , filePath ) , options ) ; } ) ) ; const hash = { } ; for ( let i = 0 ; i < filePaths . length ; i ++ ) { hash [ filePaths [ i ] ] = templates [ i ] ; } return hash ; } async render ( filePath , context = { } , options = { } ) { const encoding = options . encoding || this . encoding ; const [ template , partials ] = await Promise . all ( [ this . getTemplate ( filePath , { cache : options . cache , encoding } ) as Promise < HandlebarsTemplateDelegate > , ( options . partials || this . getPartials ( { cache : options . cache , encoding } ) ) as Promise < TemplateDelegateObject > , ] ) ; const helpers = { ... this . helpers , ... options . helpers } ; const runtimeOptions = { ... this . runtimeOptions , ... options . runtimeOptions } ; const data = { ... options . data , exphbs : { ... options , filePath , helpers , partials , runtimeOptions , } , } ; const html = this . _renderTemplate ( template , context , { ... runtimeOptions , data , helpers , partials , } ) ; return html ; } async renderView ( viewPath ) ; async renderView ( viewPath , options ) ; async renderView ( viewPath , callback ) ; async renderView ( viewPath , options , callback ) ; async renderView ( viewPath , options = { } , callback = null ) { if ( typeof options === "function" ) { callback = options ; options = { } ; } const context = options as UnknownObject ; let promise = null ; if ( ! callback ) { promise = new Promise ( ( resolve , reject ) => { callback = ( err , value ) => { err !== null ? reject ( err ) : resolve ( value ) ; } ; } ) ; } let view ; const views = options . settings && options . settings . views ; const viewsPath = this . _resolveViewsPath ( views , viewPath ) ; if ( viewsPath ) { view = this . _getTemplateName ( path . relative ( viewsPath , viewPath ) ) ; this . partialsDir = this . config . partialsDir || path . join ( viewsPath , "partials/" ) ; this . layoutsDir = this . config . layoutsDir || path . join ( viewsPath , "layouts/" ) ; } const encoding = options . encoding || this . encoding ; const helpers = { ... this . helpers , ... options . helpers } ; const partials = { ... await this . getPartials ( { cache : options . cache , encoding } ) as TemplateDelegateObject , ... ( options . partials || { } ) , } ; const renderOptions = { cache : options . cache , encoding , view , layout : "layout" in options ? options . layout : this . defaultLayout , data : options . data , helpers , partials , runtimeOptions : options . runtimeOptions , } ; try { let html = await this . render ( viewPath , context , renderOptions ) ; const layoutPath = this . _resolveLayoutPath ( renderOptions . layout ) ; if ( layoutPath ) { html = await this . render ( layoutPath , { ... context , body : html } , { ... renderOptions , layout : undefined } ) ; } callback ( null , html ) ; } catch ( err ) { callback ( err ) ; } return promise ; } protected _compileTemplate ( template , options = { } ) { return this . handlebars . compile ( template . trim ( ) , options ) ; } protected _precompileTemplate ( template , options = { } ) { return this . handlebars . precompile ( template . trim ( ) , options ) ; } protected _renderTemplate ( template , context = { } , options = { } ) { return template ( context , options ) . trim ( ) ; } private async _getDir ( dirPath , options = { } ) { dirPath = path . resolve ( dirPath ) ; const cache = this . _fsCache ; let dir = options . cache && ( cache [ dirPath ] as Promise < string [ ] > ) ; if ( dir ) { return ( await dir ) . concat ( ) ; } const pattern = "**/*" + this . extname ; try { dir = cache [ dirPath ] = glob ( pattern , { cwd : dirPath , follow : true , } ) ; if ( options . _throwTestError ) { throw new Error ( "test" ) ; } return ( await dir ) . concat ( ) ; } catch ( err ) { delete cache [ dirPath ] ; throw err ; } } private async _getFile ( filePath , options = { } ) { filePath = path . resolve ( filePath ) ; const cache = this . _fsCache ; const encoding = options . encoding || this . encoding ; const file = options . cache && ( cache [ filePath ] as Promise < string > ) ; if ( file ) { return file ; } try { cache [ filePath ] = readFile ( filePath , { encoding : encoding || "utf8" } ) ; return await cache [ filePath ] as string ; } catch ( err ) { delete cache [ filePath ] ; throw err ; } } private _getTemplateName ( filePath , namespace = null ) { let name = filePath ; if ( name . endsWith ( this . extname ) ) { name = name . substring ( 0 , name . length - this . extname . length ) ; } if ( namespace ) { name = namespace + "/" + name ; } return name ; } private _resolveViewsPath ( views , file ) { if ( ! Array . isArray ( views ) ) { return views ; } let lastDir = path . resolve ( file ) ; let dir = path . dirname ( lastDir ) ; const absoluteViews = views . map ( v => path . resolve ( v ) ) ; while ( dir !== lastDir ) { const index = absoluteViews . indexOf ( dir ) ; if ( index >= 0 ) { return views [ index ] ; } lastDir = dir ; dir = path . dirname ( lastDir ) ; } return null ; } private _resolveLayoutPath ( layoutPath ) { if ( ! layoutPath ) { return null ; } if ( ! path . extname ( layoutPath ) ) { layoutPath += this . extname ; } return path . resolve ( this . layoutsDir || "" , layoutPath ) ; } }