UNPKG

@mojojs/core

Version:

Real-time web framework

197 lines 7.84 kB
import { promisify } from 'node:util'; import { gzip } from 'node:zlib'; import Path from '@mojojs/path'; import yaml from 'js-yaml'; const gzipPromise = promisify(gzip); /** * Renderer class. */ export class Renderer { constructor() { /** * Try to negotiate compression for dynamically generated response content and gzip compress it automatically. */ this.autoCompress = true; /** * The default template engine to use for rendering in cases where auto-detection doesn't work, like for inline * templates, defaults to `tmpl`. */ this.defaultEngine = 'tmpl'; /** * The default format to render if format is not set, defaults to `html`. Note that changing the default away from * `html` is not recommended, as it has the potential to break, for example, plugins with bundled templates. */ this.defaultFormat = 'html'; /** * Template engines. */ this.engines = {}; /** * Minimum output size in bytes required for compression to be used if enabled, defaults to `860`. */ this.minCompressSize = 860; /** * Directories to look for templates in, first one has the highest precedence. */ this.viewPaths = [Path.currentFile().sibling('..', 'vendor', 'views').toString()]; this._viewIndex = undefined; } /** * Register a template engine. */ addEngine(name, engine) { this.engines[name] = engine; return this; } /** * Find a view for render parameters. * @example * // Find path to JSON view * const suggestion = renderer.findView({view: 'foo', 'format': 'json'}); * const {path} = suggestion; */ findView(options) { const { engine, format, variant, view } = options; const index = this._viewIndex; if (view === undefined || index === undefined || index[view] === undefined) return null; // Format and engine let views = index[view]; if (format !== undefined) views = views.filter(view => view.format === format); if (engine !== undefined) views = views.filter(view => view.engine === engine); // Variants let fallback; if (views.length > 1) { for (const suggestion of views) { if (suggestion.variant === variant) return suggestion; if (fallback === undefined && suggestion.variant === undefined) fallback = suggestion; } } return fallback ?? views[0]; } /** * Render output through one of the template engines. */ async render(ctx, options) { const { log } = ctx; if (options.text !== undefined) { log.trace('Rendering text response'); return { output: options.text, format: options.format ?? 'txt' }; } if (options.json !== undefined) { log.trace('Rendering JSON response'); if (options.pretty === true) { return { output: Buffer.from(JSON.stringify(options.json, null, 2)), format: options.format ?? 'json' }; } return { output: Buffer.from(JSON.stringify(options.json)), format: options.format ?? 'json' }; } if (options.yaml !== undefined) { log.trace('Rendering YAML response'); return { output: Buffer.from(yaml.dump(options.yaml)), format: options.format ?? 'yaml' }; } if (options.inline !== undefined) { log.trace('Rendering inline view'); const result = await this._renderInline(ctx, options); if (result === null) return null; if (options.inlineLayout !== undefined) { options.inline = options.inlineLayout; ctx.content.main = result.output.toString(); log.trace('Rendering inline layout'); return await this._renderInline(ctx, options); } return result; } if (options.view === undefined && options.engine !== undefined) { const engine = this.engines[options.engine]; if (engine === undefined) return null; log.trace(`Rendering response with "${options.engine}" engine`); return { output: await engine.render(ctx, options), format: options.format ?? this.defaultFormat }; } const { stash } = ctx; const controller = stash.controller; const action = stash.action; if (options.view === undefined && controller !== undefined && action !== undefined) { options.view = `${controller}/${action}`; } if (options.view !== undefined) { log.trace(`Rendering view "${options.view}"`); const result = await this._renderView(ctx, options); if (result === null) return null; if (options.layout !== undefined) { options.view = `layouts/${options.layout}`; ctx.content.main = result.output.toString(); log.trace(`Rendering layout "${options.view}"`); return await this._renderView(ctx, options); } return result; } return null; } /** * Finalize dynamically generated response content and compress it if possible. */ async respond(ctx, result, options) { const { res } = ctx; if (res.isSent) return false; let { output } = result; if (this.autoCompress === true && Buffer.byteLength(output) >= this.minCompressSize) { res.append('Vary', 'Accept-Encoding'); const accept = ctx.req.get('accept-encoding'); if (accept !== null && /gzip/i.test(accept)) { res.set('Content-Encoding', 'gzip'); output = await gzipPromise(output); } } if (options.status !== undefined) res.status(options.status); const type = ctx.app.mime.extType(result.format); if (type !== null) res.type(type); await res.send(output); return true; } /** * Prepare views for rendering. */ async warmup() { const viewIndex = (this._viewIndex = {}); for (const dir of this.viewPaths.map(path => new Path(path))) { if ((await dir.exists()) === false) continue; for await (const file of dir.list({ recursive: true })) { const name = dir.relative(file).toArray().join('/'); const match = name.match(/^(.+)\.([^.]+?)(?:\+([^.]+))?\.([^.]+)$/); if (match === null) continue; if (viewIndex[match[1]] === undefined) viewIndex[match[1]] = []; viewIndex[match[1]].push({ format: match[2], variant: match[3], engine: match[4], path: file.toString() }); } } } async _renderInline(ctx, options) { const engine = this.engines[options.engine ?? this.defaultEngine]; if (engine === undefined) return null; return { output: await engine.render(ctx, options), format: options.format ?? this.defaultFormat }; } async _renderView(ctx, options) { const view = this.findView(options); if (view === null) return null; const engine = this.engines[view.engine]; if (engine === undefined) return null; options.viewPath = view.path; return { output: await engine.render(ctx, options), format: view.format }; } } //# sourceMappingURL=renderer.js.map