caveman
Version:
A fast JS templating engine.
402 lines (340 loc) • 9.54 kB
JavaScript
/**
* Caveman
* https://github.com/andrewchilds/caveman
*/
(function () {
var partials = {};
var macros = {};
var blockState = [];
var prefixes = {};
var options = {
escapeByDefault: false,
openTag: '{{',
closeTag: '}}',
shrinkWrap: false // remove whitespace between variables
};
var addMacro = function (name, macro) {
macros[name] = macro;
};
var each = function (obj, fn) {
if (obj && obj.forEach) {
return obj.forEach(fn);
} else {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
fn(obj[prop], prop);
}
}
}
};
var escapeText = function (str) {
return str.replace(/'/g, "\\'").replace(/^\n+|\n+$/g, '').replace(/\n/g, "'+'");
};
var escapeHTML = function (str) {
return exists(str) ? (str + '').replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\'/g, ''')
.replace(/\"/g, '"') : '';
};
var exists = function (obj) {
return typeof obj !== 'undefined' && obj !== null;
};
var shrinkWrapTemplate = function (str) {
return str.replace(/^\s+/gm, '');
};
var resetBlockState = function () {
blockState = [];
};
var pushBlockState = function (state) {
blockState.push(state);
};
var popBlockState = function () {
return blockState.pop();
};
var getBlockState = function () {
return blockState;
};
var resetPrefix = function () {
prefixes = {};
};
var addPrefix = function (name, prefix) {
prefixes[name] = prefix;
};
var getPrefix = function () {
var str = '';
each(prefixes, function (prefix) {
str += prefix;
});
return str;
};
var expandShortcuts = function (str) {
var match = false;
each(macros, function (macro) {
var shortcut = macro.shortcut;
if (!match && shortcut && shortcut.find.test(str)) {
match = true;
if (typeof shortcut.replace === 'string') {
str = shortcut.replace;
} else if (typeof shortcut.replace === 'function') {
str = shortcut.replace(str);
}
}
});
return str;
};
var isScript = function (str) {
return str && str.charAt(0) === '-';
};
var isComment = function (str) {
return str && str.slice(0, 2) === '//';
};
var translateScript = function (str) {
str = str.substr(1).trim(); // remove dash character
var match = false;
each(macros, function (macro, macroName) {
if (!match && macro.find.test(str)) {
match = true;
if (typeof macro.replace === 'string') {
str = macro.replace;
} else if (typeof macro.replace === 'function') {
str = macro.replace(str);
}
if (macro.blockEnd) {
if (typeof macro.blockEnd === 'string') {
pushBlockState(macro.blockEnd);
} else if (typeof macro.blockEnd === 'function') {
pushBlockState(macro.blockEnd());
}
}
if (macro.prefix) {
addPrefix(macroName, macro.prefix);
}
}
});
if (!match) {
// script isn't using any macros
return str + ';';
}
return str;
};
var compile = function (template) {
if (typeof template !== 'string') {
return '';
}
if (options.shrinkWrap) {
template = shrinkWrapTemplate(template);
}
var output = "var _CfS = Caveman.forceStr; var str = '';";
var parts = template.split(options.openTag);
resetBlockState();
resetPrefix();
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
if (part.indexOf(options.closeTag) !== -1) {
part = part.split(options.closeTag);
} else {
part = ['', part];
}
var code = part[0];
var text = part[1];
if (code) {
code = expandShortcuts(code);
if (isComment(code)) {
output += '';
} else if (isScript(code)) {
output += translateScript(code);
} else {
output += "str += _CfS(" + code + ");";
}
}
if (text) {
text = escapeText(text);
output += ("str += '" + text + "';");
}
}
return getPrefix() + output + 'return str;';
};
var forceStr = function (str) {
str = exists(str) ? str : '';
if (options.escapeByDefault && str) {
return escapeHTML(str);
}
return str;
};
// Readymade macros
addMacro('if', {
find: /^if /,
replace: function (str) {
return str.replace(/^if (.*)/, 'if ($1) {');
},
blockEnd: '}',
shortcut: {
find: /^\?\s?/,
replace: function (str) {
return str.replace(/^\?\s?(.*)/, '- if $1');
}
}
});
addMacro('unless', {
find: /^unless /,
replace: function (str) {
return str.replace(/^unless (.*)/, 'if (!$1) {');
},
blockEnd: '}',
shortcut: {
find: /^\^\s?/,
replace: function (str) {
return str.replace(/^\^\s?(.*)/, '- unless $1');
}
}
});
addMacro('elseif', {
find: /^else if /,
replace: function (str) {
return str.replace(/^else if (.*)/, '} else if ($1) {');
}
});
addMacro('else', {
find: /^else$/,
replace: '} else {'
});
addMacro('end', {
find: /^end$/,
replace: function () {
return popBlockState();
},
shortcut: {
find: /^\/$/,
replace: '- end '
}
});
addMacro('for', {
find: /^for /,
prefix: 'var _ds = new Array(5), _i, _len;',
replace: function (str) {
var id = getBlockState().length;
var js = '_ds.push(d); var _d' + id +
' = $1; for (var _i' + id + ' = 0, _len' + id + ' = ($1 || []).length; _i' +
id + ' < _len' + id + '; _i' + id + '++) { _i = _i' + id +
'; _len = _len' + id + ';';
return str.replace(/^for (.*) as (.*)/, js + 'var $2 = _d' + id + '[_i' + id + '];')
.replace(/^for (.*)/, js + 'd = _d' + id + '[_i' + id + '];');
},
blockEnd: function () {
var id = getBlockState().length - 1;
var resetIndex = '';
var lastState = getBlockState()[id] + '';
var lastStateIsForBlock = lastState.indexOf('_ds.pop()') !== -1;
if (id >= 0 && lastStateIsForBlock) {
resetIndex = '_i = _i' + id + '; _len = _len' + id + ';';
}
return '} d = _ds.pop();' + resetIndex;
}
});
addMacro('each', {
find: /^each /,
prefix: 'var _Ce = Caveman.each;',
replace: function (str) {
return str.replace(/^each (.*) as (.*)/, '_Ce($1, function ($2, _key) {')
.replace(/^each (.*)/, '_Ce($1, function (d, _key) {');
},
blockEnd: '});'
});
addMacro('with', {
find: /^with /,
prefix: 'var _Cw = Caveman.each;',
replace: function (str) {
return str.replace(/^with (.*) as (.*)/, '_Cw([$1], function ($2, _key) {')
.replace(/^with (.*)/, '_Cw([$1], function (d, _key) {');
},
blockEnd: '});'
});
addMacro('render', {
find: /^render /,
prefix: 'var _Cr = Caveman.render;',
replace: function (str) {
return str.replace(/^render ([^ ]*) (.*)/, 'str += _Cr(\'$1\', $2);')
.replace(/^render (.*)/, 'str += _Cr(\'$1\', d);');
}
});
addMacro('print', {
find: /^print /,
replace: function (str) {
return str.replace(/^print (.*)/, 'str += ($1);');
}
});
addMacro('log', {
find: /^log /,
replace: function (str) {
return str.replace(/^log (.*)/, 'console.log($1);');
}
});
addMacro('escape', {
find: /^escape /,
prefix: 'var _CeH = Caveman.escapeHTML;',
replace: function (str) {
return str.replace(/^escape (.*)/, 'str += _CeH($1);');
}
});
addMacro('unescape', {
find: /^unescape /,
replace: function (str) {
return str.replace(/^unescape (.*)/, 'str += ($1);');
}
});
addMacro('first', {
find: /^first$/,
replace: 'if (_i === 0) {',
blockEnd: '}'
});
addMacro('last', {
find: /^last$/,
replace: 'if (_i === _len - 1) {',
blockEnd: '}'
});
// Init
var register = function (partialName, template) {
if (typeof template === 'string') {
template = Caveman(template);
}
partials[partialName] = template;
return partials[partialName];
};
var render = function (partialName, data) {
if (partials[partialName]) {
return partials[partialName](Caveman, data);
} else {
throw Error('Partial "' + partialName + '" not found.');
}
};
var Caveman = function (template, data) {
var compiled = compile(template, options);
var fn = new Function('Caveman', 'd', compiled);
var renderFn = function (Caveman, data) {
return fn(Caveman, data);
};
renderFn.compiled = compiled;
return data ? renderFn(Caveman, data) : renderFn;
};
Caveman.options = options;
Caveman.each = each;
Caveman.escapeHTML = escapeHTML;
Caveman.compile = compile;
Caveman.forceStr = forceStr;
Caveman.partials = partials;
Caveman.register = register;
Caveman.render = render;
Caveman.macros = macros;
Caveman.addMacro = addMacro;
Caveman.popBlockState = popBlockState;
Caveman.pushBlockState = pushBlockState;
// Expose
if (typeof define === 'function' && define.amd) {
define(function() { return Caveman; });
} else if (typeof module === 'object' && typeof exports === 'object') {
module.exports = Caveman;
} else if (typeof window !== 'undefined') {
window.Caveman = Caveman;
}
}());