You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					766 lines
				
				22 KiB
			
		
		
			
		
	
	
					766 lines
				
				22 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | const fp = require('fastify-plugin') | ||
|  | const readFile = require('fs').readFile | ||
|  | const accessSync = require('fs').accessSync | ||
|  | const existsSync = require('fs').existsSync | ||
|  | const mkdirSync = require('fs').mkdirSync | ||
|  | const readdirSync = require('fs').readdirSync | ||
|  | const resolve = require('path').resolve | ||
|  | const join = require('path').join | ||
|  | const { basename, dirname, extname } = require('path') | ||
|  | const HLRU = require('hashlru') | ||
|  | const supportedEngines = ['ejs', 'nunjucks', 'pug', 'handlebars', 'mustache', 'art-template', 'twig', 'liquid', 'dot', 'eta'] | ||
|  | 
 | ||
|  | function fastifyView (fastify, opts, next) { | ||
|  |   if (!opts.engine) { | ||
|  |     next(new Error('Missing engine')) | ||
|  |     return | ||
|  |   } | ||
|  |   const type = Object.keys(opts.engine)[0] | ||
|  |   if (supportedEngines.indexOf(type) === -1) { | ||
|  |     next(new Error(`'${type}' not yet supported, PR? :)`)) | ||
|  |     return | ||
|  |   } | ||
|  |   const charset = opts.charset || 'utf-8' | ||
|  |   const propertyName = opts.propertyName || 'view' | ||
|  |   const engine = opts.engine[type] | ||
|  |   const globalOptions = opts.options || {} | ||
|  |   const templatesDir = resolveTemplateDir(opts) | ||
|  |   const lru = HLRU(opts.maxCache || 100) | ||
|  |   const includeViewExtension = opts.includeViewExtension || false | ||
|  |   const viewExt = opts.viewExt || '' | ||
|  |   const prod = typeof opts.production === 'boolean' ? opts.production : process.env.NODE_ENV === 'production' | ||
|  |   const defaultCtx = opts.defaultContext || {} | ||
|  |   const globalLayoutFileName = opts.layout | ||
|  | 
 | ||
|  |   function templatesDirIsValid (_templatesDir) { | ||
|  |     if (Array.isArray(_templatesDir) && type !== 'nunjucks') { | ||
|  |       throw new Error('Only Nunjucks supports the "templates" option as an array') | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function layoutIsValid (_layoutFileName) { | ||
|  |     if (type !== 'dot' && type !== 'handlebars' && type !== 'ejs' && type !== 'eta') { | ||
|  |       throw new Error('Only Dot, Handlebars, EJS, and Eta support the "layout" option') | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!hasAccessToLayoutFile(_layoutFileName, getDefaultExtension(type))) { | ||
|  |       throw new Error(`unable to access template "${_layoutFileName}"`) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   try { | ||
|  |     templatesDirIsValid(templatesDir) | ||
|  | 
 | ||
|  |     if (globalLayoutFileName) { | ||
|  |       layoutIsValid(globalLayoutFileName) | ||
|  |     } | ||
|  |   } catch (error) { | ||
|  |     next(error) | ||
|  |     return | ||
|  |   } | ||
|  | 
 | ||
|  |   const dotRender = type === 'dot' ? viewDot.call(fastify, preProcessDot.call(fastify, templatesDir, globalOptions)) : null | ||
|  |   const nunjucksEnv = type === 'nunjucks' ? engine.configure(templatesDir, globalOptions) : null | ||
|  | 
 | ||
|  |   const renders = { | ||
|  |     ejs: withLayout(viewEjs, globalLayoutFileName), | ||
|  |     handlebars: withLayout(viewHandlebars, globalLayoutFileName), | ||
|  |     mustache: viewMustache, | ||
|  |     nunjucks: viewNunjucks, | ||
|  |     'art-template': viewArtTemplate, | ||
|  |     twig: viewTwig, | ||
|  |     liquid: viewLiquid, | ||
|  |     dot: withLayout(dotRender, globalLayoutFileName), | ||
|  |     eta: withLayout(viewEta, globalLayoutFileName), | ||
|  |     _default: view | ||
|  |   } | ||
|  | 
 | ||
|  |   const renderer = renders[type] ? renders[type] : renders._default | ||
|  | 
 | ||
|  |   function viewDecorator () { | ||
|  |     const args = Array.from(arguments) | ||
|  | 
 | ||
|  |     let done | ||
|  |     if (typeof args[args.length - 1] === 'function') { | ||
|  |       done = args.pop() | ||
|  |     } | ||
|  | 
 | ||
|  |     const promise = new Promise((resolve, reject) => { | ||
|  |       renderer.apply({ | ||
|  |         getHeader: () => { }, | ||
|  |         header: () => { }, | ||
|  |         send: result => { | ||
|  |           if (result instanceof Error) { | ||
|  |             reject(result) | ||
|  |             return | ||
|  |           } | ||
|  | 
 | ||
|  |           resolve(result) | ||
|  |         } | ||
|  |       }, args) | ||
|  |     }) | ||
|  | 
 | ||
|  |     if (done && typeof done === 'function') { | ||
|  |       promise.then(done.bind(null, null), done) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     return promise | ||
|  |   } | ||
|  | 
 | ||
|  |   viewDecorator.clearCache = function () { | ||
|  |     lru.clear() | ||
|  |   } | ||
|  | 
 | ||
|  |   fastify.decorate(propertyName, viewDecorator) | ||
|  | 
 | ||
|  |   fastify.decorateReply(propertyName, function () { | ||
|  |     renderer.apply(this, arguments) | ||
|  |     return this | ||
|  |   }) | ||
|  | 
 | ||
|  |   if (!fastify.hasReplyDecorator('locals')) { | ||
|  |     fastify.decorateReply('locals', null) | ||
|  | 
 | ||
|  |     fastify.addHook('onRequest', (req, reply, done) => { | ||
|  |       reply.locals = {} | ||
|  |       done() | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   function getPage (page, extension) { | ||
|  |     const pageLRU = `getPage-${page}-${extension}` | ||
|  |     let result = lru.get(pageLRU) | ||
|  | 
 | ||
|  |     if (typeof result === 'string') { | ||
|  |       return result | ||
|  |     } | ||
|  | 
 | ||
|  |     const filename = basename(page, extname(page)) | ||
|  |     result = join(dirname(page), filename + getExtension(page, extension)) | ||
|  | 
 | ||
|  |     lru.set(pageLRU, result) | ||
|  | 
 | ||
|  |     return result | ||
|  |   } | ||
|  | 
 | ||
|  |   function getDefaultExtension (type) { | ||
|  |     const mappedExtensions = { | ||
|  |       'art-template': 'art', | ||
|  |       handlebars: 'hbs', | ||
|  |       nunjucks: 'njk' | ||
|  |     } | ||
|  | 
 | ||
|  |     return viewExt || (mappedExtensions[type] || type) | ||
|  |   } | ||
|  | 
 | ||
|  |   function getExtension (page, extension) { | ||
|  |     let filextension = extname(page) | ||
|  |     if (!filextension) { | ||
|  |       filextension = '.' + getDefaultExtension(type) | ||
|  |     } | ||
|  | 
 | ||
|  |     return viewExt ? `.${viewExt}` : (includeViewExtension ? `.${extension}` : filextension) | ||
|  |   } | ||
|  | 
 | ||
|  |   function isPathExcludedMinification (currentPath, pathsToExclude) { | ||
|  |     return (pathsToExclude && Array.isArray(pathsToExclude)) ? pathsToExclude.includes(currentPath) : false | ||
|  |   } | ||
|  | 
 | ||
|  |   function useHtmlMinification (globalOpts, requestedPath) { | ||
|  |     return globalOptions.useHtmlMinifier && | ||
|  |       (typeof globalOptions.useHtmlMinifier.minify === 'function') && | ||
|  |       !isPathExcludedMinification(requestedPath, globalOptions.pathsToExcludeHtmlMinifier) | ||
|  |   } | ||
|  | 
 | ||
|  |   function getRequestedPath (fastify) { | ||
|  |     return (fastify && fastify.request) ? fastify.request.routerPath : null | ||
|  |   } | ||
|  |   // Gets template as string (or precompiled for Handlebars)
 | ||
|  |   // from LRU cache or filesystem.
 | ||
|  |   const getTemplate = function (file, callback, requestedPath) { | ||
|  |     const data = lru.get(file) | ||
|  |     if (data && prod) { | ||
|  |       callback(null, data) | ||
|  |     } else { | ||
|  |       readFile(join(templatesDir, file), 'utf-8', (err, data) => { | ||
|  |         if (err) { | ||
|  |           callback(err, null) | ||
|  |           return | ||
|  |         } | ||
|  | 
 | ||
|  |         if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |           data = globalOptions.useHtmlMinifier.minify(data, globalOptions.htmlMinifierOptions || {}) | ||
|  |         } | ||
|  |         if (type === 'handlebars') { | ||
|  |           data = engine.compile(data) | ||
|  |         } | ||
|  |         lru.set(file, data) | ||
|  |         callback(null, data) | ||
|  |       }) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // Gets partials as collection of strings from LRU cache or filesystem.
 | ||
|  |   const getPartials = function (page, { partials, requestedPath }, callback) { | ||
|  |     const cacheKey = getPartialsCacheKey(page, partials, requestedPath) | ||
|  |     const partialsObj = lru.get(cacheKey) | ||
|  |     if (partialsObj && prod) { | ||
|  |       callback(null, partialsObj) | ||
|  |     } else { | ||
|  |       let filesToLoad = Object.keys(partials).length | ||
|  |       if (filesToLoad === 0) { | ||
|  |         callback(null, {}) | ||
|  |         return | ||
|  |       } | ||
|  |       let error = null | ||
|  |       const partialsHtml = {} | ||
|  |       Object.keys(partials).forEach((key, index) => { | ||
|  |         readFile(join(templatesDir, partials[key]), 'utf-8', (err, data) => { | ||
|  |           if (err) { | ||
|  |             error = err | ||
|  |           } | ||
|  |           if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |             data = globalOptions.useHtmlMinifier.minify(data, globalOptions.htmlMinifierOptions || {}) | ||
|  |           } | ||
|  | 
 | ||
|  |           partialsHtml[key] = data | ||
|  |           if (--filesToLoad === 0) { | ||
|  |             lru.set(cacheKey, partialsHtml) | ||
|  |             callback(error, partialsHtml) | ||
|  |           } | ||
|  |         }) | ||
|  |       }) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function getPartialsCacheKey (page, partials, requestedPath) { | ||
|  |     let cacheKey = page | ||
|  | 
 | ||
|  |     for (const key of Object.keys(partials)) { | ||
|  |       cacheKey += `|${key}:${partials[key]}` | ||
|  |     } | ||
|  | 
 | ||
|  |     cacheKey += `|${requestedPath}-Partials` | ||
|  | 
 | ||
|  |     return cacheKey | ||
|  |   } | ||
|  | 
 | ||
|  |   function readCallback (that, page, data, localOptions) { | ||
|  |     return function _readCallback (err, html) { | ||
|  |       const requestedPath = getRequestedPath(that) | ||
|  | 
 | ||
|  |       if (err) { | ||
|  |         that.send(err) | ||
|  |         return | ||
|  |       } | ||
|  | 
 | ||
|  |       let compiledPage | ||
|  |       try { | ||
|  |         if ((type === 'ejs') && viewExt && !globalOptions.includer) { | ||
|  |           globalOptions.includer = (originalPath, parsedPath) => { | ||
|  |             return { | ||
|  |               filename: parsedPath || join(templatesDir, originalPath + '.' + viewExt) | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |         globalOptions.filename = join(templatesDir, page) | ||
|  |         if (localOptions) { | ||
|  |           for (const key in globalOptions) { | ||
|  |             if (!Object.prototype.hasOwnProperty.call(localOptions, key)) localOptions[key] = globalOptions[key] | ||
|  |           } | ||
|  |         } else localOptions = globalOptions | ||
|  |         compiledPage = engine.compile(html, localOptions) | ||
|  |       } catch (error) { | ||
|  |         that.send(error) | ||
|  |         return | ||
|  |       } | ||
|  |       lru.set(page, compiledPage) | ||
|  | 
 | ||
|  |       if (!that.getHeader('content-type')) { | ||
|  |         that.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |       } | ||
|  |       let cachedPage | ||
|  |       try { | ||
|  |         cachedPage = lru.get(page)(data) | ||
|  |       } catch (error) { | ||
|  |         cachedPage = error | ||
|  |       } | ||
|  |       if (type === 'ejs' && (localOptions.async ?? globalOptions.async)) { | ||
|  |         cachedPage.then(html => { | ||
|  |           if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |             html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {}) | ||
|  |           } | ||
|  |           that.send(html) | ||
|  |         }).catch(err => that.send(err)) | ||
|  |         return | ||
|  |       } | ||
|  |       if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |         cachedPage = globalOptions.useHtmlMinifier.minify(cachedPage, globalOptions.htmlMinifierOptions || {}) | ||
|  |       } | ||
|  |       that.send(cachedPage) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function preProcessDot (templatesDir, options) { | ||
|  |     // Process all templates to in memory functions
 | ||
|  |     // https://github.com/olado/doT#security-considerations
 | ||
|  |     const destinationDir = options.destination || join(__dirname, 'out') | ||
|  |     if (!existsSync(destinationDir)) { | ||
|  |       mkdirSync(destinationDir) | ||
|  |     } | ||
|  | 
 | ||
|  |     const renderer = engine.process(Object.assign( | ||
|  |       {}, | ||
|  |       options, | ||
|  |       { | ||
|  |         path: templatesDir, | ||
|  |         destination: destinationDir | ||
|  |       } | ||
|  |     )) | ||
|  | 
 | ||
|  |     // .jst files are compiled to .js files so we need to require them
 | ||
|  |     for (const file of readdirSync(destinationDir, { withFileTypes: false })) { | ||
|  |       renderer[basename(file, '.js')] = require(resolve(join(destinationDir, file))) | ||
|  |     } | ||
|  |     if (Object.keys(renderer).length === 0) { | ||
|  |       this.log.warn(`WARN: no template found in ${templatesDir}`) | ||
|  |     } | ||
|  | 
 | ||
|  |     return renderer | ||
|  |   } | ||
|  | 
 | ||
|  |   function view (page, data, opts) { | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |     // append view extension
 | ||
|  |     page = getPage(page, type) | ||
|  | 
 | ||
|  |     const toHtml = lru.get(page) | ||
|  | 
 | ||
|  |     if (toHtml && prod) { | ||
|  |       if (!this.getHeader('content-type')) { | ||
|  |         this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |       } | ||
|  |       this.send(toHtml(data)) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     readFile(join(templatesDir, page), 'utf8', readCallback(this, page, data, opts)) | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewEjs (page, data, opts) { | ||
|  |     if (opts && opts.layout) { | ||
|  |       try { | ||
|  |         layoutIsValid(opts.layout) | ||
|  |         const that = this | ||
|  |         return withLayout(viewEjs, opts.layout).call(that, page, data) | ||
|  |       } catch (error) { | ||
|  |         this.send(error) | ||
|  |         return | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  |     data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |     // append view extension
 | ||
|  |     page = getPage(page, type) | ||
|  |     const requestedPath = getRequestedPath(this) | ||
|  |     getTemplate(page, (err, template) => { | ||
|  |       if (err) { | ||
|  |         this.send(err) | ||
|  |         return | ||
|  |       } | ||
|  |       const toHtml = lru.get(page) | ||
|  |       if (toHtml && prod && (typeof (toHtml) === 'function')) { | ||
|  |         if (!this.getHeader('content-type')) { | ||
|  |           this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |         } | ||
|  |         if (opts?.async ?? globalOptions.async) { | ||
|  |           toHtml(data).then(html => { | ||
|  |             if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |               this.send(globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {})) | ||
|  |               return | ||
|  |             } | ||
|  |             this.send(html) | ||
|  |           }).catch(err => this.send(err)) | ||
|  |           return | ||
|  |         } | ||
|  |         if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |           this.send(globalOptions.useHtmlMinifier.minify(toHtml(data), globalOptions.htmlMinifierOptions || {})) | ||
|  |           return | ||
|  |         } | ||
|  |         this.send(toHtml(data)) | ||
|  |         return | ||
|  |       } | ||
|  |       readFile(join(templatesDir, page), 'utf8', readCallback(this, page, data, opts)) | ||
|  |     }, requestedPath) | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewArtTemplate (page, data) { | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  |     data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |     // Append view extension.
 | ||
|  |     page = getPage(page, 'art') | ||
|  | 
 | ||
|  |     const defaultSetting = { | ||
|  |       debug: process.env.NODE_ENV !== 'production', | ||
|  |       root: templatesDir | ||
|  |     } | ||
|  | 
 | ||
|  |     // merge engine options
 | ||
|  |     const confs = Object.assign({}, defaultSetting, globalOptions) | ||
|  | 
 | ||
|  |     function render (filename, data) { | ||
|  |       confs.filename = join(templatesDir, filename) | ||
|  |       const render = engine.compile(confs) | ||
|  |       return render(data) | ||
|  |     } | ||
|  | 
 | ||
|  |     try { | ||
|  |       const html = render(page, data) | ||
|  |       if (!this.getHeader('content-type')) { | ||
|  |         this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |       } | ||
|  |       this.send(html) | ||
|  |     } catch (error) { | ||
|  |       this.send(error) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewNunjucks (page, data) { | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  |     if (typeof globalOptions.onConfigure === 'function') { | ||
|  |       globalOptions.onConfigure(nunjucksEnv) | ||
|  |     } | ||
|  |     data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |     // Append view extension.
 | ||
|  |     page = getPage(page, 'njk') | ||
|  |     nunjucksEnv.render(page, data, (err, html) => { | ||
|  |       const requestedPath = getRequestedPath(this) | ||
|  |       if (err) return this.send(err) | ||
|  |       if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |         html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {}) | ||
|  |       } | ||
|  |       if (!this.getHeader('content-type')) { | ||
|  |         this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |       } | ||
|  |       this.send(html) | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewHandlebars (page, data, opts) { | ||
|  |     if (opts && opts.layout) { | ||
|  |       try { | ||
|  |         layoutIsValid(opts.layout) | ||
|  |         const that = this | ||
|  |         return withLayout(viewHandlebars, opts.layout).call(that, page, data) | ||
|  |       } catch (error) { | ||
|  |         this.send(error) | ||
|  |         return | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     const options = Object.assign({}, globalOptions) | ||
|  |     data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |     // append view extension
 | ||
|  |     page = getPage(page, 'hbs') | ||
|  |     const requestedPath = getRequestedPath(this) | ||
|  |     getTemplate(page, (err, template) => { | ||
|  |       if (err) { | ||
|  |         this.send(err) | ||
|  |         return | ||
|  |       } | ||
|  | 
 | ||
|  |       if (prod) { | ||
|  |         try { | ||
|  |           const html = template(data) | ||
|  |           if (!this.getHeader('content-type')) { | ||
|  |             this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |           } | ||
|  |           this.send(html) | ||
|  |         } catch (e) { | ||
|  |           this.send(e) | ||
|  |         } | ||
|  |       } else { | ||
|  |         getPartials(type, { partials: options.partials || {}, requestedPath }, (err, partialsObject) => { | ||
|  |           if (err) { | ||
|  |             this.send(err) | ||
|  |             return | ||
|  |           } | ||
|  | 
 | ||
|  |           try { | ||
|  |             Object.keys(partialsObject).forEach((name) => { | ||
|  |               engine.registerPartial(name, engine.compile(partialsObject[name])) | ||
|  |             }) | ||
|  | 
 | ||
|  |             const html = template(data) | ||
|  | 
 | ||
|  |             if (!this.getHeader('content-type')) { | ||
|  |               this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |             } | ||
|  |             this.send(html) | ||
|  |           } catch (e) { | ||
|  |             this.send(e) | ||
|  |           } | ||
|  |         }) | ||
|  |       } | ||
|  |     }, requestedPath) | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewMustache (page, data, opts) { | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     const options = Object.assign({}, opts) | ||
|  |     data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |     // append view extension
 | ||
|  |     page = getPage(page, 'mustache') | ||
|  |     const requestedPath = getRequestedPath(this) | ||
|  |     getTemplate(page, (err, templateString) => { | ||
|  |       if (err) { | ||
|  |         this.send(err) | ||
|  |         return | ||
|  |       } | ||
|  |       getPartials(page, { partials: options.partials || {}, requestedPath }, (err, partialsObject) => { | ||
|  |         if (err) { | ||
|  |           this.send(err) | ||
|  |           return | ||
|  |         } | ||
|  |         const html = engine.render(templateString, data, partialsObject) | ||
|  | 
 | ||
|  |         if (!this.getHeader('content-type')) { | ||
|  |           this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |         } | ||
|  |         this.send(html) | ||
|  |       }) | ||
|  |     }, requestedPath) | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewTwig (page, data, opts) { | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     data = Object.assign({}, defaultCtx, globalOptions, this.locals, data) | ||
|  |     // Append view extension.
 | ||
|  |     page = getPage(page, 'twig') | ||
|  |     engine.renderFile(join(templatesDir, page), data, (err, html) => { | ||
|  |       const requestedPath = getRequestedPath(this) | ||
|  |       if (err) { | ||
|  |         return this.send(err) | ||
|  |       } | ||
|  |       if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |         html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {}) | ||
|  |       } | ||
|  |       if (!this.getHeader('content-type')) { | ||
|  |         this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |       } | ||
|  |       this.send(html) | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewLiquid (page, data, opts) { | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |     // Append view extension.
 | ||
|  |     page = getPage(page, 'liquid') | ||
|  | 
 | ||
|  |     engine.renderFile(join(templatesDir, page), data, opts) | ||
|  |       .then((html) => { | ||
|  |         const requestedPath = getRequestedPath(this) | ||
|  |         if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |           html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {}) | ||
|  |         } | ||
|  |         if (!this.getHeader('content-type')) { | ||
|  |           this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |         } | ||
|  |         this.send(html) | ||
|  |       }) | ||
|  |       .catch((err) => { | ||
|  |         this.send(err) | ||
|  |       }) | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewDot (renderModule) { | ||
|  |     return function _viewDot (page, data, opts) { | ||
|  |       if (opts && opts.layout) { | ||
|  |         try { | ||
|  |           layoutIsValid(opts.layout) | ||
|  |           const that = this | ||
|  |           return withLayout(dotRender, opts.layout).call(that, page, data) | ||
|  |         } catch (error) { | ||
|  |           this.send(error) | ||
|  |           return | ||
|  |         } | ||
|  |       } | ||
|  |       if (!page) { | ||
|  |         this.send(new Error('Missing page')) | ||
|  |         return | ||
|  |       } | ||
|  |       data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |       let html = renderModule[page](data) | ||
|  |       const requestedPath = getRequestedPath(this) | ||
|  |       if (useHtmlMinification(globalOptions, requestedPath)) { | ||
|  |         html = globalOptions.useHtmlMinifier.minify(html, globalOptions.htmlMinifierOptions || {}) | ||
|  |       } | ||
|  |       if (!this.getHeader('content-type')) { | ||
|  |         this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |       } | ||
|  |       this.send(html) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function viewEta (page, data, opts) { | ||
|  |     if (opts && opts.layout) { | ||
|  |       try { | ||
|  |         layoutIsValid(opts.layout) | ||
|  |         const that = this | ||
|  |         return withLayout(viewEta, opts.layout).call(that, page, data) | ||
|  |       } catch (error) { | ||
|  |         this.send(error) | ||
|  |         return | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!page) { | ||
|  |       this.send(new Error('Missing page')) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     lru.define = lru.set | ||
|  |     engine.configure({ | ||
|  |       templates: globalOptions.templates ? globalOptions.templates : lru | ||
|  |     }) | ||
|  | 
 | ||
|  |     const config = Object.assign({ | ||
|  |       cache: prod, | ||
|  |       views: templatesDir | ||
|  |     }, globalOptions) | ||
|  | 
 | ||
|  |     const sendContent = html => { | ||
|  |       if ( | ||
|  |         config.useHtmlMinifier && | ||
|  |         typeof config.useHtmlMinifier.minify === 'function' && | ||
|  |         !isPathExcludedMinification(getRequestedPath(this), config.pathsToExcludeHtmlMinifier) | ||
|  |       ) { | ||
|  |         html = config.useHtmlMinifier.minify( | ||
|  |           html, | ||
|  |           config.htmlMinifierOptions || {} | ||
|  |         ) | ||
|  |       } | ||
|  |       this.header('Content-Type', 'text/html; charset=' + charset) | ||
|  |       this.send(html) | ||
|  |     } | ||
|  | 
 | ||
|  |     data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |     // Append view extension (Eta will append '.eta' by default,
 | ||
|  |     // but this also allows custom extensions)
 | ||
|  |     page = getPage(page, 'eta') | ||
|  |     if (opts?.async ?? globalOptions.async) { | ||
|  |       engine | ||
|  |         .renderFile(page, data, config) | ||
|  |         .then(sendContent) | ||
|  |         .catch(err => this.send(err)) | ||
|  |     } else { | ||
|  |       engine.renderFile(page, data, config, (err, html) => { | ||
|  |         err | ||
|  |           ? this.send(err) | ||
|  |           : sendContent(html) | ||
|  |       }) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (prod && type === 'handlebars' && globalOptions.partials) { | ||
|  |     getPartials(type, { partials: globalOptions.partials || {}, requestedPath: getRequestedPath(this) }, (err, partialsObject) => { | ||
|  |       if (err) { | ||
|  |         next(err) | ||
|  |         return | ||
|  |       } | ||
|  |       Object.keys(partialsObject).forEach((name) => { | ||
|  |         engine.registerPartial(name, engine.compile(partialsObject[name])) | ||
|  |       }) | ||
|  |       next() | ||
|  |     }) | ||
|  |   } else { | ||
|  |     next() | ||
|  |   } | ||
|  | 
 | ||
|  |   function withLayout (render, layout) { | ||
|  |     if (layout) { | ||
|  |       return function (page, data, opts) { | ||
|  |         if (opts && opts.layout) throw new Error('A layout can either be set globally or on render, not both.') | ||
|  |         const that = this | ||
|  |         data = Object.assign({}, defaultCtx, this.locals, data) | ||
|  |         render.call({ | ||
|  |           getHeader: () => { }, | ||
|  |           header: () => { }, | ||
|  |           send: (result) => { | ||
|  |             if (result instanceof Error) { | ||
|  |               throw result | ||
|  |             } | ||
|  | 
 | ||
|  |             data = Object.assign((data || {}), { body: result }) | ||
|  |             render.call(that, layout, data, opts) | ||
|  |           } | ||
|  |         }, page, data, opts) | ||
|  |       } | ||
|  |     } | ||
|  |     return render | ||
|  |   } | ||
|  | 
 | ||
|  |   function resolveTemplateDir (_opts) { | ||
|  |     if (_opts.root) { | ||
|  |       return _opts.root | ||
|  |     } | ||
|  | 
 | ||
|  |     return Array.isArray(_opts.templates) | ||
|  |       ? _opts.templates.map((dir) => resolve(dir)) | ||
|  |       : resolve(_opts.templates || './') | ||
|  |   } | ||
|  | 
 | ||
|  |   function hasAccessToLayoutFile (fileName, ext) { | ||
|  |     try { | ||
|  |       accessSync(join(templatesDir, getPage(fileName, ext))) | ||
|  | 
 | ||
|  |       return true | ||
|  |     } catch (e) { | ||
|  |       return false | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = fp(fastifyView, { | ||
|  |   fastify: '4.x', | ||
|  |   name: '@fastify/view' | ||
|  | }) | ||
|  | module.exports.default = fastifyView | ||
|  | module.exports.fastifyView = fastifyView |