'use strict'
/*
* This file is part of "Lassi".
* Copyright 2009-2014, arNuméral
* Author : Yoran Brault
* eMail : yoran.brault@arnumeral.fr
* Site : http://arnumeral.fr
*
* "Lassi" is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* "Lassi" is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with "Lassi"; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
const pathlib = require('path')
const fs = require('fs')
const _ = require('lodash')
const { hasProp } = require('sesajstools')
// pas vraiment la peine de le require, c'est là pour montrer qu'on en a besoin
// car dustjs-helpers en a besoin mais ce n'est que dans ses devDependencies
require('dustjs-linkedin')
const dust = require('dustjs-helpers')
/**
* Le moteur de rendu html, accessible via lassi.transports.html.engine
* @param options
* @constructor Renderer
*/
function Renderer (options) {
this.cache = true
this.cacheStore = {}
this.keepWhiteSpace = false
for (const key in options) this[key] = options[key]
this.dust = dust
}
/**
* Permet d'ajouter un helper dust
* @see http://www.dustjs.com/guides/dust-helpers/
* @param {string} name
* @param {function} callback La callback (cf doc pour les arguments)
*/
Renderer.prototype.helper = function (name, callback) {
const self = this
this.dust.helpers[name] = function () {
callback.apply(self.dust, Array.prototype.slice.call(arguments))
}
}
/**
* Permet d'ajouter un filtre dust
* @see http://www.dustjs.com/docs/filter-api/
* @param {string} name le nom du filtre (à utiliser dans les templates avec |monFiltre)
* @param {function} callback La callback qui reçoit la valeur et devra la retournée filtrée (contexte dust)
*/
Renderer.prototype.addFilter = function (name, callback) {
const self = this
this.dust.filters[name] = function (value) {
return callback.call(self.dust, value)
}
}
/**
* Récupère le chemin absolu du template et le passe à callback
* @param viewsPath
* @param unresolvedPath
* @param locals
* @param callback
*/
Renderer.prototype.resolveTemplate = function (viewsPath, unresolvedPath, locals, callback) {
// Normalize
let path = unresolvedPath
// ajout de l'extension si elle n'y est pas
if (pathlib.extname(path) === '') path += '.dust'
// on autorise les chemins absolus, sinon c'est relatif a viewsPath
// @see https://nodejs.org/api/path.html#path_path_resolve_from_to
if (path.charAt(0) !== '/') {
if (!_.isString(viewsPath)) throw new Error('Wrong views path', viewsPath)
path = pathlib.resolve(viewsPath, path)
}
// Check if path exists
fs.lstat(path, function (err) {
callback(err, path)
})
}
/**
* Lit un template et le passe à callback
* @param viewsPath
* @param unresolvedPath
* @param locals
* @param callback
*/
Renderer.prototype.readTemplate = function (viewsPath, unresolvedPath, locals, callback) {
const self = this
if (self.cache && self.cacheStore[unresolvedPath]) {
callback(null, self.cacheStore[unresolvedPath])
} else {
self.resolveTemplate(viewsPath, unresolvedPath, locals, function (err, path) {
if (err) { callback(err); return }
fs.readFile(path, 'utf8', function (err, res) {
if (err) { callback(err); return }
if (self.cache) self.cacheStore[unresolvedPath] = res
callback(null, res)
})
})
}
}
/**
* Calcule le rendu et le passe à callback
* @param viewsPath
* @param unresolvedPath
* @param locals
* @param callback
*/
Renderer.prototype.render = function (viewsPath, unresolvedPath, locals, callback) {
const self = this
let template = (this.cache && this.cacheStore[unresolvedPath]) || null
if (template) {
template(locals, callback)
} else {
self.dust.onLoad = function (path, callback) {
// lassi.settings.application.partialsPath peut être une chaîne vide
let partialsPath
if (hasProp(lassi.settings.application, 'partialsPath')) partialsPath = lassi.settings.application.partialsPath
else partialsPath = '/partials'
self.readTemplate(viewsPath + partialsPath, path, locals, callback)
}
if (unresolvedPath) {
this.resolveTemplate(viewsPath, unresolvedPath, locals, function (err, path) {
if (err) { callback(err); return }
fs.readFile(path, 'utf8', function (err, str) {
if (err) {
console.error(err)
return callback(new Error(`La vue ${path} n’existe pas`))
}
template = self.dust.compileFn(str)
if (self.cache) self.cacheStore[unresolvedPath] = template
template(locals, callback)
})
})
} else {
callback(new Error('render appelé sans path à résoudre'))
}
}
}
// @see https://github.com/linkedin/dustjs/wiki/Dust-Tutorial#Controlling_whitespace_suppression
/**
* Un dust.optimizers.format qui ne fait rien (pour conserver les espaces)
* @private
* @param ctx
* @param node
* @returns {*} node tel quel
*/
Renderer.prototype.whiteSpaceKeeper = function (ctx, node) { return node }
/**
* Désactive la suppression des espaces
*/
Renderer.prototype.disableWhiteSpaceCompression = function () {
if (this.dust.optimizers.format !== this.whiteSpaceKeeper) {
this.originalFormat = this.dust.optimizers.format
this.dust.optimizers.format = this.whiteSpaceKeeper
}
}
/**
* Active la suppression des espaces
*/
Renderer.prototype.enableWhiteSpaceCompression = function () {
if (this.originalFormat) {
this.dust.optimizers.format = this.originalFormat
this.originalFormat = null
}
}
module.exports = Renderer