import {AppClass} from '../lifecycle/AppClass' import {Config} from '../service/Config' import {Container} from '../di' import {ErrorWithContext, hasOwnProperty, Maybe, UniversalPath} from '../util' import {Routing} from '../service/Routing' import {RequestLocalStorage} from '../http/RequestLocalStorage' import {Request} from '../http/lifecycle/Request' /** * Abstract base class for rendering views via different view engines. */ export abstract class ViewEngine extends AppClass { protected readonly config: Config protected readonly routing: Routing protected readonly request: RequestLocalStorage protected readonly debug: boolean protected readonly namespaces: {[key: string]: UniversalPath} = {} protected readonly globals: {[key: string]: (req: Maybe) => any} = {} constructor() { super() this.config = Container.getContainer().make(Config) this.routing = Container.getContainer().make(Routing) this.request = Container.getContainer().make(RequestLocalStorage) this.debug = (this.config.get('server.mode', 'production') === 'development' || this.config.get('server.debug', false)) } /** * Get the UniversalPath to the base directory where views are loaded from. */ public get path(): UniversalPath { return this.app().appPath(...['resources', 'views']) // FIXME allow configuring } /** * Given a template string and a set of variables for the view, render the string to HTML and return it. * @param templateString * @param locals */ public abstract renderString(templateString: string, locals: {[key: string]: any}): string | Promise /** * Given the canonical name of a template file, render the file using the provided variables. * @param templateName * @param locals */ public abstract renderByName(templateName: string, locals: {[key: string]: any}): string | Promise /** * Get the file extension of template files of this engine. * @example `.pug` */ public abstract getFileExtension(): string /** * Get the global variables that should be passed to every view rendered. * @protected */ protected getGlobals(): {[key: string]: any} { const globals: {[key: string]: any} = { app: this.app(), config: (key: string, fallback?: any) => this.config.get(key, fallback), asset: (...parts: string[]) => this.routing.getAssetPath(...parts).toRemote, vendor: (namespace: string, ...parts: string[]) => this.routing.getVendorPath(namespace, ...parts).toRemote, named: (name: string) => this.routing.getNamedPath(name).toRemote, route: (...parts: string[]) => this.routing.getAppUrl().concat(...parts).toRemote, hasRoute: (name: string) => this.routing.hasNamedRoute(name), } const req = this.request.get() if ( req ) { globals.request = () => req } for ( const key in this.globals ) { if ( !hasOwnProperty(this.globals, key) ) { continue } globals[key] = this.globals[key](req) } return globals } /** * Register a new factory that produces a global available in the templates by default. * @param name * @param factory */ public registerGlobalFactory(name: string, factory: (req: Maybe) => any): this { this.globals[name] = factory return this } /** * Register a path as a root for rendering views prefixed with the given namespace. * @param namespace * @param basePath */ public registerNamespace(namespace: string, basePath: UniversalPath): this { if ( namespace.startsWith('@') ) { namespace = namespace.substr(1) } this.namespaces[namespace] = basePath return this } /** * Given the name of a template, get a UniversalPath pointing to its file. * @param templateName */ public resolveName(templateName: string): UniversalPath { let path = this.path if ( templateName.startsWith('@') ) { const [namespace, ...parts] = templateName.split(':') path = this.namespaces[namespace.substr(1)] if ( !path ) { throw new ErrorWithContext('Invalid template namespace: ' + namespace, { namespace, templateName, }) } templateName = parts.join(':') } if ( !templateName.endsWith(this.getFileExtension()) ) { templateName += this.getFileExtension() } return path.concat(...templateName.split(':')) } /** * Given the name of a template, get a UniversalPath to the root of the tree where * that template resides. * @param templateName */ public resolveBasePath(templateName: string): UniversalPath { let path = this.path if ( templateName.startsWith('@') ) { const [namespace] = templateName.split(':') path = this.namespaces[namespace.substr(1)] if ( !path ) { throw new ErrorWithContext('Invalid template namespace: ' + namespace, { namespace, templateName, }) } } return path } }