Comparing version 0.4.0 to 0.5.1
164
beard.js
@@ -1,14 +0,55 @@ | ||
module.exports = function(cache = {}, lookup = path => path) { | ||
let compiledCache = {}; | ||
const fs = require('fs'); | ||
const exts = '(.brd$|.brd.html$)'; | ||
const traversy = require('traversy'); | ||
const normalize = require('path').normalize; | ||
function hash(str) { | ||
let hash = 5381; | ||
let i = str.length; | ||
while (i) hash = (hash * 33) ^ str.charCodeAt(--i); | ||
return hash >>> 0; | ||
} | ||
module.exports = function(opts = {}) { | ||
opts.cache = opts.cache != undefined ? opts.cache : true; | ||
opts.templates = opts.templates || {}; | ||
let fnCache = {}; | ||
let pathMap = {}; | ||
let iterator = 0; | ||
const Beard = function() {} | ||
const Beard = function() { | ||
if (opts.root) { | ||
const regex = new RegExp(`(^${opts.root}|.brd$|.brd.html$)`, 'g'); | ||
traversy(opts.root, exts, (path) => { | ||
const key = path.replace(regex, ''); | ||
opts.templates[key] = fs.readFileSync(path, 'utf8'); | ||
pathMap[key] = path; | ||
}); | ||
} | ||
} | ||
Beard.prototype = { | ||
render: (template, data = {}) => { | ||
render: (path, data = {}) => { | ||
iterator = 0; | ||
return compiled(template, data)(data); | ||
let context = { | ||
globals: {}, | ||
locals: [data], | ||
path: null | ||
}; | ||
return compiled(path, '/')(context); | ||
} | ||
}; | ||
function resolvePath(path, parentPath) { | ||
if (path.startsWith('/')) { | ||
return path; | ||
} else { | ||
const currentDir = parentPath.replace(/\/[^\/]+$/, ''); | ||
return normalize(`${currentDir}/${path}`); | ||
} | ||
} | ||
const exps = { | ||
@@ -18,4 +59,6 @@ extends: (/\{{extends\s\'([^}}]+?)\'\}}/g), | ||
includeFn: (/^include\((\s?\'([^\(]*?)\'\,\s?\{([^\)]*)\})\)$/g), | ||
block: (/{{block\s+(.[^}]*)}}([^]*?){{endblock}}/g), | ||
statement: (/{{\s*((?!}}).+?)\s*}}/g), | ||
block: (/^block\s+(.[^}]*)/g), | ||
blockEnd: (/^endblock$/g), | ||
encode: (/^\:(.*)/), | ||
statement: (/{{\s*([\S\s(?!}})]+?)\s*}}/g), | ||
if: (/^if\s+([^]*)$/), | ||
@@ -30,5 +73,7 @@ elseIf: (/^else\s+if\s+([^]*)$/), | ||
const parse = { | ||
include: (_, path) => `_buffer += compiled("${cache[lookup(path)]}", _data)(_data)`, | ||
includeFn: (_, __, path, data) => `_buffer += compiled("${cache[lookup(path)]}", _data)({${data}})`, | ||
block: (_, varname, content) => `{{:var ${varname} = compiled("${content}", _data)(_data)}}{{:_data["${varname}"] = ${varname}}}`, | ||
include: (_, includePath) => `_capture(compiled("${includePath}", path)(_context));`, | ||
includeFn: (_, __, includePath, data) => `_context.locals.push({${data}}); _capture(compiled("${includePath}", path)(_context)); _context.locals.pop();`, | ||
block: (_, blockname) => `_blockName = "${blockname}"; _blockCapture = "";`, | ||
blockEnd: () => 'eval(`var ${_blockName} = _blockCapture`); _context.globals[_blockName] = _blockCapture; _blockName = null;', | ||
encode: (_, statement) => `_encode(${statement});`, | ||
if: (_, statement) => `if (${statement}) {`, | ||
@@ -38,3 +83,2 @@ elseIf: (_, statement) => `} else if (${statement}) {`, | ||
end: () => '}', | ||
for: (_, key, value, object) => { | ||
@@ -44,3 +88,2 @@ if (!value) key = (value = key, 'iterator' + iterator++); | ||
}, | ||
each: (_, iter, value, array) => { | ||
@@ -58,2 +101,5 @@ if (!value) iter = (value = iter, 'iterator' + iterator++); | ||
.replace(exps.includeFn, parse.includeFn) | ||
.replace(exps.block, parse.block) | ||
.replace(exps.blockEnd, parse.blockEnd) | ||
.replace(exps.encode, parse.encode) | ||
.replace(exps.end, parse.end) | ||
@@ -66,37 +112,36 @@ .replace(exps.else, parse.else) | ||
return `"; ${(inner === prev && !/^:/.test(inner) ? ' _buffer += ' : '')} ${inner.replace(/\t|\n|\r|^:/, '')}; _buffer += "`; | ||
const middle = inner === prev && !/^:/.test(inner) | ||
? `_capture(${inner});` | ||
: inner.replace(/\t|\n|\r|^:/, ''); | ||
return `"); ${middle} _capture("`; | ||
} | ||
function compiled(str, data) { | ||
function compiled(path, parentPath) { | ||
const fullPath = resolvePath(path, parentPath); | ||
const str = opts.cache | ||
? opts.templates[fullPath] | ||
: fs.readFileSync(pathMap[fullPath], 'utf8'); | ||
let key = hash(str); | ||
if (!compiledCache[key]) { | ||
compiledCache[key] = compile(str); | ||
if (!fnCache[key]) { | ||
fnCache[key] = compile(str, fullPath); | ||
} | ||
return compiledCache[key]; | ||
return fnCache[key]; | ||
} | ||
function hash(str) { | ||
let hash = 5381; | ||
let i = str.length; | ||
function compile(str, path) { | ||
let layout = ''; | ||
while(i) { | ||
hash = (hash * 33) ^ str.charCodeAt(--i); | ||
} | ||
return hash >>> 0; | ||
} | ||
function compile(str) { | ||
let layout; | ||
str = str | ||
.replace(exps.extends, (_, path) => { | ||
layout = cache[lookup(path)]; | ||
layout = ` | ||
_context.globals.view = _buffer; | ||
_buffer = compiled("${path}", path)(_context); | ||
`; | ||
return ''; | ||
}) | ||
.replace(exps.block, parse.block) | ||
.replace(new RegExp('\\\\', 'g'), '\\\\').replace(/"/g, '\\"') | ||
.replace(exps.statement, parser) | ||
.replace(/_buffer_\s\+=\s"";/g, '') | ||
.replace(/\n/g, '\\n') | ||
@@ -106,34 +151,49 @@ .replace(/\t/g, '\\t') | ||
let fn = ` | ||
function _compiledTemplate(_data){ | ||
const fn = ` | ||
function _compiledFn(_context){ | ||
var path = "${path}"; | ||
var _buffer = ""; | ||
var _blockName; | ||
var _blockCapture; | ||
function _valForEval(val) { | ||
if (typeof val == 'function') return val.toString(); | ||
return JSON.stringify(val); | ||
function _capture(str) { | ||
if (_blockName) { | ||
_blockCapture += str; | ||
} else { | ||
_buffer += str; | ||
} | ||
} | ||
for (var prop in _data) { | ||
if (_data.hasOwnProperty(prop)) { | ||
eval("var " + prop + " = " + _valForEval(_data[prop])); | ||
function _encode(str) { | ||
_capture(str | ||
.replace(/&(?!\\w+;)/g, '&') | ||
.replace(/\</g, '<') | ||
.replace(/\>/g, '>') | ||
.replace(/\"/g, '"') | ||
.replace(/\'/g, ''') | ||
.replace(/\\//g, '/')); | ||
} | ||
for (var prop in _context.globals) { | ||
if (_context.globals.hasOwnProperty(prop)) { | ||
eval("var " + prop + " = _context.globals[prop]"); | ||
} | ||
} | ||
_buffer += "${str}"; | ||
`; | ||
if (layout) { | ||
fn += ` | ||
_data['view'] = _buffer; | ||
_buffer = compiled("${layout}", _data)(_data); | ||
`; | ||
} | ||
var _locals = _context.locals[_context.locals.length - 1]; | ||
for (var prop in _locals) { | ||
if (_locals.hasOwnProperty(prop)) { | ||
eval("var " + prop + " = _locals[prop]"); | ||
} | ||
} | ||
fn += ` | ||
_capture("${str}"); | ||
${layout} | ||
return _buffer; | ||
} | ||
`; | ||
`.replace(/_capture\(""\);(\s+)?/g, ''); | ||
try { | ||
eval(fn); | ||
return _compiledTemplate.bind(_compiledTemplate); | ||
return _compiledFn.bind(_compiledFn); | ||
} catch (e) { | ||
@@ -140,0 +200,0 @@ throw new Error(`Compilation error: ${fn}`); |
const { time } = require('brisky-performance'); | ||
const beard = require('./beard'); | ||
const benchmarkTemplate = function(name, template, beardInstance, times = 10000) { | ||
const benchmarkTemplate = function(name, path, engine, times = 10000) { | ||
let start; | ||
@@ -11,6 +11,6 @@ let elapsed; | ||
beardInstance.render(template) | ||
engine.render(path); | ||
start = time(); | ||
for(i = 0; i < times; i++) beardInstance.render(template); | ||
for(i = 0; i < times; i++) engine.render(path); | ||
elapsed = time(start); | ||
@@ -22,10 +22,16 @@ console.log(`Rendering ${times} times with caching took ${elapsed}ms to complete.`); | ||
benchmarkTemplate('Simple Content', 'some content', beard({})); | ||
benchmarkTemplate('Simple Content', 'content', beard({ | ||
templates: { | ||
'/content': 'some content' | ||
} | ||
})); | ||
benchmarkTemplate( | ||
'Page with Layout', | ||
"{{include 'page'}}", | ||
'page', | ||
beard({ | ||
'page': "{{extends 'layout'}}page content{{block header}}the header{{endblock}}", | ||
'layout': 'top of page {{header}} -- {{view}} -- the footer' | ||
templates: { | ||
'/page': "{{extends 'layout'}}page content{{block header}}the header{{endblock}}", | ||
'/layout': 'top of page {{header}} -- {{view}} -- the footer' | ||
} | ||
}) | ||
@@ -35,10 +41,9 @@ ); | ||
benchmarkTemplate( | ||
'Page with Layout with Path Lookup', | ||
"{{include 'page'}}", | ||
'Escaped Content', | ||
'escape', | ||
beard({ | ||
'/views/page': "{{extends 'layout'}}page content{{block header}}the header{{endblock}}", | ||
'/views/layout': 'top of page {{header}} -- {{view}} -- the footer' | ||
}, | ||
(path) => `/views/${path}` | ||
) | ||
templates: { | ||
'/escape': "{{:'<script>alert('this\'')</script>'}" | ||
} | ||
}) | ||
); |
{ | ||
"name": "beard", | ||
"version": "0.4.0", | ||
"version": "0.5.1", | ||
"description": "More than a mustache.", | ||
"license": "MIT", | ||
"keywords": [ | ||
@@ -19,2 +20,5 @@ "templating engine", | ||
}, | ||
"dependencies": { | ||
"traversy": "0.0.2" | ||
}, | ||
"devDependencies": { | ||
@@ -21,0 +25,0 @@ "mocha": "4.1.0", |
169
README.md
@@ -1,71 +0,162 @@ | ||
Beard | ||
=================== | ||
# Beard | ||
More than a mustache. | ||
Released under a [MIT license](http://en.wikipedia.org/wiki/MIT_License). | ||
## Features | ||
Features | ||
-------- | ||
* Clean syntax | ||
* Ability to dynamically cache templates | ||
* Cached rendered templates for faster renders | ||
Usage | ||
----- | ||
## Install | ||
### Install ### | ||
`npm install beard` | ||
### API ### | ||
## Usage | ||
``` js | ||
const data = { | ||
noun: 'beards', | ||
capitalize: str => str.charAt(0).toUpperCase() + str.slice(1) | ||
}; | ||
const Beard = require('beard'); | ||
const engine = new Beard(cache, lookup); | ||
engine.render(template, locals); | ||
const engine = new Beard({ | ||
templates: { | ||
'/example': '{{capitalize(noun)}} are itchy.' | ||
} | ||
}); | ||
const result = engine.render('/example', data); | ||
console.log(result); // returns 'Beards are itchy.' | ||
``` | ||
### Beard Constructor Arguments ### | ||
### Constructor Arguments | ||
**cache** - (object) An object literal containing your templates. | ||
**opts** (object) - An object literal with the following optional engine options: | ||
**lookup** - (function) A function that accepts the path value and can modify the path value before Beard looks up your template from the cache. E.g., `(path) => '/absolute/cached/path/${path}'`. | ||
- **templates** (object) - An object literal containing your templates index. | ||
- **root** (string) - The absolute path to the root directory where all templates are stored. If you provide a root directory, beard will create your templates cache for you. | ||
- **home** (string) - Relative path to home directory (used via `'~'` in paths, E.g. `'~/layout'`). | ||
- **cache** (boolean) - Set to `false` to disable caching of template files. Defaults to `true`. | ||
### Render Arguments ### | ||
### Render Arguments | ||
**template** - (string) A string to be parsed and populated by the view object. | ||
**path** (string) - A string to be parsed and populated by the view object. | ||
**locals** - (object) An object of data and/or methods which will populate the template string. | ||
**locals** (object) - An object of data and/or methods which will populate the template string. | ||
### Example ### | ||
## Examples | ||
``` js | ||
const templates = { | ||
'example': "{{noun}} get {{makeUpperCase('stinky')}}." | ||
}; | ||
const Beard = require('beard'); | ||
const engine = new Beard({ | ||
templates: { | ||
'/layout': 'header | {{view}} | footer', | ||
'/app/page/content': "{{extends '/layout'}}content {{include '~/component'}}", | ||
'/app/component': 'and component' | ||
}, | ||
root: '/', | ||
home: 'app/', | ||
cache: true | ||
}); | ||
const result = engine.render('/app/page/content'); | ||
console.log(result); // returns 'header | content and component | footer' | ||
const locals = { | ||
noun: "Beards", | ||
makeUpperCase: function(str){ | ||
return str.toUpperCase(); | ||
} | ||
}; | ||
``` | ||
const Beard = require('beard'); | ||
const engine = new Beard(templates); | ||
const result = engine.render("{{include 'example'}}", locals); | ||
console.log(result); // returns 'Beards get STINKY.' | ||
### include | ||
Includes a template, can optionally pass locals. | ||
``` | ||
{{include 'template'}} | ||
{{include('template', {arg: 'val', arg2: 'val2'})}} | ||
``` | ||
### More docs to come... ### | ||
### extends | ||
Extends template with a layout. Template will be accessible as "view" variable. | ||
* cache | ||
* optional cache lookup function | ||
**layout.brd.html** | ||
``` | ||
<html> | ||
<body> | ||
{{view}} | ||
</body> | ||
</html> | ||
``` | ||
### Thanks to ### | ||
and rendering: | ||
* keeto (Mark Obcena) for the parser/compiler | ||
* shinetech (Danny Brain) for syntax ideas | ||
``` | ||
{{extends 'layout'}}view content | ||
``` | ||
Returns: | ||
``` | ||
<html> | ||
<body> | ||
view content | ||
</body> | ||
</html> | ||
``` | ||
### block | ||
Makes content available as variable name. | ||
``` | ||
{{block middle}} | ||
Middle | ||
{{endblock}} | ||
Top | ||
{{middle}} | ||
Bottom | ||
``` | ||
Returns: | ||
``` | ||
Top | ||
Middle | ||
Bottom | ||
``` | ||
### conditionals | ||
``` | ||
{{if x === 1}} | ||
x is 1 | ||
{{else if x > 1}} | ||
x is greater than 1 | ||
{{else}} | ||
x is less than 1 | ||
{{end}} | ||
``` | ||
### for loop | ||
Iterate over properties in object. | ||
``` | ||
{{for key, value in object}} | ||
{{key}} = {{value}} | ||
{{end}} | ||
``` | ||
#### each loop | ||
Iterate over array. | ||
``` | ||
{{each item in array}} | ||
{{item.property}} | ||
{{end}} | ||
``` | ||
## Thanks to | ||
* keeto (Mark Obcena) for the first iteration of the parser/compiler | ||
* joeosburn (Joe Osburn) for the updated compiler, cached compiled functions, tests, and benchmarks | ||
Released under [MIT license](http://en.wikipedia.org/wiki/MIT_License). |
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
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
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
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
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
162
12462
1
6
232
2
2
+ Addedtraversy@0.0.2
+ Addedtraversy@0.0.2(transitive)