twig-layout
Advanced tools
Comparing version 0.1.2 to 0.1.3
262
block.js
/*! | ||
* twig-layout | ||
* | ||
* Copyright(c) 2018 Metais Fabien | ||
* Copyright(c) 2019 Metais Fabien | ||
* MIT Licensed | ||
@@ -9,2 +9,7 @@ */ | ||
/** | ||
* | ||
* @typedef {Object} BlockConfig | ||
* @property {Array} blocks Children blocks to add | ||
*/ | ||
/** | ||
* Block Object | ||
@@ -19,27 +24,104 @@ * | ||
* | ||
* @param {string} name name | ||
* @param {string} html html template | ||
* @param {Object} config block config | ||
* @param {string} parent block parent name | ||
* @param {Layout} layout layout instance | ||
* @param {Layout} layout Layout instance | ||
* @param {Object} options Block options | ||
* @param {string} options.name Block name | ||
* @param {string} options.html Block html | ||
* @param {BlockConfig} options.config Block config | ||
* @param {string} options.parent Block parnet name | ||
* @param {string} options.template Block tamplate path | ||
* @param {Cache} options.cache Cache instance | ||
*/ | ||
constructor (name, html, config, parent, layout) { | ||
//layout instance | ||
constructor (layout, options) { | ||
/** | ||
* Layout instance | ||
* @type {Layout} | ||
*/ | ||
this.layout = layout | ||
//block parent name overwritted by the config | ||
this.parent = config.parent ? config.parent : parent | ||
/** | ||
* template path | ||
* @type {string} | ||
*/ | ||
this.template = options.template | ||
//block name | ||
this.name = name | ||
/** | ||
* Config object | ||
* @type {Object} | ||
*/ | ||
this.config = options.config | ||
/** | ||
* block parent name take from config or options | ||
* @type {string} | ||
*/ | ||
this.parent = this.config.parent ? this.config.parent : options.parent | ||
//html template | ||
this.html = html | ||
this.config = config | ||
/** | ||
* Block name | ||
* @type {string} | ||
*/ | ||
this.name = options.name | ||
/** | ||
* Block html | ||
* @type {string} | ||
*/ | ||
this.html = options.html | ||
/** | ||
* Block data object, it's the object passed to twig for render | ||
* @type {string} | ||
*/ | ||
this.data = {} | ||
this.blocks = [] | ||
this.page = config.page ? config.page : null | ||
this.extend() | ||
//bind block in data | ||
this.data.this = this; | ||
/** | ||
* Store children blocks from the config to add them after this block | ||
* @private | ||
*/ | ||
this._blocks = [] | ||
/** | ||
* Block page path | ||
* @type {string|null} | ||
*/ | ||
this.page = this.config.page ? this.config.page : null | ||
/** | ||
* Cache instance | ||
* @type {Cache} | ||
* @private | ||
*/ | ||
this._cache = options.cache | ||
/** | ||
* Cache render key | ||
* @type {string|null} | ||
* @private | ||
*/ | ||
this._cacheRenderKey = null | ||
/** | ||
* Cache render key prefix | ||
* @type {string} | ||
* @private | ||
*/ | ||
this._cacheRenderKeyPrefix = 'layout:block.render:' | ||
/** | ||
* Store the html cached of the block | ||
* @type {string} | ||
* @private | ||
*/ | ||
this._cacheRender = null | ||
/** | ||
* Cache render ttl | ||
* @type {int|null} | ||
* @private | ||
*/ | ||
this._cacheRenderTtl = null | ||
//add children block | ||
if (this.config.blocks) { | ||
@@ -54,16 +136,75 @@ this.addBlocks(this.config.blocks) | ||
*/ | ||
async init () {} | ||
async init() {} | ||
/** | ||
* AfterLoad call back | ||
* afterLoadChrildren hook | ||
* called after all blocks are loaded | ||
*/ | ||
async afterLoad () {} | ||
async afterInitChildren() {} | ||
/** | ||
* Before render callback | ||
* AfterLoad hook | ||
* called after all blocks are loaded | ||
*/ | ||
async beforeRender () {} | ||
async afterLoad() {} | ||
/** | ||
* beforeRender hook | ||
* Call before the render | ||
*/ | ||
async beforeRender() {} | ||
/** | ||
* Check if the block render is cached | ||
* @returns {Boolean} | ||
*/ | ||
async isCached() { | ||
if (!this._cache || !this.cache) { | ||
return false | ||
} else { | ||
return this._getCacheRender().then(cacheRender => { | ||
return cacheRender !== null ? true : false | ||
}) | ||
} | ||
} | ||
/** | ||
* renturn the render from the cache | ||
* @returns {String} | ||
* @private | ||
*/ | ||
async _getCacheRender() { | ||
if (this._cacheRender === null) { | ||
//get the cache | ||
const cacheRender = await this._cache.get(this._getCacheRenderKey()) | ||
//if is in cache | ||
if (cacheRender !== null && cacheRender !== undefined) { | ||
this._cacheRender = cacheRender | ||
return cacheRender | ||
} | ||
return null | ||
} | ||
return this._cacheRender | ||
} | ||
/** | ||
* Return the render cache key | ||
* | ||
* @returns {String} | ||
* @private | ||
*/ | ||
_getCacheRenderKey() { | ||
if (!this._cacheRenderKey) { | ||
if (!this.template) { | ||
throw new Error ('A block has no cacheRenderKey and no template') | ||
} else { | ||
return this._cacheRenderKeyPrefix + this.template | ||
} | ||
} else { | ||
return this._cacheRenderKeyPrefix + this._cacheRenderKey | ||
} | ||
} | ||
/** | ||
* Add many blocks | ||
@@ -74,3 +215,3 @@ * | ||
addBlocks(blocks) { | ||
for (var key in blocks) { | ||
for (const key in blocks) { | ||
this.addBlock(blocks[key]) | ||
@@ -83,11 +224,29 @@ } | ||
* All blocks are loaded by the layout | ||
* after this block instance is created | ||
* after this block init method is called | ||
* | ||
* @param {Object} block Block config | ||
* @private | ||
*/ | ||
addBlock(block) { | ||
this.blocks.push(block) | ||
this._blocks.push(block) | ||
} | ||
/** | ||
* return the children block to load after this block init | ||
*/ | ||
getChildrenBlock() { | ||
return this._blocks | ||
} | ||
/** | ||
* Get a block instance by name | ||
* | ||
* @param {string} name Block name | ||
* @return {Block} Block instance | ||
*/ | ||
getBlock(name) { | ||
return this.layout.getBlock(name) | ||
} | ||
/** | ||
* Render the block | ||
@@ -97,22 +256,45 @@ * | ||
* | ||
* @return {String} | ||
* @return {string} Block html rendered | ||
*/ | ||
async render () { | ||
await this.beforeRender() | ||
this.data.blocks = await this.layout.renderBlocks(this.name) | ||
return this.layout.renderHtml(this.html, this.data) | ||
//if cache is disable render html | ||
if (!this._cache || !this.cache) { | ||
return this._getHtml() | ||
} else { | ||
//try to get cache render | ||
const cacheRender = await this._getCacheRender() | ||
//if render is cached return it | ||
if (cacheRender !== null) { | ||
return cacheRender | ||
} else { | ||
//render html and cache it | ||
const html = await this._getHtml() | ||
let opts = {} | ||
//set ttl | ||
if (this._cacheRenderTtl) { | ||
opts.ttl = this._cacheRenderTtl | ||
} | ||
//save render in cache | ||
await this._cache.set(this._getCacheRenderKey(), html, opts) | ||
return html | ||
} | ||
} | ||
} | ||
/** | ||
* Extend the block Object from the layout config | ||
* Extend the template data Object from the layout config | ||
* | ||
* Used to define methods to use them in the Block or in the html template | ||
* Render block and return html | ||
* | ||
* @returns {string} Block html rendered | ||
* @private | ||
*/ | ||
extend () { | ||
var extend = this.layout.options.extendBlock || {} | ||
Object.assign(this, extend) | ||
var extend = this.layout.options.extendTemplate || {} | ||
Object.assign(this.data, extend) | ||
async _getHtml() { | ||
if (!this.html) { | ||
return '' | ||
} | ||
return this.layout.renderHtml(this.html, this.data).catch((error) => { | ||
this.layout.emit('error', error, { block: this.name, html: this.html }) | ||
return '' | ||
}) | ||
} | ||
@@ -119,0 +301,0 @@ } |
806
layout.js
@@ -8,67 +8,214 @@ /*! | ||
var twig = require('twig') | ||
var path = require('path') | ||
var fs = require('fs') | ||
const twig = require('twig') | ||
const stringHash = require('string-hash') | ||
var Block = require('./block') | ||
const EventEmitter = require('events') | ||
const {promisify} = require('util') | ||
const path = require('path') | ||
const fs = require('fs') | ||
const utils = require('@midgar/utils') | ||
const Block = require('./block') | ||
/** | ||
* Layout Object | ||
* | ||
* Load the blocks and render them | ||
* Cache object use standard functions you'd expect in most caches | ||
* | ||
* @typedef {Object} Cache | ||
* @property {function(string, *):undefined} set Set something in cache | ||
* @property {function(string):*} get Get something from cache | ||
* @property {function(string):*} detl Delete from cache | ||
*/ | ||
class Layout { | ||
constructor (options) { | ||
/** | ||
* Layout Class | ||
* Manage blocks | ||
*/ | ||
class Layout extends EventEmitter { | ||
/** | ||
* @param {Object} options Layout options | ||
* @param {Cache} options.cache Cache instance | ||
* @param {string} options.tmpDir temp dir to write block in file and require them | ||
* @param {string} options.views Path to the views dir | ||
*/ | ||
constructor(options) { | ||
super() | ||
//options | ||
this.options = options || {} | ||
this._options = Object.assign({ | ||
cache: false, | ||
tmpDir: null, | ||
sourceMap: false, | ||
mode: 'tmpfile', | ||
views: '', | ||
}, options) | ||
//store the block instance by name | ||
if (!this._options.tmpDir) | ||
throw new Error('No temp dir set') | ||
// | ||
/** | ||
* Store blocks instance by name | ||
* @type {Object} | ||
*/ | ||
this.blocks = {} | ||
//store the block instance by parent | ||
/** | ||
* Store the block instance by parent | ||
* @type {Object} | ||
* @private | ||
*/ | ||
this._blocks = {} | ||
//template loading state | ||
this.isLoad = false | ||
/** | ||
* Define if render a page or juste template block | ||
* @type {boolen} | ||
*/ | ||
this.renderPage = true | ||
/** | ||
* Template block instance | ||
* @type {Block} | ||
* @private | ||
*/ | ||
this._templateBlock = null | ||
/** | ||
* Cache instance | ||
* @type {Cache} | ||
* @private | ||
*/ | ||
this._cache = options.cache | ||
/** | ||
* Cache key prefix | ||
* @type {string} | ||
* @private | ||
*/ | ||
this._cacheFilePrefix = 'layout:' | ||
} | ||
/** | ||
* load a template file | ||
* Extend twig and init cache | ||
*/ | ||
async init() { | ||
await Promise.all([this._extendTwig(), this._initCache()]) | ||
} | ||
/** | ||
* Extend twig with options extendFilter and extendFunction | ||
* @private | ||
*/ | ||
async _extendTwig() { | ||
const extendFilters = this._options.extendFilters || {} | ||
const extendFunctions = this._options.extendFunctions || {} | ||
await Promise.all([this.extendTwigFilters(extendFilters),this.extendTwigFunctions(extendFunctions)]) | ||
} | ||
/** | ||
* Extend twig filters | ||
* | ||
* @param {Object} extendFilters filters object {name: filter function, ...} | ||
*/ | ||
async extendTwigFilters(extendFilters) { | ||
//list filers | ||
await utils.asyncMap(extendFilters, async (filter, name) => { | ||
//add filter | ||
this.extendTwigFilter (name, filter) | ||
}) | ||
} | ||
/** | ||
* Extend twig functions | ||
* | ||
* @param {Object} extendFilters filters object {name: filter function, ...} | ||
*/ | ||
async extendTwigFunctions(extendFunctions) { | ||
//list functions | ||
await utils.asyncMap(extendFunctions, async (fn, name) => { | ||
//add function | ||
this.extendTwigFunction (name, fn) | ||
}) | ||
this.extendTwigFunction ('getBlockHtml', (name) => this.getBlockHtml(name)) | ||
} | ||
/** | ||
* Extend twig filter | ||
* | ||
* @param {String} name filter name | ||
* @param {Function} filter filter function | ||
*/ | ||
extendTwigFilter (name, filter) { | ||
twig.extendFilter(name, filter) | ||
} | ||
/** | ||
* Extend twig function | ||
* | ||
* @param {String} name function name | ||
* @param {Function} fn function | ||
*/ | ||
extendTwigFunction (name, fn) { | ||
twig.extendFunction(name, fn) | ||
} | ||
/** | ||
* Check if the cache dir exist and create it | ||
* @private | ||
*/ | ||
async _initCache() { | ||
//if cache | ||
//check if cache dir exist | ||
const exists = await utils.asyncFileExists(this._options.tmpDir) | ||
if (!exists) { | ||
//create cache dir | ||
await utils.asyncMkdir(this._options.tmpDir) | ||
} | ||
} | ||
/** | ||
* Load a template file | ||
* | ||
* @param blocks | ||
* @param {string} template Template path relative to the views dir | ||
* @param {Object} config Block config | ||
* @param {Object} config.script Block script path | ||
*/ | ||
async loadTemplate (template, config) { | ||
config = config || {} | ||
async loadTemplate(template, config = {}) { | ||
//reset blocks arrays | ||
this._blocks = {} | ||
this.blocks = {} | ||
this.blocks = {} | ||
//path of the block script | ||
let script = config.script ? config.script : null | ||
//Load the template block | ||
var block = await this._loadBlock('', template, config, null, null) | ||
const block = await this._loadBlock('', template, config, script, null) | ||
this._templateBlock = block | ||
if (!block) { | ||
throw new Error('Cannot load the main template: ' + template) | ||
} | ||
// Check if the page is defined | ||
if (!block.page) { | ||
throw new Error( | ||
'Page is not defined in the layout config for the block ' + template) | ||
if (this.renderPage && !block.page) { | ||
throw new Error('Page is not defined in the layout config for the block ' + template) | ||
} | ||
//load the page block | ||
this._pageBlock = await this._loadBlock('page', block.page, {}, null, | ||
'root') | ||
if (this.renderPage) { | ||
//load the page block | ||
const pageBlock = await this._loadBlock('page', block.page, {}, null, 'root') | ||
//call after load callbacks | ||
await this._afterLoad() | ||
if (!pageBlock) { | ||
throw new Error('Cannot load the page template: ' + block.page) | ||
} | ||
// execute the page block actions | ||
if (this._pageBlock.config.actions) { | ||
this._processActions(this._pageBlock.config.actions) | ||
this._pageBlock = pageBlock | ||
} | ||
// execute the blocks actions | ||
if (block.config.actions) { | ||
this._processActions(block.config.actions) | ||
} | ||
//set the template is loaded | ||
this.isLoad = true | ||
await this._afterLoad() | ||
} | ||
@@ -78,4 +225,2 @@ | ||
* Call the afterLoad Callbacks on all the blocks | ||
* | ||
* @return {<void>} | ||
* @private | ||
@@ -85,10 +230,10 @@ */ | ||
//promises array | ||
var afterLoads = [] | ||
const afterLoads = [] | ||
//add the promises | ||
for (var name in this.blocks) | ||
for (const name in this.blocks) | ||
afterLoads.push(this.blocks[name].afterLoad()) | ||
//wait the end | ||
await Promise.all(afterLoads) | ||
//wait the endprivate | ||
return Promise.all(afterLoads) | ||
} | ||
@@ -101,14 +246,13 @@ | ||
* @param {string} parent block name | ||
* | ||
* @private | ||
*/ | ||
_loadBlocks (blocks, parent) { | ||
var loadBlocks = [] | ||
for (var key in blocks) { | ||
var config = blocks[key] | ||
async _loadBlocks(blocks, parent) { | ||
const loadBlocks = [] | ||
for (let key in blocks) { | ||
const config = blocks[key] | ||
/* | ||
if (!config.name) { | ||
throw new Error('A block have no name: ' + JSON.stringify(config)) | ||
} | ||
*/ | ||
if (this.blocks[config.name]) { | ||
@@ -130,3 +274,3 @@ throw new Error('The block ' + config.name + ' is already defined') | ||
//path of the block script | ||
var script = config.script ? config.script : null | ||
let script = config.script ? config.script : null | ||
@@ -153,16 +297,35 @@ //load the block | ||
*/ | ||
async _loadBlock (name, template, config, script, parent) { | ||
//if the block have no name and a template | ||
//get the name of the template file for name | ||
if (!name && template) { | ||
name = template.replace(path.extname(template), '') | ||
async _loadBlock(name, template, config, script, parent) { | ||
//check if the block have no name and no template connot load the block | ||
if (!name && !template) { | ||
let error = new Error('A block have no name and no template') | ||
this.emit('error', error, {}) | ||
return null | ||
} | ||
//check if the block have a name | ||
if (!name) { | ||
throw new Error('A block have no name and no template') | ||
let block = null | ||
try { | ||
//create block | ||
block = await this.createBlock({ name: name, template: template, config: config, script: script, parent: parent }) | ||
} catch (error) { | ||
const params = {} | ||
if (error.code != 'ENOENT') { | ||
if (name) params.block = name | ||
if (template) params.template = template | ||
if (script) params.script = script | ||
} else { | ||
if (parent) params.block = parent | ||
} | ||
this.emit('error', error, params) | ||
return null | ||
} | ||
//create the block instance | ||
var block = await this.createBlock({name: name, template: template,config: config, script: script, parent: parent}) | ||
if (!block) | ||
return null | ||
if (!block.name) { | ||
this.emit('error', 'A block have no name', {template: template, script: script}) | ||
return null | ||
} | ||
@@ -177,8 +340,2 @@ if (this._blocks[block.parent] == undefined) { | ||
//load the children blocks define in the block config | ||
if (block.config.children != undefined && | ||
Array.isArray(block.config.children)) { | ||
this._loadBlocks(block.config.children, block.name) | ||
} | ||
return block | ||
@@ -190,46 +347,141 @@ } | ||
* | ||
* @param {string} script block file path | ||
* @param {Object} options block options | ||
* @param {string} template template file path | ||
* | ||
* @return {*} | ||
* | ||
* @return {Block} | ||
* @private | ||
*/ | ||
_loadBlockClass(script) { | ||
var filePath = script + '.js' | ||
async _loadBlockClass(options, template) { | ||
let BlockClass = null | ||
const html = template && template.html ? template.html : null | ||
if (!template || !template.script) { | ||
//if the template contain no block class try to use the script | ||
if (options.script) { | ||
BlockClass = await utils.asyncRequire(options.script) | ||
} else { | ||
//if no block class found use a Block class | ||
BlockClass = Block | ||
} | ||
} else { | ||
BlockClass = template.script | ||
} | ||
//the block is in the block directory or in the path define in this.options.blocks | ||
var dirs = [] | ||
if (this.options.blocks) dirs.push(this.options.blocks) | ||
dirs.push(__dirname + '/blocks') | ||
//if the file exist require the file and return it | ||
return this._createBlockInstance(BlockClass, options, html) | ||
} | ||
for (var key in dirs) { | ||
var dir = dirs[key] | ||
/** | ||
* Create the block instance | ||
* | ||
* @param {constructor} BlockClass block constructor | ||
* @param {Object} options Block options | ||
* @param {string} options.name Block name | ||
* @param {string} options.parent Block parent name | ||
* @param {string} options.template Block template path | ||
* @param {String} html | ||
* @private | ||
*/ | ||
async _createBlockInstance(BlockClass, options, html) { | ||
try { | ||
//create the block instance | ||
const block = new BlockClass(this, { | ||
name: options.name, | ||
html: html, | ||
config: options.config || {}, | ||
parent: options.parent, | ||
template: options.template || null, | ||
cache: this._cache | ||
}) | ||
//if the file exist require the file and return it | ||
if (fs.existsSync(path.join(dir, filePath))) { | ||
return require(path.join(dir, filePath)) | ||
try { | ||
await block.init() | ||
} catch (error) { | ||
const params = {} | ||
if (options.name) params.block = options.name | ||
if (options.template) params.template = options.template | ||
if (options.script) params.script = options.script | ||
this.emit('error', 'connot init block') | ||
this.emit('error', error, params) | ||
return null | ||
} | ||
await this._afterInitBlock(block) | ||
//if block have no html and no template in his option | ||
//and have a temple on his instance load it | ||
if (!block.html && !options.template && block.template) { | ||
const template = await this._loadTemplate(block.template) | ||
if (template.html) | ||
block.html = template.html | ||
} | ||
return block | ||
} catch (error) { | ||
console.log(error) | ||
throw error | ||
} | ||
} | ||
//if no file is found | ||
throw new Error('Invalid block script ' + script) | ||
/** | ||
* Call afterInit callback and load child block | ||
* | ||
* @param {*} block | ||
*/ | ||
async _afterInitBlock(block) { | ||
if (!block) | ||
return null | ||
const childrenBlock = block.getChildrenBlock() | ||
//load children block | ||
if (childrenBlock && childrenBlock.length) { | ||
await this._loadBlocks(childrenBlock, block.name) | ||
} | ||
//after init children hook | ||
await block.afterInitChildren() | ||
} | ||
/** | ||
* load a template file and return the html and the Block class | ||
* | ||
* @param {string} template template path | ||
* | ||
* @return {Array} | ||
* @private | ||
* Put the html and the script in a cache file | ||
* @param {*} template | ||
* @param {*} content | ||
*/ | ||
_loadTemplate (template) { | ||
var html = '' | ||
var BlockClass = null | ||
//get the template content | ||
var templateContent = this.getTemplateContent(template) | ||
async _cacheTemplate (template, content) { | ||
if (!content) | ||
return null | ||
const promises = [] | ||
if (content.script) { | ||
promises.push(this._cacheFile(template + '.js', content.script)) | ||
} | ||
if (content.html) { | ||
promises.push(this._cacheFile(template + '.html', content.html)) | ||
} | ||
await Promise.all(promises) | ||
} | ||
/** | ||
* Open a template file | ||
* search for template and script tags | ||
* | ||
* return and object with html and scipt | ||
* | ||
* @param {String} template tamplate path | ||
* | ||
* @returns {Object} | ||
*/ | ||
async _loadFileTemplate(templateContent) { | ||
let html = '' | ||
let script = null | ||
if (!templateContent) | ||
return null | ||
//search for the <template> part | ||
var templateTags = templateContent.match(/<template>([\s\S]*?)<\/template>/g) | ||
const templateTags = templateContent.match(/<template>([\s\S]*?)<\/template>/g) | ||
//if there are a <template> tag | ||
@@ -239,125 +491,222 @@ if (templateTags && templateTags[0]) { | ||
html = templateTags[0].replace(/<\/?template>/g, '') | ||
//remove the <template> part of the template content | ||
var scriptContent = templateContent.substring(templateTags[0].length) | ||
templateContent = templateContent.substring(templateTags[0].length) | ||
//search for a <script> part | ||
var scriptTags = scriptContent.match(/<script>([\s\S]*?)<\/script>/g) | ||
const scriptTags = templateContent.match(/<script(?:[\s\S]*?)>([\s\S]*?)<\/script>/g) | ||
if (scriptTags && scriptTags[0]) { | ||
//get the script class and eval it | ||
var script = scriptTags[0].replace(/<\/?script>/g, '') | ||
BlockClass = eval(script) | ||
script = scriptTags[0].replace(/<\/?script(?:[\s\S]*?)>/g, '') | ||
} | ||
} | ||
//if no template tag and no script tag | ||
if (!html && !script) { | ||
return {html: templateContent} | ||
} else { | ||
//if there a no <template> part get all the content | ||
html = templateContent | ||
return {html, script} | ||
} | ||
return [html, BlockClass] | ||
} | ||
/** | ||
* Create a Block instance | ||
* load a template file and return the html and the Block class | ||
* | ||
* @param {Object} options block params | ||
* @param {string} template template path | ||
* | ||
* @return Block | ||
* @return {Array} | ||
* @private | ||
*/ | ||
async createBlock (options) { | ||
var html = '' | ||
var BlockClass = null | ||
async _loadTemplate(template) { | ||
let Block = null; | ||
if (this._options.mode == 'tmpfile') { | ||
//if use cache load template from cache if it cached | ||
Block = await this._loadTmpBlock(template) | ||
} | ||
//if the block have a templete | ||
if (options.template) { | ||
var tpl = this._loadTemplate(options.template) | ||
html = tpl[0] | ||
BlockClass = tpl[1] | ||
let html = null | ||
if (this._options.cache) { | ||
html = await this._getCacheFile(template) | ||
} | ||
if (html == null || Block === null) { | ||
if (!BlockClass) { | ||
//if the template contain no block class try to use the script | ||
if (options.script) { | ||
BlockClass = this._loadBlockClass(options.script) | ||
} else { | ||
//if no block class found use a Block class | ||
BlockClass = Block | ||
//get the template content | ||
let templateContent = await this.getTemplateContent(template) | ||
const content = await this._loadFileTemplate(templateContent) | ||
html = content.html | ||
if (Block !== null) { | ||
content.script = Block | ||
} else if (content.script) { | ||
content.script = await this._createTmpBlock(template, templateContent, content.script) | ||
} | ||
if (this._options.cache && html !== null) | ||
await this._cacheFile(template, html) | ||
return content | ||
} else { | ||
return {html, script: Block} | ||
} | ||
} | ||
//create the instance | ||
var block = new BlockClass(options.name, html, options.config || {}, options.parent, this) | ||
await block.init() | ||
/** | ||
* Create the js file in the temp folder | ||
* | ||
* @param {*} filePath | ||
* @param {*} templateContent | ||
* @param {*} content | ||
* @private | ||
*/ | ||
async _createTmpBlock(filePath, templateContent, content) { | ||
//get a hash of the file path | ||
const hash = stringHash(filePath) | ||
//temp file path | ||
const newFilePath = path.join(this._options.tmpDir, hash.toString() + '.js') | ||
// load blocks | ||
if (block.blocks) { | ||
await this._loadBlocks(block.blocks, block.name) | ||
//if source map is enable generate it and update content to load sourcemap | ||
if (this._options.sourceMap) { | ||
let sourcemap = await this._getSourceMap(templateContent, content, filePath, newFilePath) | ||
await utils.asyncWriteFile(newFilePath + '.map', sourcemap) | ||
// s = Buffer.from(s).toString('base64') | ||
content = "require('source-map-support').install({environment: 'node', hookRequire: true});" + content +"\n//# sourceMappingURL=" + newFilePath + '.map' //data:application/json;" + s | ||
} | ||
return block | ||
let Block = null | ||
if (this._options.mode == 'tmpfile') { | ||
//create tmp file | ||
await utils.asyncWriteFile(newFilePath, content) | ||
try { | ||
Block = require(newFilePath) | ||
} catch (error) { | ||
console.log(error) | ||
} | ||
} else if (this._options.mode == 'eval') { | ||
try { | ||
const nodeEval = require('node-eval'); | ||
Block = nodeEval(content, filePath) | ||
} catch (error) { | ||
console.log(error) | ||
} | ||
} | ||
return Block | ||
} | ||
/** | ||
* Return a Block instance by name | ||
* | ||
* @param name | ||
* | ||
* @returns Block | ||
* Create a source map file for the block scripts | ||
* | ||
* @param {string} templateContent Original template content | ||
* @param {string} content Script tag content | ||
* @param {string} filePath Template file path | ||
* @param {string} newFilePath Temp script file path | ||
* @private | ||
*/ | ||
getBlock (name) { | ||
if (this.blocks[name] != undefined) { | ||
return this.blocks[name] | ||
} else { | ||
throw new Error('The block ' + name + ' doesn\'t exist') | ||
} | ||
async _getSourceMap(templateContent, content, filePath, newFilePath) { | ||
//get start js part index | ||
const index = templateContent.indexOf(content) | ||
//get non js part | ||
const tempString = templateContent.substring(0, index) | ||
const lineNumber = tempString.split('\n').length | ||
const esprima = require('esprima'); | ||
const SourceMapGenerator = require('source-map').SourceMapGenerator; | ||
const map = new SourceMapGenerator({ | ||
file: filePath | ||
}) | ||
const tokens = esprima.tokenize(content, { loc: true }) | ||
let offset = lineNumber - 1 | ||
tokens.forEach(function(token) { | ||
const loc = token.loc.start | ||
map.addMapping({ | ||
source: filePath, | ||
//add lineNumber offset | ||
original: {line: loc.line + offset, column: loc.column}, | ||
generated: {line: loc.line, column: loc.column} | ||
}) | ||
}) | ||
return map.toString() | ||
} | ||
/** | ||
* Exec the actions of a layout config | ||
* Try to require a temp block module | ||
* | ||
* read the config object and exec actions | ||
* | ||
* @param config | ||
* | ||
* @param {string} filePath Original file path | ||
* @private | ||
*/ | ||
_processActions (config) { | ||
for (var name in this.blocks) { | ||
var block = this.blocks[name] | ||
if (config[block.name] != undefined) { | ||
this._execActions(block.name, config[block.name]) | ||
async _loadTmpBlock(filePath) { | ||
//get a hash of the file path | ||
const hash = stringHash(filePath) | ||
let Block = null | ||
try { | ||
Block = require(path.join(this._options.tmpDir, hash + '.js')) | ||
} catch (error) { | ||
//Prevent error if the file not exists | ||
if(error.code != 'MODULE_NOT_FOUND') { | ||
throw error | ||
} | ||
} | ||
return Block | ||
} | ||
/** | ||
* Exec the actions of a block | ||
* | ||
* @param {*} filename | ||
*/ | ||
async _getCacheFile(filename) { | ||
return this._cache.get(this._cacheFilePrefix + filename) | ||
} | ||
/** | ||
* | ||
* @param {*} filename | ||
* @param {*} content | ||
*/ | ||
async _cacheFile(filename, content) { | ||
return this._cache.set(this._cacheFilePrefix + filename, content) | ||
} | ||
/** | ||
* Create a Block instance | ||
* | ||
* @param blockName | ||
* @param actions | ||
* @param {Object} options block params | ||
* | ||
* @private | ||
* @return Block | ||
*/ | ||
_execActions (blockName, actions) { | ||
//get the block instance | ||
var block = this.getBlock(blockName) | ||
for (var key in actions) { | ||
var action = actions[key] | ||
async createBlock(options) { | ||
//if the block have a templete | ||
if (options.template) { | ||
const template = await this._loadTemplate(options.template) | ||
return this._loadBlockClass(options, template) | ||
} | ||
//if the action have no method | ||
if (!action.method) { | ||
throw new Error('Invalid action in the block ' + blockName) | ||
} | ||
return this._loadBlockClass(options) | ||
} | ||
//check of the block have the method defined | ||
if (block[action.method] == undefined) { | ||
throw new Error( | ||
'The block ' + blockName + ' (' + block.script + ') have no method ' + action.method) | ||
} | ||
//call the method | ||
if (action.args != undefined) { | ||
block[action.method].apply(block, action.args) | ||
} else { | ||
block[action.method]() | ||
} | ||
/** | ||
* Return a Block instance by name | ||
* | ||
* @param {Sting} name block name | ||
* | ||
* @returns Block | ||
*/ | ||
getBlock(name) { | ||
if (this.blocks[name] != undefined) { | ||
return this.blocks[name] | ||
} else { | ||
throw new Error('The block ' + name + ' doesn\'t exist') | ||
} | ||
@@ -374,23 +723,32 @@ } | ||
*/ | ||
async renderBlocks (parent) { | ||
//promises array | ||
var renders = [] | ||
async renderBlocks(parent) { | ||
const renders = [] | ||
for (let key in this._blocks[parent]) { | ||
const block = this._blocks[parent][key] | ||
renders[block.name] = block.render().catch(async (error) => { | ||
this.emit('error', error, { block: block.name }) | ||
//return { key: block.name, value: ''} //continue other blocks | ||
}) | ||
} | ||
return utils.objectPromises(renders) | ||
} | ||
//Add the render proomise for each child blocks | ||
for (var key in this._blocks[parent]) | ||
renders.push(this._blocks[parent][key].render()) | ||
/** | ||
* Return the block html | ||
* | ||
* @param {Sting} name block name | ||
* | ||
* @returns {String} | ||
*/ | ||
async getBlockHtml(name) { | ||
try { | ||
const block = this.getBlock(name) | ||
return block.render().catch(async (error) => { | ||
this.emit('error', error, { block: block.name }) | ||
//return { key: block.name, value: ''} //continue other blocks | ||
}) | ||
} catch (error) { | ||
//wait the result | ||
var result = await Promise.all(renders) | ||
var blocks = {} | ||
var i = 0 | ||
//add the html in an object with the block name for key | ||
for (var key in this._blocks[parent]) { | ||
var block = this._blocks[parent][key] | ||
blocks[block.name] = result[i] | ||
i++ | ||
} | ||
return blocks | ||
} | ||
@@ -406,9 +764,11 @@ | ||
*/ | ||
renderHtml (html, data) { | ||
async renderHtml(html, data) { | ||
try { | ||
return twig.twig({data: html}).render(data) | ||
} catch (e) { | ||
console.log(e) | ||
console.log(html) | ||
return '' | ||
return twig.twig({ | ||
data: html, | ||
rethrow: true, | ||
allow_async: true | ||
}).renderAsync(data) | ||
} catch (error) { | ||
this.emit('error', error, { html: html }) | ||
} | ||
@@ -425,9 +785,10 @@ } | ||
*/ | ||
renderFile (file, data) { | ||
try { | ||
//get the template content and render it | ||
return this.renderHtml(this.getTemplateContent(file), data) | ||
} catch (e) { | ||
throw new Error('Error in template ' + file) | ||
async renderFile(file, data) { | ||
const html = await this.getTemplateContent(file) | ||
if (!html) { | ||
return '' | ||
} | ||
//get the template content and render it | ||
return this.renderHtml(html, data) | ||
} | ||
@@ -443,4 +804,10 @@ | ||
*/ | ||
getTemplateContent (file) { | ||
return fs.readFileSync(path.join(this.options.views, file), 'utf8') | ||
async getTemplateContent(file) { | ||
const filePath = path.join(this._options.views, file); | ||
const exists = await utils.asyncFileExists(filePath) | ||
if (exists) { | ||
return promisify(fs.readFile)(filePath, 'utf8') | ||
} else { | ||
throw new Error ('file not found: ' + file) | ||
} | ||
} | ||
@@ -453,5 +820,22 @@ | ||
*/ | ||
render () { | ||
if (!this.isLoad) throw new Error ('Layout is not loaded') | ||
return this._pageBlock.render() | ||
async render() { | ||
//call before render hook | ||
await utils.asyncMap(this.blocks, async block => { | ||
if (block.beforeRender) { | ||
const isCached = await block.isCached() | ||
if (!isCached) { | ||
try { | ||
await block.beforeRender() | ||
} catch (error) { | ||
this.emit('error', error, { block: block.name }) | ||
} | ||
} | ||
} | ||
}) | ||
if (this.renderPage) { | ||
return this._pageBlock.render() | ||
} else { | ||
return this._templateBlock.render() | ||
} | ||
} | ||
@@ -458,0 +842,0 @@ } |
{ | ||
"name": "twig-layout", | ||
"description": "A layout system", | ||
"author": { | ||
@@ -8,7 +6,20 @@ "name": "Metais Fabien", | ||
}, | ||
"version": "0.1.2", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/metaisfabien/node-twig-layout.git" | ||
"bugs": { | ||
"url": "https://github.com/metaisfabien/node-twig-layout/issues" | ||
}, | ||
"contributors": [], | ||
"dependencies": { | ||
"@midgar/utils": "^1.0.0-alpha.1.2", | ||
"node-eval": "^2.0.0", | ||
"string-hash": "^1.1.3", | ||
"twig": "^1.13.2" | ||
}, | ||
"description": "A layout system build on top of twig js", | ||
"devDependencies": { | ||
"ink-docstrap": "^1.3.2" | ||
}, | ||
"engines": { | ||
"node": ">= 0.8.0" | ||
}, | ||
"homepage": "https://github.com/metaisfabien/node-twig-layout#readme", | ||
"keywords": [ | ||
@@ -23,14 +34,14 @@ "twig", | ||
], | ||
"license": "MIT", | ||
"main": "layout.js", | ||
"contributors": [], | ||
"maintainers": [], | ||
"license": "MIT", | ||
"dependencies": { | ||
"twig": "^1.12.0" | ||
"name": "twig-layout", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/metaisfabien/node-twig-layout.git" | ||
}, | ||
"devDependencies": {}, | ||
"engines": { | ||
"node": ">= 0.8.0" | ||
"scripts": { | ||
"build-docs": "jsdoc -c ./jsdoc.js -t ./node_modules/ink-docstrap/template -r ." | ||
}, | ||
"scripts": {} | ||
"version": "0.1.3" | ||
} |
## twig-layout | ||
! In Dev dont use this ! | ||
twig-layout is a layout system based on twig. | ||
@@ -25,19 +27,20 @@ | ||
```js | ||
var express = require('express') | ||
var layout = require('express-twig-layout') | ||
var app = express() | ||
const express = require('express') | ||
const layout = require('express-twig-layout') | ||
const app = express() | ||
//set the views directory | ||
app.set('view', './views') | ||
app.use(layout()) | ||
app.use(layout({ | ||
extendFilter: {}, | ||
extendFunction:{} | ||
})) | ||
//home route | ||
app.get('/home', function (req, res) { | ||
app.get('/home', async (req, res) => { | ||
//load the layout from the file home.html | ||
req.layout.loadTemplate('home.html').then(() => { | ||
await req.layout.loadTemplate('home.html') | ||
//send the layout html | ||
req.layout.render().then((html) => { | ||
res.send(html) | ||
}) | ||
}) | ||
const html = await req.layout.render() | ||
res.send(html) | ||
}) | ||
@@ -48,10 +51,8 @@ | ||
//load the layout from the file test.html | ||
req.layout.loadTemplate('test.html').then(() => { | ||
//Set the title of the block head | ||
req.layout.getBlock('head').data.title = 'Test page' | ||
//send the layout html | ||
req.layout.render().then((html) => { | ||
res.send(html) | ||
}) | ||
}) | ||
await req.layout.loadTemplate('test.html') | ||
//Set the title of the block head | ||
req.layout.getBlock('head').data.title = 'Test page' | ||
//send the layout html | ||
const html = await req.layout.render() | ||
res.send(html) | ||
}) | ||
@@ -76,3 +77,3 @@ | ||
<!-- block head --> | ||
{{blocks.head}} | ||
{{ getBlockHtml('head') }} | ||
<body> | ||
@@ -93,4 +94,6 @@ | ||
<main role="main"> | ||
<!-- block content --> | ||
{{blocks.content}} | ||
<h1>{{ this.getSomething() }}</h1> | ||
<!-- block content --> | ||
{{ getBlockHtml('content') }} | ||
<!-- /block content --> | ||
</main> | ||
@@ -106,7 +109,7 @@ | ||
//Require the block dependency | ||
var Block = require('node-twig-layout/block') | ||
const Block = require('node-twig-layout/block') | ||
//Block for the page | ||
class Default extends Block { | ||
init () { | ||
async init () { | ||
//set the name of the block | ||
@@ -121,9 +124,16 @@ //the name of the block can be define in this way or for other block it can be defined in the config | ||
//to use block with no html temple use type | ||
this.addBlock({name: 'content', script: 'container'}) | ||
this.addBlock({name: 'content', script: 'twig-layout/scripts/container'}) | ||
} | ||
/** | ||
* A method called in the template | ||
*/ | ||
async getSomething() { | ||
return 'something'; | ||
} | ||
/** | ||
* before render callback | ||
*/ | ||
beforeRender () { | ||
async beforeRender () { | ||
//Add a css file | ||
@@ -160,3 +170,3 @@ this.layout.getBlock('head').addCss('https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css', -10) | ||
//requite the block object | ||
var Block = require('node-twig-layout/block') | ||
const Block = require('node-twig-layout/block') | ||
@@ -168,3 +178,3 @@ //A Test class for the test page | ||
*/ | ||
init() { | ||
async init() { | ||
//unsorted array | ||
@@ -180,5 +190,5 @@ this._css = [] | ||
//add css files | ||
addCss (cssFiles, weight = 0) { | ||
async addCss (cssFiles, weight = 0) { | ||
if (Array.isArray(cssFiles)) { | ||
for (var key in cssFiles) { | ||
for (let key in cssFiles) { | ||
this._css.push({weight: weight, file: cssFiles[key]}) | ||
@@ -194,5 +204,5 @@ } | ||
//add js files to the data object | ||
addJs (jsFiles) { | ||
async addJs (jsFiles) { | ||
if (Array.isArray(jsFiles)) { | ||
for (var key in jsFiles) { | ||
for (let key in jsFiles) { | ||
this._js.push({weight: weight, file: jsFiles[key]}) | ||
@@ -210,4 +220,4 @@ } | ||
*/ | ||
beforeRender() { | ||
var sort = function(a, b) { | ||
async beforeRender() { | ||
const sort = function(a, b) { | ||
return a.weight - b.weight | ||
@@ -239,7 +249,7 @@ } | ||
//requite the block object | ||
var Block = require('node-twig-layout/block') | ||
const Block = require('node-twig-layout/block') | ||
//A Block class for the home page | ||
class Home extends Block { | ||
init () { | ||
async init () { | ||
this.page ='page/default.html' | ||
@@ -269,3 +279,3 @@ //name of the parent block of this block | ||
//requite the block object | ||
var Block = require('node-twig-layout/block') | ||
const Block = require('node-twig-layout/block') | ||
@@ -272,0 +282,0 @@ //A Test class for the test page |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 4 instances in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
3717309
55
131032
1
287
4
1
1
3
7
+ Addednode-eval@^2.0.0
+ Addedstring-hash@^1.1.3
+ Added@midgar/utils@1.0.1(transitive)
+ Addednode-eval@2.0.0(transitive)
+ Addedpath-is-absolute@1.0.1(transitive)
+ Addedstring-hash@1.1.3(transitive)
Updatedtwig@^1.13.2