squirrelly
Version:
Simple and powerful template engine that supports helpers, partials, filters, native code, and Express.
564 lines (523 loc) • 19.2 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.Sqrl = {}));
}(this, function (exports) { 'use strict';
var helpers = {
// No helpers are included by default for the sake of size,
// But there's an example of a helper below
/*
Date: function (args, content, blocks, options) {
var today = new Date()
var dd = today.getDate()
var mm = today.getMonth() + 1 // January is 0!
var yyyy = today.getFullYear()
if (dd < 10) {
dd = '0' + dd
}
if (mm < 10) {
mm = '0' + mm
}
today = mm + '/' + dd + '/' + yyyy
return today
} */
};
var Partials = {/*
partialName: "partialString"
*/};
var initialRegEx = /{{ *?(?:(?:(?:(?:([\w$]+ *?(?:[^\s\w($][^\n]*?)*?))|(?:@(?:([\w$]+:|(?:\.\.\/)+))? *(.+?) *))(?: *?(\| *?[\w$]+? *?)+?)?)|(?:([\w$]+) *?\(([^\n]*?)\) *?([\w$]*))|(?:\/ *?([\w$]+))|(?:# *?([\w$]+))|(?:([\w$]+) *?\(([^\n]*?)\) *?\/)|(?:!--[^]+?--)) *?}}\n?/g;
var initialTags = {
s: '{{',
e: '}}'
};
// The regExp below matches all helper references inside helper parameters
var paramHelperRefRegExp = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[\\]@(?:[\w$]*:)?[\w$]+|@(?:([\w$]*):)?([\w$]+)/g;
var regEx = initialRegEx;
var tags = initialTags;
function setup () { // Resets the current tags to the default tags
tags = initialTags;
regEx = initialRegEx;
regEx.lastIndex = 0;
}
function defaultTags (tagArray) { // Redefine the default tags of the regexp
changeTags(tagArray[0], tagArray[1]);
initialRegEx = regEx;
initialTags = tags;
}
function changeTags (firstTag, secondTag) { // Update current tags
var newRegEx = firstTag + regEx.source.slice(tags.s.length, 0 - (tags.e.length + 3)) + secondTag + '\\n?';
var lastIndex = regEx.lastIndex;
tags = {
s: firstTag,
e: secondTag
};
regEx = RegExp(newRegEx, 'g');
regEx.lastIndex = lastIndex;
}
function replaceParamHelpers (params) {
params = params.replace(paramHelperRefRegExp, function (m, p1, p2) { // p1 scope, p2 string
if (typeof p2 === 'undefined') {
return m
} else {
if (typeof p1 === 'undefined') {
p1 = '';
}
return 'hvals' + p1 + '.' + p2
}
});
return params
}
// The whole regular expression can be hard to comprehend, so here it's broken down.
// You can pass the string between "START REGEXP" and "END REGEXP" into a regular expression
// That removes whitespace and comments, and outputs a working regular expression.
/* START REGEXP
{{ *? //the beginning
(?: //or for each possible tag
(?: //if a global or helper ref
(?: //choosing global or helper ref
(?:([\w$]+ *?(?:[^\s\w($][^\n]*?)*?)) //global reference
|
(?:@(?:([\w$]+:|(?:\.\.\/)+))? *(.+?) *) //helper reference
)
(?: *?(\| *?[\w$]+? *?)+?)? //filter
) //end if a global or helper ref
| //now if a helper oTag
(?:([\w$]+) *?\(([^\n]*?)\) *?([\w$]*))
| //now if a helper cTag
(?:\/ *?([\w$]+))
| //now if a helper block
(?:# *?([\w$]+))
| //now for a self closing tag
(?:([\w$]+) *?\(([^\n]*?)\) *?\/)
| //now for comments
(?:!--[^]+?--)
) //end or for each possible tag
*?}}
\n? //To replace a newline at the end of a line
END REGEXP */
/*
p1: global ref main
p2: helper ref id (with ':' after it) or path
p3: helper ref main
p4: filters
p5: helper name
p6: helper parameters
p7: helper id
p8: helper cTag name
p9: helper block name
p10: self closing helper name
p11: self closing helper params
Here's the RegExp I use to turn the expanded version between START REGEXP and END REGEXP to a working one: I replace [\f\n\r\t\v\u00a0\u1680\u2000\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]| \/\/[\w ']+\n with nothing.
*/
var nativeHelpers = {
if: {
helperStart: function (param) { // helperStart is called with (params, id) but id isn't needed
return 'if(' + param + '){'
},
helperEnd: function () {
return '}'
},
blocks: {
else: function () { // called with (id) but neither param is needed
return '}else{'
}
}
},
each: {
helperStart: function (param, id) { // helperStart is called with (params, id) but id isn't needed
return 'for(var i=0;i<' + param + ".length; i++){tR+=(function(hvals){var tR='';var hvals" + id + '=hvals;'
},
helperEnd: function (param) {
return 'return tR})({this:' + param + '[i],index:i})};'
}
},
foreach: {
helperStart: function (param, id) {
return 'for(var key in ' + param + '){if(!' + param + ".hasOwnProperty(key)) continue;tR+=(function(hvals){var tR='';var hvals" + id + '=hvals;'
},
helperEnd: function (param) {
return 'return tR})({this:' + param + '[key], key: key})};'
}
},
log: {
selfClosing: function (param) {
return 'console.log(' + param + ');'
}
},
tags: {
selfClosing: function (param) {
var firstTag = param.slice(0, param.indexOf(',')).trim();
var secondTag = param.slice(param.indexOf(',') + 1).trim();
changeTags(firstTag, secondTag);
return ''
}
},
js: { // The js self-closing helper allows you to inject JavaScript straight into your template function
selfClosing: function (param) {
return param + ';'
}
}
};
var escMap = {
'&': '&',
'<': '<',
'"': '"',
"'": '''
};
function replaceChar (s) {
return escMap[s]
}
var escapeRegEx = /[&<"']/g;
var escapeRegExTest = /[&<"']/;
var filters = {
e: function (str) {
// To deal with XSS. Based on Escape implementations of Mustache.JS and Marko, then customized.
var newStr = String(str);
if (escapeRegExTest.test(newStr)) {
return newStr.replace(escapeRegEx, replaceChar)
} else {
return newStr
}
}
};
// Don't need a filter for unescape because that's just a flag telling Squirrelly not to escape
var defaultFilters = {
/*
All strings are automatically passed through each of the default filters the user
Has set to true. This opens up a realm of possibilities.
*/
// somefilter: false
};
var defaultFilterCache = {
// This is to prevent having to re-calculate default filters every time you return a filtered string
start: '',
end: ''
};
function setDefaultFilters (obj) {
if (obj === 'clear') { // If someone calls Sqrl.setDefaultFilters('clear') it clears all default filters
defaultFilters = {};
} else {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
defaultFilters[key] = obj[key];
}
}
}
cacheDefaultFilters();
}
var autoEscape = true;
function autoEscaping (bool) {
autoEscape = bool;
return autoEscape
}
function cacheDefaultFilters () {
defaultFilterCache = {
start: '',
end: ''
};
for (var key in defaultFilters) {
if (!defaultFilters.hasOwnProperty(key) || !defaultFilters[key]) continue
defaultFilterCache.start += 'Sqrl.F.' + key + '(';
defaultFilterCache.end += ')';
}
}
function parseFiltered (initialString, filterString) {
var filtersArray;
var safe = false;
var filterStart = '';
var filterEnd = '';
if (filterString && filterString !== '') {
filtersArray = filterString.split('|');
for (var i = 0; i < filtersArray.length; i++) {
filtersArray[i] = filtersArray[i].trim(); // Removing the spaces just in case someone put | filter| or | filter | or something similar
if (filtersArray[i] === '') continue
if (filtersArray[i] === 'safe') {
// If 'safe' is one of the filters, set safe to true but don't add Sqrl.F.safe
// Essentially, 'safe' is a flag telling Squirrelly not to autoEscape
safe = true;
continue
}
filterStart = 'Sqrl.F.' + filtersArray[i] + '(' + filterStart;
filterEnd += ')';
}
}
filterStart += defaultFilterCache.start;
filterEnd += defaultFilterCache.end;
if (!safe && autoEscape) {
filterStart += 'Sqrl.F.e(';
filterEnd += ')';
}
return filterStart + initialString + filterEnd
}
function Compile (str) {
var lastIndex = 0; // Because lastIndex can be complicated, and this way the minifier can minify more
var funcStr = "var tR='';"; // This will be called with Function() and returned
var helperArray = []; // A list of all 'outstanding' helpers, or unclosed helpers
var helperNumber = -1;
var helperAutoId = 0; // Squirrelly automatically generates an ID for helpers that don't have a custom ID
var helperContainsBlocks = {}; // If a helper contains any blocks, helperContainsBlocks[helperID] will be set to true
var m;
function addString (indx) {
if (lastIndex !== indx) {
funcStr +=
"tR+='" +
str
.slice(lastIndex, indx)
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'") +
"';";
}
}
setup();
while ((m = regEx.exec(str)) !== null) {
addString(m.index); // Add the string between the last tag (or start of file) and the current tag
lastIndex = m[0].length + m.index;
if (m[1]) {
// It's a global ref. p4 = filters
funcStr += 'tR+=' + globalRef(m[1], m[4]) + ';';
} else if (m[3]) {
// It's a helper ref. p2 = id (with ':' after it) or path, p4 = filters
funcStr += 'tR+=' + helperRef(m[3], m[2], m[4]) + ';';
} else if (m[5]) {
// It's a helper oTag. p6 parameters, p7 id
var id = m[7];
if (id === '' || id === null) {
id = helperAutoId;
helperAutoId++;
}
var native = nativeHelpers.hasOwnProperty(m[5]); // true or false
helperNumber += 1;
var params = m[6] || '';
params = replaceParamHelpers(params);
if (!native) {
params = '[' + params + ']';
}
var helperTag = {
name: m[5],
id: id,
params: params,
native: native
};
helperArray[helperNumber] = helperTag;
if (native) {
funcStr += nativeHelpers[m[5]].helperStart(params, id);
lastIndex = regEx.lastIndex; // the changeTags function sets lastIndex already
} else {
funcStr +=
'tR+=Sqrl.H.' +
m[5] +
'(' +
params +
',function(hvals){var hvals' +
id +
"=hvals;var tR='';";
}
} else if (m[8]) {
// It's a helper cTag.
var mostRecentHelper = helperArray[helperNumber];
if (mostRecentHelper && mostRecentHelper.name === m[8]) {
helperNumber -= 1;
if (mostRecentHelper.native === true) {
funcStr += nativeHelpers[mostRecentHelper.name].helperEnd(
mostRecentHelper.params,
mostRecentHelper.id
);
} else {
if (helperContainsBlocks[mostRecentHelper.id]) {
funcStr += 'return tR}});';
} else {
funcStr += 'return tR});';
}
}
} else {
console.error("Helper beginning & end don't match.");
}
} else if (m[9]) {
// It's a helper block.
var parent = helperArray[helperNumber];
if (parent.native) {
var nativeH = nativeHelpers[parent.name];
if (nativeH.blocks && nativeH.blocks[m[9]]) {
funcStr += nativeH.blocks[m[9]](parent.id);
lastIndex = regEx.lastIndex; // Some native helpers set regEx.lastIndex
} else {
console.warn(
"Native helper '%s' doesn't accept that block.",
parent.name
);
}
} else {
if (!helperContainsBlocks[parent.id]) {
funcStr +=
'return tR},{' +
m[9] +
':function(hvals){var hvals' +
parent.id +
"=hvals;var tR='';";
helperContainsBlocks[parent.id] = true;
} else {
funcStr +=
'return tR},' +
m[9] +
':function(hvals){var hvals' +
parent.id +
"=hvals;var tR='';";
}
}
} else if (m[10]) {
// It's a self-closing helper
var innerParams = m[11] || '';
innerParams = replaceParamHelpers(innerParams);
if (m[10] === 'include') {
// This code literally gets the template string up to the include self-closing helper,
// adds the content of the partial, and adds the template string after the include self-closing helper
var preContent = str.slice(0, m.index);
var endContent = str.slice(m.index + m[0].length);
var partialParams = innerParams.replace(/'|"/g, ''); // So people can write {{include(mypartial)/}} or {{include('mypartial')/}}
var partialContent = Partials[partialParams];
str = preContent + partialContent + endContent;
lastIndex = regEx.lastIndex = m.index;
} else if (
nativeHelpers.hasOwnProperty(m[10]) &&
nativeHelpers[m[10]].hasOwnProperty('selfClosing')
) {
funcStr += nativeHelpers[m[10]].selfClosing(innerParams);
lastIndex = regEx.lastIndex; // changeTags sets regEx.lastIndex
} else {
funcStr += 'tR+=Sqrl.H.' + m[10] + '(' + innerParams + ');'; // If it's not native, passing args to a non-native helper
}
}
/* eslint-disable no-inner-declarations */
function globalRef (refName, filters) {
return parseFiltered('options.' + refName, filters)
}
function helperRef (name, id, filters) {
var prefix;
if (typeof id !== 'undefined') {
if (/(?:\.\.\/)+/g.test(id)) {
// Test if the helper reference is prefixed with ../
prefix = helperArray[helperNumber - id.length / 3 - 1].id;
} else {
prefix = id.slice(0, -1);
}
return parseFiltered('hvals' + prefix + '.' + name, filters)
} // Implied 'else'
return parseFiltered('hvals.' + name, filters)
}
/* eslint-enable no-inner-declarations */
}
addString(str.length); // Add the string from the last tag-close to the end of the file, if there is one
funcStr += 'return tR';
var func = new Function( //eslint-disable-line
'options',
'Sqrl',
funcStr.replace(/\n/g, '\\n').replace(/\r/g, '\\r')
);
return func
}
function defineFilter (name, callback) {
filters[name] = callback;
}
function defineHelper (name, callback) {
helpers[name] = callback;
}
function defineNativeHelper (name, obj) {
nativeHelpers[name] = obj;
}
function Render (template, options) {
// If the template parameter is a function, call that function with (options, squirrelly stuff)
// If it's a string, first compile the string and then call the function
if (typeof template === 'function') {
return template(options, { H: helpers, F: filters, P: Partials })
} else if (typeof template === 'string') {
var res = load(options, template)(options, { H: helpers, F: filters, P: Partials });
return res
}
}
function definePartial (name, str) {
Partials[name] = str;
}
var cache = {};
function load (options, str) {
var filePath = options.$file;
var name = options.$name;
var caching = options.$cache;
if (caching !== false) {
// If caching isn't disabled
if (filePath) {
// If the $file attribute is passed in
if (cache[filePath]) {
// If the template is cached
return cache[filePath] // Return template
} else {
// Otherwise, read file
var fs = require('fs');
var fileContent = fs.readFileSync(filePath, 'utf8');
cache[filePath] = Compile(fileContent); // Add the template to the cache
return cache[filePath] // Then return the cached template
}
} else if (name) {
// If the $name attribute is passed in
if (cache[name]) {
// If there's a cache for that name
return cache[name] // Return cached template
} else if (str) {
// Otherwise, as long as there's a string passed in
cache[name] = Compile(str); // Add the template to the cache
return cache[name] // Return cached template
}
} else if (str) {
// If the string is passed in
if (caching === true) {
if (cache[str]) {
// If it's cached
return cache[str]
} else {
cache[str] = Compile(str); // Add it to cache
return cache[str]
}
} else {
return Compile(str)
}
} else {
return 'Error'
}
} else {
// If caching is disabled
if (filePath) {
// If the $file attribute is passed in
var fs2 = require('fs');
return Compile(fs2.readFileSync(filePath, 'utf8')) // Then return the cached template
} else if (str) {
// If the string is passed in
return Compile(str)
} else {
throw Error('No template')
}
}
}
function renderFile (filePath, options) {
options.$file = filePath;
return load(options)(options, { H: helpers, F: filters, P: Partials })
}
function __express (filePath, options, callback) {
return callback(null, renderFile(filePath, options))
}
exports.Compile = Compile;
exports.F = filters;
exports.H = helpers;
exports.P = Partials;
exports.Render = Render;
exports.__express = __express;
exports.autoEscaping = autoEscaping;
exports.defaultTags = defaultTags;
exports.defineFilter = defineFilter;
exports.defineHelper = defineHelper;
exports.defineNativeHelper = defineNativeHelper;
exports.definePartial = definePartial;
exports.load = load;
exports.renderFile = renderFile;
exports.setDefaultFilters = setDefaultFilters;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=squirrelly.dev.js.map