mirror of
				https://scm.univ-tours.fr/22107988t/rappaurio-sae501_502.git
				synced 2025-11-04 02:45:22 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			429 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			429 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/*
 | 
						|
 * Copyright (c) 2015, Yahoo Inc. All rights reserved.
 | 
						|
 * Copyrights licensed under the New BSD License.
 | 
						|
 * See the accompanying LICENSE file for terms.
 | 
						|
 */
 | 
						|
 | 
						|
import * as Handlebars from "handlebars";
 | 
						|
import * as fs from "graceful-fs";
 | 
						|
import * as path from "node:path";
 | 
						|
import { promisify } from "node:util";
 | 
						|
import { glob } from "glob";
 | 
						|
import type {
 | 
						|
	UnknownObject,
 | 
						|
	HelperDelegateObject,
 | 
						|
	ConfigOptions,
 | 
						|
	Engine,
 | 
						|
	TemplateSpecificationObject,
 | 
						|
	TemplateDelegateObject,
 | 
						|
	FsCache,
 | 
						|
	PartialTemplateOptions,
 | 
						|
	PartialsDirObject,
 | 
						|
	RenderOptions,
 | 
						|
	RenderViewOptions,
 | 
						|
	RenderCallback,
 | 
						|
	HandlebarsImport,
 | 
						|
	CompiledCache,
 | 
						|
	PrecompiledCache,
 | 
						|
	RenameFunction,
 | 
						|
} from "../types";
 | 
						|
 | 
						|
const readFile = promisify(fs.readFile);
 | 
						|
 | 
						|
// -----------------------------------------------------------------------------
 | 
						|
 | 
						|
const defaultConfig: ConfigOptions = {
 | 
						|
	handlebars: Handlebars,
 | 
						|
	extname: ".handlebars",
 | 
						|
	encoding: "utf8",
 | 
						|
	layoutsDir: undefined, // Default layouts directory is relative to `express settings.view` + `layouts/`
 | 
						|
	partialsDir: undefined, // Default partials directory is relative to `express settings.view` + `partials/`
 | 
						|
	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: HelperDelegateObject;
 | 
						|
	defaultLayout: string;
 | 
						|
	handlebars: HandlebarsImport;
 | 
						|
 | 
						|
	constructor (config: ConfigOptions = {}) {
 | 
						|
		// Config properties with defaults.
 | 
						|
		Object.assign(this, defaultConfig, config);
 | 
						|
 | 
						|
		// save given config to override other settings.
 | 
						|
		this.config = config;
 | 
						|
 | 
						|
		// Express view engine integration point.
 | 
						|
		this.engine = this.renderView.bind(this);
 | 
						|
 | 
						|
		// Normalize `extname`.
 | 
						|
		if (this.extname.charAt(0) !== ".") {
 | 
						|
			this.extname = "." + this.extname;
 | 
						|
		}
 | 
						|
 | 
						|
		// Internal caches of compiled and precompiled templates.
 | 
						|
		this.compiled = {};
 | 
						|
		this.precompiled = {};
 | 
						|
 | 
						|
		// Private internal file system cache.
 | 
						|
		this._fsCache = {};
 | 
						|
	}
 | 
						|
 | 
						|
	async getPartials (options: PartialTemplateOptions = {}): Promise<TemplateSpecificationObject|TemplateDelegateObject> {
 | 
						|
		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: string;
 | 
						|
			let dirTemplates: TemplateDelegateObject;
 | 
						|
			let dirNamespace: string;
 | 
						|
			let dirRename: RenameFunction;
 | 
						|
 | 
						|
			// Support `partialsDir` collection with object entries that contain a
 | 
						|
			// templates promise and a namespace.
 | 
						|
			if (typeof dir === "string") {
 | 
						|
				dirPath = dir;
 | 
						|
			} else if (typeof dir === "object") {
 | 
						|
				dirTemplates = dir.templates;
 | 
						|
				dirNamespace = dir.namespace;
 | 
						|
				dirRename = dir.rename;
 | 
						|
				dirPath = dir.dir;
 | 
						|
			}
 | 
						|
 | 
						|
			// We must have some path to templates, or templates themselves.
 | 
						|
			if (!dirPath && !dirTemplates) {
 | 
						|
				throw new Error("A partials dir must be a string or config object");
 | 
						|
			}
 | 
						|
 | 
						|
			const templates: HandlebarsTemplateDelegate|TemplateSpecification = dirTemplates || await this.getTemplates(dirPath, options);
 | 
						|
 | 
						|
			return {
 | 
						|
				templates: templates as HandlebarsTemplateDelegate|TemplateSpecification,
 | 
						|
				namespace: dirNamespace,
 | 
						|
				rename: dirRename,
 | 
						|
			};
 | 
						|
		}));
 | 
						|
 | 
						|
		const partials: TemplateDelegateObject|TemplateSpecificationObject = {};
 | 
						|
 | 
						|
		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: string, options: PartialTemplateOptions = {}): Promise<HandlebarsTemplateDelegate|TemplateSpecification> {
 | 
						|
		filePath = path.resolve(filePath);
 | 
						|
 | 
						|
		const encoding = options.encoding || this.encoding;
 | 
						|
		const cache: PrecompiledCache|CompiledCache = options.precompiled ? this.precompiled : this.compiled;
 | 
						|
		const template: Promise<HandlebarsTemplateDelegate|TemplateSpecification> = options.cache && cache[filePath];
 | 
						|
 | 
						|
		if (template) {
 | 
						|
			return template;
 | 
						|
		}
 | 
						|
 | 
						|
		// Optimistically cache template promise to reduce file system I/O, but
 | 
						|
		// remove from cache if there was a problem.
 | 
						|
		try {
 | 
						|
			cache[filePath] = this._getFile(filePath, { cache: options.cache, encoding })
 | 
						|
				.then((file: string) => {
 | 
						|
					const compileTemplate: (file: string, options: RuntimeOptions) => TemplateSpecification|HandlebarsTemplateDelegate = (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: string, options: PartialTemplateOptions = {}): Promise<HandlebarsTemplateDelegate|TemplateSpecification> {
 | 
						|
		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: string, context: UnknownObject = {}, options: RenderOptions = {}): Promise<string> {
 | 
						|
		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: HelperDelegateObject = { ...this.helpers, ...options.helpers };
 | 
						|
		const runtimeOptions = { ...this.runtimeOptions, ...options.runtimeOptions };
 | 
						|
 | 
						|
		// Add ExpressHandlebars metadata to the data channel so that it's
 | 
						|
		// accessible within the templates and helpers, namespaced under:
 | 
						|
		// `@exphbs.*`
 | 
						|
		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: string): Promise<string>;
 | 
						|
	async renderView (viewPath: string, options: RenderViewOptions): Promise<string>;
 | 
						|
	async renderView (viewPath: string, callback: RenderCallback): Promise<null>;
 | 
						|
	async renderView (viewPath: string, options: RenderViewOptions, callback: RenderCallback): Promise<null>;
 | 
						|
	async renderView (viewPath: string, options: RenderViewOptions|RenderCallback = {}, callback: RenderCallback|null = null): Promise<string|null> {
 | 
						|
		if (typeof options === "function") {
 | 
						|
			callback = options;
 | 
						|
			options = {};
 | 
						|
		}
 | 
						|
 | 
						|
		const context = options as UnknownObject;
 | 
						|
 | 
						|
		let promise: Promise<string>|null = null;
 | 
						|
		if (!callback) {
 | 
						|
			promise = new Promise((resolve, reject) => {
 | 
						|
				callback = (err, value) => { err !== null ? reject(err) : resolve(value); };
 | 
						|
			});
 | 
						|
		}
 | 
						|
 | 
						|
		// Express provides `settings.views` which is the path to the views dir that
 | 
						|
		// the developer set on the Express app. When this value exists, it's used
 | 
						|
		// to compute the view's name. Layouts and Partials directories are relative
 | 
						|
		// to `settings.view` path
 | 
						|
		let view: string;
 | 
						|
		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;
 | 
						|
 | 
						|
		// Merge render-level and instance-level helpers together.
 | 
						|
		const helpers = { ...this.helpers, ...options.helpers };
 | 
						|
 | 
						|
		// Merge render-level and instance-level partials together.
 | 
						|
		const partials: TemplateDelegateObject = {
 | 
						|
			...await this.getPartials({ cache: options.cache, encoding }) as TemplateDelegateObject,
 | 
						|
			...(options.partials || {}),
 | 
						|
		};
 | 
						|
 | 
						|
		// Pluck-out ExpressHandlebars-specific options and Handlebars-specific
 | 
						|
		// rendering options.
 | 
						|
		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;
 | 
						|
	}
 | 
						|
 | 
						|
	resetCache (filePathsOrFilter?: string | string[] | ((template: string) => boolean)) {
 | 
						|
		let filePaths: string[] = [];
 | 
						|
 | 
						|
		if (typeof filePathsOrFilter === "undefined") {
 | 
						|
			filePaths = Object.keys(this._fsCache);
 | 
						|
		} else if (typeof filePathsOrFilter === "string") {
 | 
						|
			filePaths = [filePathsOrFilter];
 | 
						|
		} else if (typeof filePathsOrFilter === "function") {
 | 
						|
			filePaths = Object.keys(this._fsCache).filter(filePathsOrFilter);
 | 
						|
		} else if (Array.isArray(filePathsOrFilter)) {
 | 
						|
			filePaths = filePathsOrFilter;
 | 
						|
		}
 | 
						|
 | 
						|
		for (const filePath of filePaths) {
 | 
						|
			delete this._fsCache[filePath];
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// -- Protected Hooks ----------------------------------------------------------
 | 
						|
 | 
						|
	protected _compileTemplate (template: string, options: RuntimeOptions = {}): HandlebarsTemplateDelegate {
 | 
						|
		return this.handlebars.compile(template.trim(), options);
 | 
						|
	}
 | 
						|
 | 
						|
	protected _precompileTemplate (template: string, options: RuntimeOptions = {}): TemplateSpecification {
 | 
						|
		return this.handlebars.precompile(template.trim(), options);
 | 
						|
	}
 | 
						|
 | 
						|
	protected _renderTemplate (template: HandlebarsTemplateDelegate, context: UnknownObject = {}, options: RuntimeOptions = {}): string {
 | 
						|
		return template(context, options).trim();
 | 
						|
	}
 | 
						|
 | 
						|
	// -- Private ------------------------------------------------------------------
 | 
						|
 | 
						|
	private async _getDir (dirPath: string, options: PartialTemplateOptions = {}): Promise<string[]> {
 | 
						|
		dirPath = path.resolve(dirPath);
 | 
						|
 | 
						|
		const cache = this._fsCache;
 | 
						|
		let dir = options.cache && (cache[dirPath] as Promise<string[]>);
 | 
						|
 | 
						|
		if (dir) {
 | 
						|
			return [...await dir];
 | 
						|
		}
 | 
						|
 | 
						|
		const pattern = "**/*" + this.extname;
 | 
						|
 | 
						|
		// Optimistically cache dir promise to reduce file system I/O, but remove
 | 
						|
		// from cache if there was a problem.
 | 
						|
 | 
						|
		try {
 | 
						|
			dir = cache[dirPath] = glob(pattern, {
 | 
						|
				cwd: dirPath,
 | 
						|
				follow: true,
 | 
						|
				posix: true,
 | 
						|
			});
 | 
						|
			// @ts-expect-error FIXME: not sure how to throw error in glob for test coverage
 | 
						|
			if (options._throwTestError) {
 | 
						|
				throw new Error("test");
 | 
						|
			}
 | 
						|
 | 
						|
			return [...await dir];
 | 
						|
		} catch (err) {
 | 
						|
			delete cache[dirPath];
 | 
						|
			throw err;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	private async _getFile (filePath: string, options: PartialTemplateOptions = {}): Promise<string> {
 | 
						|
		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;
 | 
						|
		}
 | 
						|
 | 
						|
		// Optimistically cache file promise to reduce file system I/O, but remove
 | 
						|
		// from cache if there was a problem.
 | 
						|
		try {
 | 
						|
			cache[filePath] = readFile(filePath, { encoding: encoding || "utf8" });
 | 
						|
			return await cache[filePath] as string;
 | 
						|
		} catch (err) {
 | 
						|
			delete cache[filePath];
 | 
						|
			throw err;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	private _getTemplateName (filePath: string, namespace: string = null): string {
 | 
						|
		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: string|string[], file: string): string|null {
 | 
						|
		if (!Array.isArray(views)) {
 | 
						|
			return views;
 | 
						|
		}
 | 
						|
 | 
						|
		let lastDir = path.resolve(file);
 | 
						|
		let dir = path.dirname(lastDir);
 | 
						|
		const absoluteViews = views.map(v => path.resolve(v));
 | 
						|
 | 
						|
		// find the closest parent
 | 
						|
		while (dir !== lastDir) {
 | 
						|
			const index = absoluteViews.indexOf(dir);
 | 
						|
			if (index >= 0) {
 | 
						|
				return views[index];
 | 
						|
			}
 | 
						|
			lastDir = dir;
 | 
						|
			dir = path.dirname(lastDir);
 | 
						|
		}
 | 
						|
 | 
						|
		// cannot resolve view
 | 
						|
		return null;
 | 
						|
	}
 | 
						|
 | 
						|
	private _resolveLayoutPath (layoutPath: string): string|null {
 | 
						|
		if (!layoutPath) {
 | 
						|
			return null;
 | 
						|
		}
 | 
						|
 | 
						|
		if (!path.extname(layoutPath)) {
 | 
						|
			layoutPath += this.extname;
 | 
						|
		}
 | 
						|
 | 
						|
		return path.resolve(this.layoutsDir || "", layoutPath);
 | 
						|
	}
 | 
						|
}
 |