squirrelly
Version:
Lightweight, fast, and powerful JS template engine. Supports helpers, filters, template inheritance
1,146 lines (1,131 loc) • 40.9 kB
JavaScript
// TODO: allow '-' to trim up until newline. Use [^\S\n\r] instead of \s
// TODO: only include trimLeft polyfill if not in ES6
/* END TYPES */
var promiseImpl = new Function('return this')().Promise;
var asyncFunc = false;
try {
asyncFunc = new Function('return (async function(){}).constructor')();
}
catch (e) {
// We shouldn't actually ever have any other errors, but...
if (!(e instanceof SyntaxError)) {
throw e;
}
}
function hasOwnProp(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
function copyProps(toObj, fromObj, notConfig) {
for (var key in fromObj) {
if (hasOwnProp(fromObj, key)) {
if (fromObj[key] != null &&
typeof fromObj[key] == 'object' &&
(key === 'storage' || key === 'prefixes') &&
!notConfig // not called from Cache.load
) {
// plugins or storage
// Note: this doesn't merge from initial config!
// Deep clone instead of assigning
// TODO: run checks on this
toObj[key] = copyProps(/*toObj[key] ||*/ {}, fromObj[key]);
}
else {
toObj[key] = fromObj[key];
}
}
}
return toObj;
}
function trimWS(str, env, wsLeft, wsRight) {
var leftTrim;
var rightTrim;
if (typeof env.autoTrim === 'string') {
leftTrim = rightTrim = env.autoTrim;
// Don't need to check if env.autoTrim is false
// Because leftTrim, rightTrim are initialized as falsy
}
else if (Array.isArray(env.autoTrim)) {
// kinda confusing
// but _}} will trim the left side of the following string
leftTrim = env.autoTrim[1];
rightTrim = env.autoTrim[0];
}
if (wsLeft || wsLeft === false) {
leftTrim = wsLeft;
}
if (wsRight || wsRight === false) {
rightTrim = wsRight;
}
if (leftTrim === 'slurp' && rightTrim === 'slurp') {
return str.trim();
}
if (leftTrim === '_' || leftTrim === 'slurp') {
// console.log('trimming left' + leftTrim)
// full slurp
// eslint-disable-next-line no-extra-boolean-cast
if (!!String.prototype.trimLeft) {
str = str.trimLeft();
}
else {
str = str.replace(/^[\s\uFEFF\xA0]+/, '');
}
}
else if (leftTrim === '-' || leftTrim === 'nl') {
// console.log('trimming left nl' + leftTrim)
// nl trim
str = str.replace(/^(?:\n|\r|\r\n)/, '');
}
if (rightTrim === '_' || rightTrim === 'slurp') {
// console.log('trimming right' + rightTrim)
// full slurp
// eslint-disable-next-line no-extra-boolean-cast
if (!!String.prototype.trimRight) {
str = str.trimRight();
}
else {
str = str.replace(/[\s\uFEFF\xA0]+$/, '');
}
}
else if (rightTrim === '-' || rightTrim === 'nl') {
// console.log('trimming right nl' + rightTrim)
// nl trim
str = str.replace(/(?:\n|\r|\r\n)$/, ''); // TODO: make sure this gets \r\n
}
return str;
}
// credit to pugjs/pug
function isValidJSIdentifier(name) {
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
}
/* END TYPES */
var Cacher = /** @class */ (function () {
function Cacher(cache) {
this.cache = cache;
}
Cacher.prototype.define = function (key, val) {
this.cache[key] = val;
};
Cacher.prototype.get = function (key) {
// string | array.
// TODO: allow array of keys to look down
// TODO: create plugin to allow referencing helpers, filters with dot notation
return this.cache[key];
};
Cacher.prototype.remove = function (key) {
delete this.cache[key];
};
Cacher.prototype.reset = function () {
this.cache = {};
};
Cacher.prototype.load = function (cacheObj) {
// TODO: this will err with deep objects and `storage` or `plugins` keys.
// Update Feb 26: EDITED so it shouldn't err
copyProps(this.cache, cacheObj, true);
};
return Cacher;
}());
function setPrototypeOf(obj, proto) {
if (Object.setPrototypeOf) {
Object.setPrototypeOf(obj, proto);
}
else {
obj.__proto__ = proto;
}
}
function SqrlErr(message) {
var err = new Error(message);
setPrototypeOf(err, SqrlErr.prototype);
return err;
}
SqrlErr.prototype = Object.create(Error.prototype, {
name: { value: 'Squirrelly Error', enumerable: false }
});
// TODO: Class transpilation adds a lot to the bundle size
function ParseErr(message, str, indx) {
var whitespace = str.slice(0, indx).split(/\n/);
var lineNo = whitespace.length;
var colNo = whitespace[lineNo - 1].length + 1;
message +=
' at line ' +
lineNo +
' col ' +
colNo +
':\n\n' +
' ' +
str.split(/\n/)[lineNo - 1] +
'\n' +
' ' +
Array(colNo).join(' ') +
'^';
throw SqrlErr(message);
}
/* END TYPES */
var asyncRegExp = /^async +/;
var templateLitReg = /`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})*}|(?!\${)[^\\`])*`/g;
var singleQuoteReg = /'(?:\\[\s\w"'\\`]|[^\n\r'\\])*?'/g;
var doubleQuoteReg = /"(?:\\[\s\w"'\\`]|[^\n\r"\\])*?"/g;
var specialCharsReg = /[.*+\-?^${}()|[\]\\]/g;
function escapeRegExp(string) {
// From MDN
return specialCharsReg.test(string)
? string.replace(specialCharsReg, '\\$&') // $& means the whole matched string
: string;
}
function parse(str, env) {
/* Adding for EJS compatibility */
if (env.rmWhitespace) {
// Code taken directly from EJS
// Have to use two separate replaces here as `^` and `$` operators don't
// work well with `\r` and empty lines don't work well with the `m` flag.
// Essentially, this replaces the whitespace at the beginning and end of
// each line and removes multiple newlines.
str = str.replace(/[\r\n]+/g, '\n').replace(/^\s+|\s+$/gm, '');
}
/* End rmWhitespace option */
templateLitReg.lastIndex = 0;
singleQuoteReg.lastIndex = 0;
doubleQuoteReg.lastIndex = 0;
var envPrefixes = env.prefixes;
var prefixes = [
envPrefixes.h,
envPrefixes.b,
envPrefixes.i,
envPrefixes.r,
envPrefixes.c,
envPrefixes.e
].reduce(function (accumulator, prefix) {
if (accumulator && prefix) {
return accumulator + '|' + escapeRegExp(prefix);
}
else if (prefix) {
// accumulator is empty
return escapeRegExp(prefix);
}
else {
// prefix and accumulator are both empty strings
return accumulator;
}
}, '');
var parseCloseReg = new RegExp('([|()]|=>)|' + // powerchars
'(\'|"|`|\\/\\*)|\\s*((\\/)?(-|_)?' + // comments, strings
escapeRegExp(env.tags[1]) +
')', 'g');
var tagOpenReg = new RegExp('([^]*?)' + escapeRegExp(env.tags[0]) + '(-|_)?\\s*(' + prefixes + ')?\\s*', 'g');
var startInd = 0;
var trimNextLeftWs = false;
function parseTag(tagOpenIndex, currentType) {
var currentObj = { f: [] };
var numParens = 0;
var currentAttribute = 'c'; // default - Valid values: 'c'=content, 'f'=filter, 'fp'=filter params, 'p'=param, 'n'=name
if (currentType === 'h' || currentType === 'b' || currentType === 'c') {
currentAttribute = 'n';
}
else if (currentType === 'r') {
currentObj.raw = true;
currentType = 'i';
}
function addAttrValue(indx) {
var valUnprocessed = str.slice(startInd, indx);
// console.log(valUnprocessed)
var val = valUnprocessed.trim();
if (currentAttribute === 'f') {
if (val === 'safe') {
currentObj.raw = true;
}
else {
if (env.async && asyncRegExp.test(val)) {
val = val.replace(asyncRegExp, '');
currentObj.f.push([val, '', true]);
}
else {
currentObj.f.push([val, '']);
}
}
}
else if (currentAttribute === 'fp') {
currentObj.f[currentObj.f.length - 1][1] += val;
}
else if (currentAttribute === 'err') {
if (val) {
var found = valUnprocessed.search(/\S/);
ParseErr('invalid syntax', str, startInd + found);
}
}
else {
// if (currentObj[currentAttribute]) { // TODO make sure no errs
// currentObj[currentAttribute] += val
// } else {
currentObj[currentAttribute] = val;
// }
}
startInd = indx + 1;
}
parseCloseReg.lastIndex = startInd;
var m;
// tslint:disable-next-line:no-conditional-assignment
while ((m = parseCloseReg.exec(str)) !== null) {
var char = m[1];
var punctuator = m[2];
var tagClose = m[3];
var slash = m[4];
var wsControl = m[5];
var i = m.index;
if (char) {
// Power character
if (char === '(') {
if (numParens === 0) {
if (currentAttribute === 'n') {
addAttrValue(i);
currentAttribute = 'p';
}
else if (currentAttribute === 'f') {
addAttrValue(i);
currentAttribute = 'fp';
}
}
numParens++;
}
else if (char === ')') {
numParens--;
if (numParens === 0 && currentAttribute !== 'c') {
// Then it's closing a filter, block, or helper
addAttrValue(i);
currentAttribute = 'err'; // Reset the current attribute
}
}
else if (numParens === 0 && char === '|') {
addAttrValue(i); // this should actually always be whitespace or empty
currentAttribute = 'f';
}
else if (char === '=>') {
addAttrValue(i);
startInd += 1; // this is 2 chars
currentAttribute = 'res';
}
}
else if (punctuator) {
if (punctuator === '/*') {
var commentCloseInd = str.indexOf('*/', parseCloseReg.lastIndex);
if (commentCloseInd === -1) {
ParseErr('unclosed comment', str, m.index);
}
parseCloseReg.lastIndex = commentCloseInd + 2; // since */ is 2 characters, and we're using indexOf rather than a RegExp
}
else if (punctuator === "'") {
singleQuoteReg.lastIndex = m.index;
var singleQuoteMatch = singleQuoteReg.exec(str);
if (singleQuoteMatch) {
parseCloseReg.lastIndex = singleQuoteReg.lastIndex;
}
else {
ParseErr('unclosed string', str, m.index);
}
}
else if (punctuator === '"') {
doubleQuoteReg.lastIndex = m.index;
var doubleQuoteMatch = doubleQuoteReg.exec(str);
if (doubleQuoteMatch) {
parseCloseReg.lastIndex = doubleQuoteReg.lastIndex;
}
else {
ParseErr('unclosed string', str, m.index);
}
}
else if (punctuator === '`') {
templateLitReg.lastIndex = m.index;
var templateLitMatch = templateLitReg.exec(str);
if (templateLitMatch) {
parseCloseReg.lastIndex = templateLitReg.lastIndex;
}
else {
ParseErr('unclosed string', str, m.index);
}
}
}
else if (tagClose) {
addAttrValue(i);
startInd = i + m[0].length;
tagOpenReg.lastIndex = startInd;
// console.log('tagClose: ' + startInd)
trimNextLeftWs = wsControl;
if (slash && currentType === 'h') {
currentType = 's';
} // TODO throw err
currentObj.t = currentType;
return currentObj;
}
}
ParseErr('unclosed tag', str, tagOpenIndex);
return currentObj; // To prevent TypeScript from erroring
}
function parseContext(parentObj, firstParse) {
parentObj.b = []; // assume there will be blocks // TODO: perf optimize this
parentObj.d = [];
var lastBlock = false;
var buffer = [];
function pushString(strng, shouldTrimRightOfString) {
if (strng) {
// if string is truthy it must be of type 'string'
// TODO: benchmark replace( /(\\|')/g, '\\$1')
strng = trimWS(strng, env, trimNextLeftWs, // this will only be false on the first str, the next ones will be null or undefined
shouldTrimRightOfString);
if (strng) {
// replace \ with \\, ' with \'
strng = strng.replace(/\\|'/g, '\\$&').replace(/\r\n|\n|\r/g, '\\n');
// we're going to convert all CRLF to LF so it doesn't take more than one replace
buffer.push(strng);
}
}
}
// Random TODO: parentObj.b doesn't need to have t: #
var tagOpenMatch;
// tslint:disable-next-line:no-conditional-assignment
while ((tagOpenMatch = tagOpenReg.exec(str)) !== null) {
var precedingString = tagOpenMatch[1];
var shouldTrimRightPrecedingString = tagOpenMatch[2];
var prefix = tagOpenMatch[3] || '';
var prefixType;
for (var key in envPrefixes) {
if (envPrefixes[key] === prefix) {
prefixType = key;
break;
}
}
pushString(precedingString, shouldTrimRightPrecedingString);
startInd = tagOpenMatch.index + tagOpenMatch[0].length;
if (!prefixType) {
ParseErr('unrecognized tag type: ' + prefix, str, startInd);
}
var currentObj = parseTag(tagOpenMatch.index, prefixType);
// ===== NOW ADD THE OBJECT TO OUR BUFFER =====
var currentType = currentObj.t;
if (currentType === 'h') {
var hName = currentObj.n || '';
if (env.async && asyncRegExp.test(hName)) {
currentObj.a = true;
currentObj.n = hName.replace(asyncRegExp, '');
}
currentObj = parseContext(currentObj); // currentObj is the parent object
buffer.push(currentObj);
}
else if (currentType === 'c') {
// tag close
if (parentObj.n === currentObj.n) {
if (lastBlock) {
// If there's a previous block
lastBlock.d = buffer;
parentObj.b.push(lastBlock);
}
else {
parentObj.d = buffer;
}
// console.log('parentObj: ' + JSON.stringify(parentObj))
return parentObj;
}
else {
ParseErr("Helper start and end don't match", str, tagOpenMatch.index + tagOpenMatch[0].length);
}
}
else if (currentType === 'b') {
// block
// TODO: make sure async stuff inside blocks are recognized
if (lastBlock) {
// If there's a previous block
lastBlock.d = buffer;
parentObj.b.push(lastBlock);
}
else {
parentObj.d = buffer;
}
var blockName = currentObj.n || '';
if (env.async && asyncRegExp.test(blockName)) {
currentObj.a = true;
currentObj.n = blockName.replace(asyncRegExp, '');
}
lastBlock = currentObj; // Set the 'lastBlock' object to the value of the current block
buffer = [];
}
else if (currentType === 's') {
var selfClosingHName = currentObj.n || '';
if (env.async && asyncRegExp.test(selfClosingHName)) {
currentObj.a = true;
currentObj.n = selfClosingHName.replace(asyncRegExp, '');
}
buffer.push(currentObj);
}
else {
buffer.push(currentObj);
}
// ===== DONE ADDING OBJECT TO BUFFER =====
}
if (firstParse) {
pushString(str.slice(startInd, str.length), false);
parentObj.d = buffer;
}
else {
throw SqrlErr('unclosed helper "' + parentObj.n + '"');
// It should have returned by now
}
return parentObj;
}
var parseResult = parseContext({ f: [] }, true);
// console.log(JSON.stringify(parseResult))
if (env.plugins) {
for (var i = 0; i < env.plugins.length; i++) {
var plugin = env.plugins[i];
if (plugin.processAST) {
parseResult.d = plugin.processAST(parseResult.d, env);
}
}
}
return parseResult.d; // Parse the very outside context
}
// import SqrlErr from './err'
/* END TYPES */
function compileToString(str, env) {
var buffer = parse(str, env);
var res = "var tR='';" +
(env.useWith ? 'with(' + env.varName + '||{}){' : '') +
compileScope(buffer, env) +
'if(cb){cb(null,tR)} return tR' +
(env.useWith ? '}' : '');
if (env.plugins) {
for (var i = 0; i < env.plugins.length; i++) {
var plugin = env.plugins[i];
if (plugin.processFnString) {
res = plugin.processFnString(res, env);
}
}
}
return res;
// TODO: is `return cb()` necessary, or could we just do `cb()`
}
function filter(str, filters) {
for (var i = 0; i < filters.length; i++) {
var name = filters[i][0];
var params = filters[i][1];
var isFilterAsync = filters[i][2];
// if (isFilterAsync && !env.async) {
// throw SqrlErr("Async filter '" + name + "' in non-async env")
// }
// Let the JS compiler do this, compile() will catch it
str = (isFilterAsync ? 'await ' : '') + "c.l('F','" + name + "')(" + str;
if (params) {
str += ',' + params;
}
str += ')';
}
return str;
}
// TODO: Use type intersections for TemplateObject, etc.
// so I don't have to make properties mandatory
function compileHelper(env, res, descendants, params, isAsync, name) {
var ret = '{exec:' +
(isAsync ? 'async ' : '') +
compileScopeIntoFunction(descendants, res, env) +
',params:[' +
params +
']';
if (name) {
ret += ",name:'" + name + "'";
}
if (isAsync) {
ret += ',async:true';
}
ret += '}';
return ret;
}
function compileBlocks(blocks, env) {
var ret = '[';
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
ret += compileHelper(env, block.res || '', block.d, block.p || '', block.a, block.n);
if (i < blocks.length) {
ret += ',';
}
}
ret += ']';
return ret;
}
function compileScopeIntoFunction(buff, res, env) {
return 'function(' + res + "){var tR='';" + compileScope(buff, env) + 'return tR}';
}
function compileScope(buff, env) {
var i = 0;
var buffLength = buff.length;
var returnStr = '';
for (i; i < buffLength; i++) {
var currentBlock = buff[i];
if (typeof currentBlock === 'string') {
var str = currentBlock;
// we know string exists
returnStr += "tR+='" + str + "';";
}
else {
var type = currentBlock.t; // h, s, e, i
var content = currentBlock.c || '';
var filters = currentBlock.f;
var name = currentBlock.n || '';
var params = currentBlock.p || '';
var res = currentBlock.res || '';
var blocks = currentBlock.b;
var isAsync = !!currentBlock.a; // !! is to booleanize it
// if (isAsync && !env.async) {
// throw SqrlErr("Async block or helper '" + name + "' in non-async env")
// }
// Let compiler do this
if (type === 'i') {
if (env.defaultFilter) {
content = "c.l('F','" + env.defaultFilter + "')(" + content + ')';
}
var filtered = filter(content, filters);
if (!currentBlock.raw && env.autoEscape) {
filtered = "c.l('F','e')(" + filtered + ')';
}
returnStr += 'tR+=' + filtered + ';';
// reference
}
else if (type === 'h') {
// helper
if (env.storage.nativeHelpers.get(name)) {
returnStr += env.storage.nativeHelpers.get(name)(currentBlock, env);
}
else {
var helperReturn = (isAsync ? 'await ' : '') +
"c.l('H','" +
name +
"')(" +
compileHelper(env, res, currentBlock.d, params, isAsync);
if (blocks) {
helperReturn += ',' + compileBlocks(blocks, env);
}
else {
helperReturn += ',[]';
}
helperReturn += ',c)';
returnStr += 'tR+=' + filter(helperReturn, filters) + ';';
}
}
else if (type === 's') {
// self-closing helper
returnStr +=
'tR+=' +
filter((isAsync ? 'await ' : '') + "c.l('H','" + name + "')({params:[" + params + ']},[],c)', filters) +
';';
}
else if (type === 'e') {
// execute
returnStr += content + '\n';
}
}
}
return returnStr;
}
function errWithBlocksOrFilters(name, blocks, // false means don't check
filters, native) {
if (blocks && blocks.length > 0) {
throw SqrlErr((native ? 'Native' : '') + "Helper '" + name + "' doesn't accept blocks");
}
if (filters && filters.length > 0) {
throw SqrlErr((native ? 'Native' : '') + "Helper '" + name + "' doesn't accept filters");
}
}
/* ASYNC LOOP FNs */
function asyncArrLoop(arr, index, fn, res, cb) {
fn(arr[index], index).then(function (val) {
res += val;
if (index === arr.length - 1) {
cb(res);
}
else {
asyncArrLoop(arr, index + 1, fn, res, cb);
}
});
}
function asyncObjLoop(obj, keys, index, fn, res, cb) {
fn(keys[index], obj[keys[index]]).then(function (val) {
res += val;
if (index === keys.length - 1) {
cb(res);
}
else {
asyncObjLoop(obj, keys, index + 1, fn, res, cb);
}
});
}
var escMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
function replaceChar(s) {
return escMap[s];
}
function XMLEscape(str) {
// To deal with XSS. Based on Escape implementations of Mustache.JS and Marko, then customized.
var newStr = String(str);
if (/[&<>"']/.test(newStr)) {
return newStr.replace(/[&<>"']/g, replaceChar);
}
else {
return newStr;
}
}
/* END TYPES */
var templates = new Cacher({});
/* ASYNC LOOP FNs */
var helpers = new Cacher({
each: function (content, blocks) {
var res = '';
var arr = content.params[0];
errWithBlocksOrFilters('each', blocks, false);
if (content.async) {
return new Promise(function (resolve) {
asyncArrLoop(arr, 0, content.exec, res, resolve);
});
}
else {
for (var i = 0; i < arr.length; i++) {
res += content.exec(arr[i], i);
}
return res;
}
},
foreach: function (content, blocks) {
var obj = content.params[0];
errWithBlocksOrFilters('foreach', blocks, false);
if (content.async) {
return new Promise(function (resolve) {
asyncObjLoop(obj, Object.keys(obj), 0, content.exec, '', resolve);
});
}
else {
var res = '';
for (var key in obj) {
if (!hasOwnProp(obj, key))
continue;
res += content.exec(key, obj[key]); // todo: check on order
}
return res;
}
},
include: function (content, blocks, config) {
errWithBlocksOrFilters('include', blocks, false);
var template = config.storage.templates.get(content.params[0]);
if (!template) {
throw SqrlErr('Could not fetch template "' + content.params[0] + '"');
}
return template(content.params[1], config);
},
extends: function (content, blocks, config) {
var data = content.params[1] || {};
data.content = content.exec();
for (var i = 0; i < blocks.length; i++) {
var currentBlock = blocks[i];
data[currentBlock.name] = currentBlock.exec();
}
var template = config.storage.templates.get(content.params[0]);
if (!template) {
throw SqrlErr('Could not fetch template "' + content.params[0] + '"');
}
return template(data, config);
},
useScope: function (content, blocks) {
errWithBlocksOrFilters('useScope', blocks, false);
return content.exec(content.params[0]);
}
});
var nativeHelpers = new Cacher({
if: function (buffer, env) {
errWithBlocksOrFilters('if', false, buffer.f, true);
var returnStr = 'if(' + buffer.p + '){' + compileScope(buffer.d, env) + '}';
if (buffer.b) {
for (var i = 0; i < buffer.b.length; i++) {
var currentBlock = buffer.b[i];
if (currentBlock.n === 'else') {
returnStr += 'else{' + compileScope(currentBlock.d, env) + '}';
}
else if (currentBlock.n === 'elif') {
returnStr += 'else if(' + currentBlock.p + '){' + compileScope(currentBlock.d, env) + '}';
}
}
}
return returnStr;
},
try: function (buffer, env) {
errWithBlocksOrFilters('try', false, buffer.f, true);
if (!buffer.b || buffer.b.length !== 1 || buffer.b[0].n !== 'catch') {
throw SqrlErr("native helper 'try' only accepts 1 block, 'catch'");
}
var returnStr = 'try{' + compileScope(buffer.d, env) + '}';
var currentBlock = buffer.b[0];
returnStr +=
'catch' +
(currentBlock.res ? '(' + currentBlock.res + ')' : '') +
'{' +
compileScope(currentBlock.d, env) +
'}';
return returnStr;
},
block: function (buffer, env) {
errWithBlocksOrFilters('block', buffer.b, buffer.f, true);
var returnStr = 'if(!' +
env.varName +
'[' +
buffer.p +
']){tR+=(' +
compileScopeIntoFunction(buffer.d, '', env) +
')()}else{tR+=' +
env.varName +
'[' +
buffer.p +
']}';
return returnStr;
}
});
var filters = new Cacher({ e: XMLEscape });
/* END TYPES */
var defaultConfig = {
varName: 'it',
autoTrim: [false, 'nl'],
autoEscape: true,
defaultFilter: false,
tags: ['{{', '}}'],
l: function (container, name) {
if (container === 'H') {
var hRet = this.storage.helpers.get(name);
if (hRet) {
return hRet;
}
else {
throw SqrlErr("Can't find helper '" + name + "'");
}
}
else if (container === 'F') {
var fRet = this.storage.filters.get(name);
if (fRet) {
return fRet;
}
else {
throw SqrlErr("Can't find filter '" + name + "'");
}
}
},
async: false,
storage: {
helpers: helpers,
nativeHelpers: nativeHelpers,
filters: filters,
templates: templates
},
prefixes: {
h: '@',
b: '#',
i: '',
r: '*',
c: '/',
e: '!'
},
cache: false,
plugins: [],
useWith: false
};
defaultConfig.l.bind(defaultConfig);
function getConfig(override, baseConfig) {
// TODO: run more tests on this
var res = {}; // Linked
copyProps(res, defaultConfig); // Creates deep clone of res, 1 layer deep
if (baseConfig) {
copyProps(res, baseConfig);
}
if (override) {
copyProps(res, override);
}
res.l.bind(res);
return res;
}
/* END TYPES */
function compile(str, env) {
var options = getConfig(env || {});
var ctor = Function; // constructor
/* ASYNC HANDLING */
// The below code is modified from mde/ejs. All credit should go to them.
if (options.async) {
// Have to use generated function for this, since in envs without support,
// it breaks in parsing
if (asyncFunc) {
ctor = asyncFunc;
}
else {
throw SqrlErr("This environment doesn't support async/await");
}
}
if (options.varName && isValidJSIdentifier(options.varName) === false) {
throw SqrlErr("options.varName must be a valid JS identifier");
}
/* END ASYNC HANDLING */
try {
return new ctor(options.varName, 'c', // SqrlConfig
'cb', // optional callback
compileToString(str, options)); // eslint-disable-line no-new-func
}
catch (e) {
if (e instanceof SyntaxError) {
throw SqrlErr('Bad template syntax\n\n' +
e.message +
'\n' +
Array(e.message.length + 1).join('=') +
'\n' +
compileToString(str, options));
}
else {
throw e;
}
}
}
var fs = require('fs');
var path = require('path');
var _BOM = /^\uFEFF/;
/* END TYPES */
/**
* Get the path to the included file from the parent file path and the
* specified path.
*
* @param {String} name specified path
* @param {String} parentfile parent file path
* @param {Boolean} [isDir=false] whether parent file path is a directory
* @return {String}
*/
function getWholeFilePath(name, parentfile, isDirectory) {
var includePath = path.resolve(isDirectory ? parentfile : path.dirname(parentfile), // returns directory the parent file is in
name // file
);
var ext = path.extname(name);
if (!ext) {
includePath += '.sqrl';
}
return includePath;
}
/**
* Get the path to the included file by Options
*
* @param {String} path specified path
* @param {Options} options compilation options
* @return {String}
*/
function getPath(path, options) {
var includePath;
var filePath;
var views = options.views;
var match = /^[A-Za-z]+:\\|^\//.exec(path);
// Abs path
if (match && match.length) {
includePath = getWholeFilePath(path.replace(/^\/*/, ''), options.root || '/', true);
}
else {
// Relative paths
// Look relative to a passed filename first
if (options.filename) {
filePath = getWholeFilePath(path, options.filename);
if (fs.existsSync(filePath)) {
includePath = filePath;
}
}
// Then look in any views directories
if (!includePath) {
if (Array.isArray(views) &&
views.some(function (v) {
filePath = getWholeFilePath(path, v, true);
return fs.existsSync(filePath);
})) {
includePath = filePath;
}
}
if (!includePath) {
throw SqrlErr('Could not find the include file "' + path + '"');
}
}
return includePath;
}
function readFile(filePath) {
return fs
.readFileSync(filePath)
.toString()
.replace(_BOM, ''); // TODO: is replacing BOM's necessary?
}
function loadFile(filePath, options) {
var config = getConfig(options);
var template = readFile(filePath);
try {
var compiledTemplate = compile(template, config);
config.storage.templates.define(config.filename, compiledTemplate);
return compiledTemplate;
}
catch (e) {
throw SqrlErr('Loading file: ' + filePath + ' failed');
}
}
// express is set like: app.engine('html', require('squirrelly').renderFile)
/* END TYPES */
/**
* Get the template from a string or a file, either compiled on-the-fly or
* read from cache (if enabled), and cache the template if needed.
*
* If `options.cache` is true, this function reads the file from
* `options.filename` so it must be set prior to calling this function.
*
* @param {Options} options compilation options
* @return {(TemplateFunction|ClientFunction)}
* Depending on the value of `options.client`, either type might be returned.
* @static
*/
function handleCache(options) {
var filename = options.filename;
if (options.cache) {
var func = options.storage.templates.get(filename);
if (func) {
return func;
}
else {
return loadFile(filename, options);
}
}
return compile(readFile(filename), options);
}
/**
* Try calling handleCache with the given options and data and call the
* callback with the result. If an error occurs, call the callback with
* the error. Used by renderFile().
*
* @param {Options} options compilation options
* @param {Object} data template data
* @param {RenderFileCallback} cb callback
* @static
*/
function tryHandleCache(options, data, cb) {
var result;
if (!cb) {
// No callback, try returning a promise
if (typeof promiseImpl === 'function') {
return new promiseImpl(function (resolve, reject) {
try {
result = handleCache(options)(data, options);
resolve(result);
}
catch (err) {
reject(err);
}
});
}
else {
throw SqrlErr("Please provide a callback function, this env doesn't support Promises");
}
}
else {
try {
handleCache(options)(data, options, cb);
}
catch (err) {
return cb(err);
}
}
}
/**
* Get the template function.
*
* If `options.cache` is `true`, then the template is cached.
*
* @param {String} path path for the specified file
* @param {Options} options compilation options
* @return {(TemplateFunction|ClientFunction)}
* Depending on the value of `options.client`, either type might be returned
* @static
*/
function includeFile(path, options) {
// the below creates a new options object, using the parent filepath of the old options object and the path
var newFileOptions = getConfig({ filename: getPath(path, options) }, options);
// TODO: make sure properties are currectly copied over
return handleCache(newFileOptions);
}
function renderFile(filename, data, cb) {
data = data || {};
var Config = getConfig(data);
// TODO: make sure above doesn't error. We do set filename down below
Config.filename = filename; // Make sure filename is right
return tryHandleCache(Config, data, cb);
}
/* END TYPES */
function includeFileHelper(content, blocks, config) {
if (blocks && blocks.length > 0) {
throw SqrlErr("Helper 'includeFile' doesn't accept blocks");
}
return includeFile(content.params[0], config)(content.params[1], config);
}
function extendsFileHelper(content, blocks, config) {
var data = content.params[1] || {};
data.content = content.exec();
for (var i = 0; i < blocks.length; i++) {
var currentBlock = blocks[i];
data[currentBlock.name] = currentBlock.exec();
}
return includeFile(content.params[0], config)(data, config);
}
/* END TYPES */
function handleCache$1(template, options) {
var templateFunc;
if (options.cache && options.name && options.storage.templates.get(options.name)) {
return options.storage.templates.get(options.name);
}
if (typeof template === 'function') {
templateFunc = template;
}
else {
templateFunc = compile(template, options);
}
if (options.cache && options.name) {
options.storage.templates.define(options.name, templateFunc);
}
return templateFunc;
}
function render(template, data, env, cb) {
var options = getConfig(env || {});
if (options.async) {
var result;
if (!cb) {
// No callback, try returning a promise
if (typeof promiseImpl === 'function') {
return new promiseImpl(function (resolve, reject) {
try {
result = handleCache$1(template, options)(data, options);
resolve(result);
}
catch (err) {
reject(err);
}
});
}
else {
throw SqrlErr("Please provide a callback function, this env doesn't support Promises");
}
}
else {
try {
handleCache$1(template, options)(data, options, cb);
}
catch (err) {
return cb(err);
}
}
}
else {
return handleCache$1(template, options)(data, options);
}
}
/* Export file stuff */
/* END TYPES */
helpers.define('includeFile', includeFileHelper);
helpers.define('extendsFile', extendsFileHelper);
export { renderFile as __express, compile, compileScope, compileScopeIntoFunction, compileToString, defaultConfig, filters, getConfig, helpers, loadFile, nativeHelpers, parse, render, renderFile, templates };
//# sourceMappingURL=squirrelly.es.js.map