Comparing version 0.8.1 to 0.9.0
232
lib/index.js
@@ -1,36 +0,14 @@ | ||
const { normalize, resolve } = require('path'); | ||
const { bundle, parseBlocks, writeBlockFiles } = require('./bundle'); | ||
const { exps, parse } = require('./statements'); | ||
const { vdom, cleanWhitespace, escape, hash } = require('./utils'); | ||
const defaultTags = require('./tags'); | ||
const { merge } = require('merge-anything'); | ||
const fs = require('fs'); | ||
const { defaultTag, parsers } = require('./statements'); | ||
const { resolvePath, compile } = require('./compile'); | ||
class BeardError { | ||
constructor(realError, template, lineNumber, tag) { | ||
this.name = 'Beard Syntax Error'; | ||
this.message = `"{{${tag}}}" in ${template} on line ${lineNumber}`; | ||
this.lineNumber = lineNumber; | ||
this.fileName = template; | ||
this.functionName = tag; | ||
Error.captureStackTrace(this, compile); | ||
} | ||
} | ||
class Beard { | ||
constructor(opts = {}) { | ||
opts.templates = opts.templates || {}; | ||
opts.loadHandles = opts.hasOwnProperty('loadHandles') ? opts.loadHandles : true; | ||
opts.tags = opts.tags || {}; | ||
this.opts = opts; | ||
this.opts.tags = { ...defaultTag, ...opts.tags }; | ||
this.fns = {}; | ||
this.handles = {}; | ||
this.beardDir = `${this.opts.root}/../.beard`; | ||
if (this.opts.root) { | ||
bundle(this); | ||
} | ||
this.opts.tags = merge(defaultTags, this.opts.tags || {}); | ||
this.configureTags(this.opts.tags); | ||
@@ -41,47 +19,11 @@ | ||
} | ||
if (this.opts.ready) { | ||
this.opts.ready(this); | ||
} | ||
} | ||
bundleFile(path) { | ||
const regex = new RegExp('(.beard$)', 'g'); | ||
const key = path.replace(regex, ''); | ||
const contents = fs.readFileSync(path, 'utf8'); | ||
const template = /<template>[\s\S]*?<\/template>/gm.test(contents) | ||
? contents | ||
: `<template>${contents}</template>`; | ||
const original$ = vdom(template); | ||
const blocks = parseBlocks(original$, path); | ||
const $ = vdom(cleanWhitespace(original$('template').html())); | ||
const whitespaceTagsSelectors = 'pre, code, textarea'; | ||
const originalWhitespaceTags = original$(whitespaceTagsSelectors); | ||
const whitespaceTags = $(whitespaceTagsSelectors); | ||
originalWhitespaceTags.each((i, el) => { | ||
$(whitespaceTags[i]).replaceWith(el); | ||
}); | ||
const body = $.html().replace(/=\"=(=?)\"/gm, '==$1'); | ||
writeBlockFiles(blocks); | ||
if (this.opts.loadHandles && blocks.ssjs) { | ||
const handlePath = `${this.beardDir}/${blocks.ssjs.file}`; | ||
delete require.cache[require.resolve(handlePath)]; | ||
this.handles[key] = require(handlePath); | ||
} | ||
this.opts.templates[key] = body; | ||
} | ||
configureTags(tags) { | ||
exps.tag = new RegExp(`^(${Object.keys(tags).join('|')})\\\s+([^,]+)(?:,\\\s*([\\\s\\\S]*))?$`); | ||
parsers.tag = new RegExp(`^(${Object.keys(tags).join('|')})\\\s+([^,]+)(?:,\\\s*([\\\s\\\S]*))?$`); | ||
const contentTags = Object.keys(tags).filter((key) => tags[key].content).join('|'); | ||
const contentTags = Object.keys(tags).filter((key) => tags[key].content).join('|'); | ||
if (contentTags.length) { | ||
exps.contentTag = new RegExp(`^(${contentTags})\\\:content\\\s+([^,]+)(?:,\\\s*([\\\s\\\S]*))?$`); | ||
exps.endTag = new RegExp(`^end(${contentTags})$`); | ||
parsers.contentTag = new RegExp(`^(${contentTags})\\\:content\\\s+([^,]+)(?:,\\\s*([\\\s\\\S]*))?$`); | ||
parsers.endTag = new RegExp(`^end(${contentTags})$`); | ||
} | ||
@@ -91,8 +33,8 @@ } | ||
configureShortcuts(shortcuts, tags) { | ||
exps.shortcut = new RegExp(`^@(${Object.keys(shortcuts).join('|')})(?:(?:\\\s+)([\\\s\\\S]+))?$`); | ||
parsers.shortcut = new RegExp(`^@(${Object.keys(shortcuts).join('|')})(?:(?:\\\s+)([\\\s\\\S]+))?$`); | ||
const contentTags = Object.keys(shortcuts).filter((key) => tags[shortcuts[key].tag].content).join('|'); | ||
const contentTags = Object.keys(shortcuts).filter((key) => tags[shortcuts[key].tag].content).join('|'); | ||
if (contentTags.length) { | ||
exps.shortcutContent = new RegExp(`^@(${contentTags})\\\:content(?:(?:\\\s+)([\\\s\\\S]+))?$`); | ||
exps.endShortcut = new RegExp(`^end(${contentTags})$`); | ||
parsers.shortcutContent = new RegExp(`^@(${contentTags})\\\:content(?:(?:\\\s+)([\\\s\\\S]+))?$`); | ||
parsers.endShortcut = new RegExp(`^end(${contentTags})$`); | ||
} | ||
@@ -104,5 +46,7 @@ } | ||
const str = this.opts.templates[path]; | ||
if (process.env.NODE_ENV === 'development' || !this.fns[path]) { | ||
this.fns[path] = compile(str, path, this.opts.root); | ||
this.fns[path] = compile(str, path); | ||
} | ||
return this.fns[path]; | ||
@@ -112,7 +56,9 @@ } | ||
tag(name, parentPath, firstArg, data) { | ||
if (this.opts.tags[name].firstArgIsResolvedPath) firstArg = resolvePath(firstArg, parentPath, this.opts.root); | ||
if (this.opts.tags[name].firstArgIsResolvedPath) { | ||
firstArg = resolvePath(firstArg, parentPath, this.opts.root); | ||
} | ||
return this.opts.tags[name].render( | ||
firstArg, | ||
data, | ||
this.handles, | ||
this.render.bind(this), | ||
@@ -144,3 +90,4 @@ this.partial.bind(this) | ||
shortcut: this.shortcut.bind(this) | ||
} | ||
}; | ||
return compile(str, this.opts.root || '/')(context); | ||
@@ -157,3 +104,3 @@ } | ||
shortcut: this.shortcut.bind(this) | ||
} | ||
}; | ||
@@ -168,136 +115,3 @@ if (this.opts.root && !path.startsWith('/')) { | ||
function validateSyntax(templateCode, tag, lineNumber, template) { | ||
templateCode = templateCode.replace(/(\r\n|\n|\r)/gm, ''); | ||
if (templateCode.match(/^.*\{[^\}]*$/)) templateCode += '}'; // append a } to templateCode that needs it | ||
if (templateCode.match(/^(\s*)\}/)) templateCode = 'if (false) {' + templateCode; // prepend a { to templateCode that needs it | ||
try { | ||
new Function(templateCode); | ||
} catch(e) { | ||
throw new BeardError(e, template, lineNumber, tag); | ||
} | ||
} | ||
const getDir = path => path.replace(/\/[^\/]+$/, ''); | ||
const reducer = (inner, tag) => inner.replace(exps[tag], parse[tag]); | ||
const tags = [ | ||
'include', 'includeContent', 'endInclude', | ||
'block', 'blockEnd', 'put', 'encode', | ||
'comment', 'if', 'exists', 'existsNot', | ||
'elseIf', 'else', 'for', 'each', 'end', | ||
'extends', 'tag', 'contentTag', 'endTag', | ||
'shortcut', 'shortcutContent', 'endShortcut' | ||
]; | ||
function resolvePath(path, parentPath, root) { | ||
return path.startsWith('/') | ||
? path | ||
: path.startsWith('~') | ||
? resolve(root, path.replace(/^~/, '.')) | ||
: normalize(`${getDir(parentPath)}/${path}`); | ||
} | ||
function scanner(template, path) { | ||
let statements = []; | ||
const contentCompiler = (content) => statements.push(`_capture(\`${escape(content)}\`);`); | ||
const tagCompiler = (tag, lineNumber) => { | ||
const parsedStatement = parser(tag); | ||
validateSyntax(parsedStatement, tag, lineNumber, path); | ||
statements.push(parsedStatement); | ||
}; | ||
exps.statement.lastIndex = 0; | ||
let result = exps.statement.exec(template); | ||
let lastIndex = 0; | ||
let extendsResult; | ||
while (result) { | ||
const content = template.substring(lastIndex, result.index); | ||
if (content.length > 0) contentCompiler(content); | ||
const tag = result[1]; | ||
const extendsMatch = exps.extends.exec(tag); | ||
if (extendsMatch) { // hold extends until the end | ||
extendsResult = result; | ||
} else { | ||
const lineNumber = template.substring(0, result.index).split('\n').length; | ||
tagCompiler(tag, lineNumber); | ||
} | ||
lastIndex = exps.statement.lastIndex; | ||
result = exps.statement.exec(template); | ||
} | ||
if (lastIndex < template.length) { | ||
const content = template.substring(lastIndex, template.length); | ||
contentCompiler(content); | ||
} | ||
if (extendsResult) { | ||
const lineNumber = template.substring(0, extendsResult.index).split('\n').length; | ||
tagCompiler(extendsResult[1], lineNumber); | ||
} | ||
return statements; | ||
} | ||
function parser(statement) { | ||
const parsedStatement = tags.reduce(reducer, statement); | ||
return statement === parsedStatement | ||
? `_capture(${statement});` | ||
: cleanWhitespace(parsedStatement.replace(/\t|\n|\r/, '')); | ||
} | ||
function compile(str, path, root) { | ||
const templateCode = scanner(str.replace(new RegExp('\\\\', 'g'), '\\\\').replace(/"/g, '\\"'), path).join(' '); | ||
const fn = ` | ||
${cleanWhitespace(` | ||
var scopedClass = 'b-${hash(path.replace(root, ''))}'; | ||
var _currentPath = '${path}'; | ||
var _buffer = ''; | ||
var _blockNames = []; | ||
var _blockCaptures = []; | ||
var _captureArgs = []; | ||
function _capture(str) { | ||
if (_blockNames.length > 0) { | ||
_blockCaptures[_blockCaptures.length - 1] += str; | ||
} else { | ||
_buffer += str; | ||
} | ||
} | ||
function _encode(str) { | ||
_capture(str | ||
.replace(/&(?!\\w+;)/g, '&') | ||
.replace(/\</g, '<') | ||
.replace(/\>/g, '>') | ||
.replace(/\"/g, '"') | ||
.replace(/\'/g, ''') | ||
.replace(/\\//g, '/')); | ||
} | ||
for (var _prop in _context.globals) { | ||
eval('var ' + _prop + ' = _context.globals[_prop]'); | ||
} | ||
for (var i = 0; i < _context.locals.length; i++) { | ||
var _locals = _context.locals[i]; | ||
for (var _prop in _locals) { | ||
eval('var ' + _prop + ' = _locals[_prop]'); | ||
} | ||
} | ||
`)} | ||
${templateCode} | ||
return _buffer; | ||
`.replace(/_capture\(``\);(\s+)?/g, ''); | ||
try { | ||
return new Function('_context', fn); | ||
} catch (e) { | ||
throw new Error(`Compilation error: ${fn}`); | ||
} | ||
} | ||
module.exports = opts => new Beard(opts); | ||
module.exports = (opts) => new Beard(opts); |
@@ -1,5 +0,15 @@ | ||
const uniqueIterator = value => Math.random().toString().substring(2); | ||
const uid = () => Math.random().toString().substring(2); | ||
exports.exps = { | ||
const tags = [ | ||
'include', 'includeContent', 'endInclude', | ||
'block', 'blockEnd', 'put', 'encode', | ||
'comment', 'if', 'exists', 'existsNot', | ||
'elseIf', 'else', 'for', 'each', 'end', | ||
'extends', 'tag', 'contentTag', 'endTag', | ||
'shortcut', 'shortcutContent', 'endShortcut' | ||
]; | ||
const parsers = { | ||
extends: (/^extends\s(.+)$/g), | ||
@@ -32,3 +42,3 @@ include: (/^include\s+([^,]+)(?:,\s*([\s\S]*))?$/), | ||
exports.parse = { | ||
const converters = { | ||
@@ -192,4 +202,4 @@ extends: (_, path) => ` | ||
for: (_, value, key, objValue) => { | ||
if (!key) key = `_iterator_${uniqueIterator(value)}`; | ||
const obj = `_iterator_${uniqueIterator(value)}`; | ||
if (!key) key = `_iterator_${uid()}`; | ||
const obj = `_iterator_${uid()}`; | ||
return ` | ||
@@ -203,5 +213,5 @@ var ${obj} = ${objValue}; | ||
each: (_, value, iter, arrValue) => { | ||
if (!iter) iter = `_iterator_${uniqueIterator(value)}`; | ||
const length = `_iterator_${uniqueIterator(value)}`; | ||
const arr = `_iterator_${uniqueIterator(value)}`; | ||
if (!iter) iter = `_iterator_${uid()}`; | ||
const length = `_iterator_${uid()}`; | ||
const arr = `_iterator_${uid()}`; | ||
return ` | ||
@@ -213,1 +223,48 @@ for (var ${iter} = 0, ${arr} = ${arrValue}, ${length} = ${arr}.length; ${iter} < ${length}; ${iter}++) { | ||
}; | ||
const htmlSingletonTags = [ | ||
'area', 'base', 'br', 'col', | ||
'command', 'embed', 'hr', 'img', | ||
'input', 'keygen', 'link', 'meta', | ||
'param', 'source', 'track', 'wbr' | ||
]; | ||
const htmlTagsWithValueAttributes = [ | ||
'button', 'input', 'li', 'meter', | ||
'option', 'param', 'progress' | ||
]; | ||
const defaultTag = { | ||
tag: { | ||
render: (tagName, data) => { | ||
const isValueTag = htmlTagsWithValueAttributes.includes(tagName); | ||
const attributes = Object.entries(data).reduce((attrs, [key, value]) => { | ||
if ((value || typeof value === 'string') && ((key === 'value' && isValueTag) || !['content', 'value'].includes(key))) { | ||
attrs += value === true | ||
? ` ${key}` | ||
: ` ${key}="${value}"`; | ||
} | ||
return attrs; | ||
}, ''); | ||
const tag = `<${tagName}${attributes}>`; | ||
const tagContent = data.content || (!isValueTag && data.value ? data.value : ''); | ||
return htmlSingletonTags.includes(tagName) | ||
? tag | ||
: `${tag}${tagContent}</${tagName}>`; | ||
}, | ||
firstArgIsResolvedPath: false, | ||
content: true | ||
} | ||
}; | ||
exports.defaultTag = defaultTag; | ||
exports.tags = tags; | ||
exports.parsers = parsers; | ||
exports.converters = converters; |
{ | ||
"name": "beard", | ||
"version": "0.8.1", | ||
"version": "0.9.0", | ||
"description": "More than a mustache.", | ||
@@ -20,14 +20,2 @@ "license": "MIT", | ||
}, | ||
"bin": { | ||
"beard": "./cli.js" | ||
}, | ||
"dependencies": { | ||
"cheerio": "github:cheeriojs/cheerio#3368605edb3c5babecc8576602a9d54ccfdaef1e", | ||
"fs-extra": "7.0.1", | ||
"merge-anything": "~3.0.3", | ||
"mismatch": "^1.2.0", | ||
"normalize-selector": "0.2.0", | ||
"traversy": "0.0.2", | ||
"xregexp": "4.2.4" | ||
}, | ||
"devDependencies": { | ||
@@ -34,0 +22,0 @@ "mocha": "4.1.0", |
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
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
GitHub dependency
Supply chain riskContains a dependency which resolves to a GitHub URL. Dependencies fetched from GitHub specifiers are not immutable can be used to inject untrusted code or reduce the likelihood of a reproducible install.
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
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
0
0
2
22004
29
509
3
- Removedcheerio@github:cheeriojs/cheerio#3368605edb3c5babecc8576602a9d54ccfdaef1e
- Removedfs-extra@7.0.1
- Removedmerge-anything@~3.0.3
- Removedmismatch@^1.2.0
- Removednormalize-selector@0.2.0
- Removedtraversy@0.0.2
- Removedxregexp@4.2.4
- Removed@babel/runtime-corejs2@7.25.6(transitive)
- Removedcore-js@2.6.12(transitive)
- Removedfs-extra@7.0.1(transitive)
- Removedgraceful-fs@4.2.11(transitive)
- Removedis-what@3.14.1(transitive)
- Removedjsonfile@4.0.0(transitive)
- Removedmerge-anything@3.0.7(transitive)
- Removedmismatch@1.2.0(transitive)
- Removednormalize-selector@0.2.0(transitive)
- Removedregenerator-runtime@0.14.1(transitive)
- Removedtraversy@0.0.2(transitive)
- Removedts-toolbelt@6.15.5(transitive)
- Removeduniversalify@0.1.2(transitive)
- Removedxregexp@4.2.4(transitive)