tinyliquid
Version:
A liquid template engine
1,978 lines (1,789 loc) • 105 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.TinyLiquid = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/**
* Context Object
*
* @author Zongmin Lei<leizongmin@gmail.com>
*/
var utils = require('./utils');
var parser = require('./parser');
var filters = require('./filters');
var vm = require('./vm');
var OPCODE = require('./opcode');
var debug = utils.debug('Context');
var merge = utils.merge;
/**
* Context
*
* @param {Object} options
* - {Object} filters
* - {Object} asyncFilters
* - {Object} locals
* - {Object} syncLocals
* - {Object} asyncLocals
* - {Object} blocks
* - {Boolean} isLayout default:false
* - {Integer} timeout unit:ms, default:120000
* - {Object} parent
*/
var Context = module.exports = exports = function (options) {
options = options || {};
this._locals = {};
this._syncLocals = {};
this._asyncLocals = {};
this._asyncLocals2 = [];
this._filters = merge(filters, options.filters);
this._asyncFilters = {};
this._cycles = {};
this._buffer = '';
this._forloops = [];
this._isInForloop = false;
this._tablerowloops = [];
this._isInTablerowloop = false;
this._includeFileHandler = null;
this._position = {line: 0, column: 0};
this._astCache = {};
this._filenameStack = [];
this._filterCache = {};
this._blocks = {};
this._isLayout = !!options.isLayout;
// default configuration
options = merge({
timeout: 120000
}, options);
this.options = options;
// parent
this._parent = options.parent || null;
// initialize the configuration
var me = this;
var set = function (name) {
if (options[name] && typeof(options[name]) === 'object') {
Object.keys(options[name]).forEach(function (i) {
me['_' + name][i] = options[name][i];
});
}
};
set('locals');
set('syncLocals');
set('asyncLocals');
set('asyncLocals2');
set('filters');
set('blocks');
if (options.asyncFilters && typeof options.asyncFilters === 'object') {
Object.keys(options.asyncFilters).forEach(function (i) {
me.setAsyncFilter(i, options.asyncFilters[i]);
});
}
};
/**
* Copy the configuration from other context object
*
* @param {Object} from
* @return {Object}
*/
Context.prototype.from = function (from) {
var me = this;
var set = function (name) {
if (from[name] && typeof(from[name]) === 'object') {
for (var i in from[name]) {
if (i in me[name]) continue;
me[name][i] = from[name][i];
}
} else if (typeof(from[name] === 'function')) {
if (!me[name]) {
me[name] = from[name];
}
}
}
set('_locals');
set('_syncLocals');
set('_asyncLocals');
set('_asyncLocals2');
set('_filters');
set('_asyncFilters');
set('options');
set('_onErrorHandler');
set('_includeFileHandler');
set('_filterCache');
set('_blocks');
if (Array.isArray(from._filenameStack)) {
me._filenameStack = from._filenameStack.slice();
}
for (var i in from) {
if (i in me) continue;
me[i] = from[i];
}
me._isInForloop = from._isInForloop;
me._forloops = from._forloops.slice();
me._isInTablerowloop = from._isInTablerowloop;
me._tablerowloops = from._tablerowloops;
me._isLayout = from._isLayout;
return this;
};
/* constants */
Context.prototype.STATIC_LOCALS = 0; // normal locals
Context.prototype.SYNC_LOCALS = 1; // get value from a sync function
Context.prototype.ASYNC_LOCALS = 2; // get value from a async function
Context.prototype.SYNC_FILTER = 0; // normal filter
Context.prototype.ASYNC_FILTER = 1; // async filter
/**
* Set Timeout
*
* @param {Integer} ms
*/
Context.prototype.setTimeout = function (ms) {
ms = parseInt(ms, 10);
if (ms > 0) this.options.timeout = ms;
};
/**
* Run AST
*
* @param {Array} astList
* @param {Function} callback
*/
Context.prototype.run = function (astList, callback) {
return vm.run(astList, this, callback);
};
/**
* Register normal locals
*
* @param {String} name
* @param {Function} val
*/
Context.prototype.setLocals = function (name, val) {
this._locals[name] = val;
if (this._parent) {
this._parent.setLocals(name, val);
}
};
/**
* Register sync locals
*
* @param {String} name
* @param {Function} val
*/
Context.prototype.setSyncLocals = function (name, fn) {
this._syncLocals[name] = fn;
};
/**
* Register async locals
*
* @param {String} name
* @param {Function} fn
*/
Context.prototype.setAsyncLocals = function (name, fn) {
if (name instanceof RegExp) {
var name2 = name.toString();
// remove the same name
for (var i = 0, len = this._asyncLocals2; i < len; i++) {
var item = this._asyncLocals2[i];
if (item[0].toString() === name2) {
this._asyncLocals2.splice(i, 1);
break;
}
}
this._asyncLocals2.push([name, fn]);
} else {
this._asyncLocals[name] = fn;
}
};
/**
* Register normal filter
*
* @param {String} name
* @param {Function} fn
*/
Context.prototype.setFilter = function (name, fn) {
this._filters[name.trim()] = fn;
};
/**
* Register async filter
*
* @param {String} name
* @param {Function} fn
*/
Context.prototype.setAsyncFilter = function (name, fn) {
if (fn.enableCache) fn = utils.wrapFilterCache(name, fn);
this._asyncFilters[name.trim()] = fn;
};
/**
* Set layout file
*
* @param {String} filename
*/
Context.prototype.setLayout = function (filename) {
this._layout = filename;
};
/**
* Set block
*
* @param {String} name
* @param {String} buf
*/
Context.prototype.setBlock = function (name, buf) {
this._blocks[name] = buf;
if (this._parent) {
this._parent.setBlock(name, buf);
}
};
/**
* Set block if empty
*
* @param {String} name
* @param {String} buf
*/
Context.prototype.setBlockIfEmpty = function (name, buf) {
if (!(name in this._blocks)) {
this._blocks[name] = buf;
}
};
/**
* Get block
*
* @param {String} name
* @return {String}
*/
Context.prototype.getBlock = function (name) {
return this._blocks[name] || null;
};
/**
* Get locals
*
* @param {String} name
* @return {Array} [type, value, isAllowCache] return null if the locals not found
*/
Context.prototype.getLocals = function (name) {
if (name in this._locals) return [this.STATIC_LOCALS, this._locals[name]];
if (name in this._syncLocals) return [this.SYNC_LOCALS, this._syncLocals[name], true];
if (name in this._asyncLocals) return [this.ASYNC_LOCALS, this._asyncLocals[name], true];
for (var i = 0, len = this._asyncLocals2.length; i < len; i++) {
var item = this._asyncLocals2[i];
if (item[0].test(name)) {
return [this.ASYNC_LOCALS, item[1], true];
}
}
return null;
};
/**
* Fetch Single Locals
*
* @param {String} name
* @param {Function} callback
*/
Context.prototype.fetchSingleLocals = function (name, callback) {
var me = this;
var info = me.getLocals(name);
if (!info) return callback(null, info);
switch (info[0]) {
case me.STATIC_LOCALS:
callback(null, info[1]);
break;
case me.SYNC_LOCALS:
var v = info[1](name, me);
if (info[2]) me.setLocals(name, v);
callback(null, v);
break;
case me.ASYNC_LOCALS:
info[1](name, function (err, v) {
if (err) return callback(err);
if (info[2]) me.setLocals(name, v);
callback(null, v);
}, me);
break;
default:
callback(me.throwLocalsUndefinedError(name));
}
};
/**
* Fetch locals
*
* @param {Array|String} list
* @param {Function} callback
*/
Context.prototype.fetchLocals = function (list, callback) {
var me = this;
if (Array.isArray(list)) {
var values = [];
utils.asyncEach(list, function (name, i, done) {
me.fetchSingleLocals(name, function (err, val) {
if (err) {
values[i] = err;
} else {
values[i] = val;
}
done();
});
}, callback, null, values);
} else {
me.fetchSingleLocals(list, callback);
}
};
/**
* Get filter
*
* @param {String} name
* @return {Array} [type, function] return null if the filter not found
*/
Context.prototype.getFilter = function (name) {
name = name.trim();
if (name in this._filters) return [this.SYNC_FILTER, this._filters[name]];
if (name in this._asyncFilters) return [this.ASYNC_FILTER, this._asyncFilters[name]];
return null;
};
/**
* Call filter
*
* @param {String} method
* @param {Array} args
* @param {Function} callback
*/
Context.prototype.callFilter = function (method, args, callback) {
if (arguments.length < 3) {
callback = args;
args = [];
}
var info = this.getFilter(method);
if (!info) return callback(this.throwFilterUndefinedError(method));
if (info[0] === this.ASYNC_FILTER) {
args.push(callback);
args.push(this);
info[1].apply(null, args);
} else {
args.push(this);
callback(null, info[1].apply(null, args));
}
};
/**
* Print HTML
*
* @param {Object} str
*/
Context.prototype.print = function (str) {
this._buffer += (str === null || typeof(str) === 'undefined') ? '' : str;
};
/**
* Set buffer
*
* @param {String} buf
*/
Context.prototype.setBuffer = function (buf) {
this._buffer = buf;
};
/**
* Get buffer
*
* @return {String}
*/
Context.prototype.getBuffer = function () {
return this._buffer;
};
/**
* Clear buffer
*
* @return {String}
*/
Context.prototype.clearBuffer = function () {
var buf = this.getBuffer();
this.setBuffer('');
return buf;
};
/**
* Set cycle
*
* @param {String} name
* @param {Array} list
*/
Context.prototype.setCycle = function (name, list) {
this._cycles[name] = {index: 0, length: list.length, list: list};
};
/**
* Get the index of the cycle
*
* @param {String} name
* @return {Integer}
*/
Context.prototype.getCycleIndex = function (name) {
var cycle = this._cycles[name];
if (cycle) {
cycle.index++;
if (cycle.index >= cycle.length) cycle.index = 0;
return cycle.index;
} else {
return null;
}
};
/**
* Enter a forloop
*
* @param {Integer} length
* @param {String} itemName
*/
Context.prototype.forloopEnter = function (length, itemName) {
this._forloops.push({
length: length,
itemName: itemName
});
this._isInForloop = true;
};
/**
* Set the forloop item value
*
* @param {Object} item
* @param {Integer} index
*/
Context.prototype.forloopItem = function (item, index) {
var loop = this._forloops[this._forloops.length - 1];
loop.item = item;
loop.index = index;
};
/**
* Set the forloop information
*
* @return {Object}
*/
Context.prototype.forloopInfo = function () {
return this._forloops[this._forloops.length - 1];
};
/**
* Exit the current forloop
*/
Context.prototype.forloopEnd = function () {
this._forloops.pop();
if (this._forloops.length < 1) {
this._isInForloop = false;
}
};
/**
* Enter a tablerowloop
*
* @param {Integer} length
* @param {String} itemName
* @param {Integer} columns
*/
Context.prototype.tablerowloopEnter = function (length, itemName, columns) {
this._tablerowloops.push({
length: length,
itemName: itemName,
columns: columns
});
this._isInTablerowloop = true;
};
/**
* Set the tablerowloop item value
*
* @param {Object} item
* @param {Integer} index
* @param {Integer} colIndex
*/
Context.prototype.tablerowloopItem = function (item, index, colIndex) {
var loop = this._tablerowloops[this._tablerowloops.length - 1];
loop.item = item;
loop.index = index;
loop.colIndex = colIndex;
};
/**
* Get the tablerow information
*
* @return {Object}
*/
Context.prototype.tablerowloopInfo = function () {
return this._tablerowloops[this._tablerowloops.length - 1];
};
/**
* Exit the current tablerowloop
*/
Context.prototype.tablerowloopEnd = function () {
this._tablerowloops.pop();
if (this._tablerowloops.length < 1) {
this._isInTablerowloop = false;
}
};
/**
* Include a template file
*
* @param {String} name
* @param {Array} localsAst
* @param {Array} headerAst
* @param {Function} callback
*/
Context.prototype.include = function (name, localsAst, headerAst, callback) {
if (typeof headerAst === 'function') {
callback = headerAst;
headerAst = null;
}
var me = this;
if (typeof(this._includeFileHandler) === 'function') {
this._includeFileHandler(name, function (err, astList) {
if (err) return callback(err);
// all include files run on new context
var c = new Context({parent: me});
c.from(me);
function start () {
c.run(astList, function (err) {
//console.log(err, c.getBuffer(), headerAst);
me.print(c.clearBuffer());
callback(err);
});
}
if (headerAst && headerAst.length > 0) {
astList = [me._position.line, me._position.column,OPCODE.LIST, headerAst, astList];
}
if (localsAst) {
me.run(localsAst, function (err, locals) {
if (err) locals = {};
Object.keys(locals).forEach(function (n) {
c._locals[n] = locals[n];
});
start();
});
} else {
start();
}
});
} else {
return callback(new Error('please set an include file handler'));
}
};
/**
* Extends layout
*
* @param {String} name
* @param {Function} callback
*/
Context.prototype.extends = function (name, callback) {
if (typeof(this._includeFileHandler) === 'function') {
this._includeFileHandler(name, callback);
} else {
return callback(new Error('please set an include file handler'));
}
};
/**
* Set the include file handler
*
* @param {Function} fn format: function (name, callback)
* callback format: function (err, astList)
*/
Context.prototype.onInclude = function (fn) {
this._includeFileHandler = fn;
};
/**
* Throw locals undefined error
*
* @param {String} name
* @return {Object}
*/
Context.prototype.throwLocalsUndefinedError = function (name) {
debug('Locals ' + name + ' is undefined');
return null;
};
/**
* Throw loop item undefined error
*
* @param {String} name
* @return {Object}
*/
Context.prototype.throwLoopItemUndefinedError = function (name) {
debug('Loop item ' + name + ' is undefined');
return null;
};
/**
* Throw forloop/tablerow locals undefined error
*
* @param {String} name
* @return {Object}
*/
Context.prototype.throwLoopLocalsUndefinedError = function (name) {
debug('Loop locals ' + name + ' is undefined');
return null;
};
/**
* Throw filter undefined error
*
* @param {String} name
* @return {Object}
*/
Context.prototype.throwFilterUndefinedError = function (name) {
var err = new Error('Filter ' + name + ' is undefined ' + this.getCurrentPosition(true));
err.code = 'UNDEFINED_FILTER';
err = this.wrapCurrentPosition(err);
return err;
}
/**
* Throw unknown opcode error
*
* @param {String} code
* @return {Object}
*/
Context.prototype.throwUnknownOpcodeError = function (code) {
var err = new Error('Unknown opcode ' + code + ' ' + this.getCurrentPosition(true));
err.code = 'UNKNOWN_OPCODE';
err = this.wrapCurrentPosition(err);
return err;
};
/**
* Throw unknown tag error
*
* @param {String} name
* @param {String} body
* @return {Object}
*/
Context.prototype.throwUnknownTagError = function (name, body) {
var err = new Error('Unknown tag "' + (name + ' ' + body).trim() + '" ' + this.getCurrentPosition(true));
err.code = 'UNKNOWN_TAG';
err = this.wrapCurrentPosition(err);
return err;
};
/**
* Set current position
*
* @param {Integer} line
* @param {Integer} column
*/
Context.prototype.setCurrentPosition = function (line, column) {
this._position.line = line;
this._position.column = column;
};
/**
* Get current position
*
* @param {Boolean} getString
* @return {Object}
*/
Context.prototype.getCurrentPosition = function (getString) {
if (getString) {
return 'at line ' + this._position.line + ', column ' + this._position.column;
} else {
return this._position;
}
};
/**
* Wrap current position on a error object
*
* @param {Object} err
* @return {Object}
*/
Context.prototype.wrapCurrentPosition = function (err) {
err = err || {};
err.line = this._position.line;
err.column = this._position.column;
return err;
};
/**
* Push Filename
*
* @param {String} filename
* @return {String}
*/
Context.prototype.pushFilename = function (filename) {
this._filenameStack.push(filename);
return filename;
};
/**
* Pop Filename
*
* @return {String}
*/
Context.prototype.popFilename = function () {
return this._filenameStack.pop();
};
/**
* Get filename
*
* @return {String}
*/
Context.prototype.getFilename = function () {
return this._filenameStack[this._filenameStack.length - 1];
};
},{"./filters":2,"./opcode":5,"./parser":6,"./utils":7,"./vm":8}],2:[function(require,module,exports){
'use strict';
/**
* Default Filters
*
* @author Zongmin Lei<leizongmin@gmail.com>
*/
/**
* To string, if it's undefined or null, return an empty string
*
* @param {Object} text
* @return {String}
*/
var toString = function (text) {
return (text === null || typeof(text) === 'undefined') ? '' : String(text);
};
/*---------------------------- HTML Filters ----------------------------------*/
/**
* Generate <img> tag
*
* @param {String} url
* @param {String} alt
* @return {String}
*/
exports.img_tag = function (url, alt) {
return '<img src="' + exports.escape(url) + '" alt="' + exports.escape(alt || '') + '">';
};
/**
* Generate <script> tag
*
* @param {String} url
* @return {String}
*/
exports.script_tag = function (url) {
return '<script src="' + exports.escape(url) + '"></sc' + 'ript>';
};
/**
* Generate <link> tag
*
* @param {String} url
* @param {String} media
* @return {String}
*/
exports.stylesheet_tag = function (url, media) {
return '<link href="' + exports.escape(url) + '" rel="stylesheet" type="text/css" media="' +
exports.escape(media || 'all') + '" />';
};
/**
* Generate <a> tag
*
* @param {String} link
* @param {String} url
* @param {String} title
* @return {String}
*/
exports.link_to = function (link, url, title) {
return '<a href="' + exports.escape(url || '') + '" title="' + exports.escape(title || '') + '">' +
exports.escape(link) + '</a>';
};
/*-----------------------------Math Filters-----------------------------------*/
/**
* Add
*
* @param {Number} input
* @param {Number} operand
* @return {Number}
*/
exports.plus = function (input, operand) {
input = Number(input) || 0;
operand = Number(operand) || 0;
return input + operand;
};
/**
* Subtract
*
* @param {Number} input
* @param {Number} operand
* @return {Number}
*/
exports.minus = function (input, operand) {
input = Number(input) || 0;
operand = Number(operand) || 0;
return input - operand;
};
/**
* Multiply
*
* @param {Number} input
* @param {Number} operand
* @return {Number}
*/
exports.times = function (input, operand) {
input = Number(input) || 0;
operand = Number(operand) || 0;
return input * operand;
};
/**
* Divide
*
* @param {Number} input
* @param {Number} operand
* @return {Number}
*/
exports.divided_by = function (input, operand) {
input = Number(input) || 0;
operand = Number(operand) || 0;
return input / operand;
};
/**
* Round (specify how many places after the decimal)
*
* @param {Number} input
* @param {Number} point
* @return {Number}
*/
exports.round = function (input, point) {
point = parseInt(point, 10) || 0;
if (point < 1) return Math.round(input);
var n = Math.pow(10, point);
return Math.round(input * n) / n;
};
/**
* Round
*
* @param {Number} input
* @return {Number}
*/
exports.integer = function (input) {
return parseInt(input, 10) || 0;
};
/**
* Generate random number such that: m <= Number < n
*
* @param {Number} m
* @param {Number} n
* @return {Number}
*/
exports.random = function (m, n) {
m = parseInt(m);
n = parseInt(n);
if (!isFinite(m)) return Math.random();
if (!isFinite(n)) {
n = m;
m = 0;
}
return Math.random() * (n - m) + m;
};
/**
* If input > 1 return singular, otherwise plural
*
* @param {Number} input
* @param {String} singular
* @param {String} plural
* @return {String}
*/
exports.pluralize = function (input, singular, plural) {
return Number(input) > 1 ? plural : singular;
};
/*-------------------------- Date and Time filters ----------------------------*/
/**
* Take the current time in milliseconds and add 0
*
* @param {Number} input
* @return {Number}
*/
exports.timestamp = function (input) {
input = parseInt(input, 10) || 0;
return new Date().getTime() + input;
};
/**
* Format date/time
* see syntax reference: http://liquid.rubyforge.org/classes/Liquid/StandardFilters.html#M000012
*
* @param {String} input
* @param {String} format
* @return {String}
*/
exports.date = function (input, format) {
if (toString(input).toLowerCase() == 'now') {
var time = new Date();
} else {
var timestamp = parseInt(input, 10);
if (timestamp == input) {
var time = new Date(timestamp);
} else {
var time = new Date(input);
}
}
if (!time || !isFinite(time.valueOf())) return 'Invalid Date';
if (!format) format = '%Y-%m-%j %H:%M:%S';
// example: ["Wed", "Apr", "11", "2012"]
var dates = time.toDateString().split(/\s/);
// example: ["Wednesday,", "April", "11,", "2012"]
var dateS = time.toLocaleDateString().split(/\s/);
// example: ["10", "37", "44", "GMT", "0800", "(中国标准时间)"]
var times = time.toTimeString().split(/[\s:\+]/);
var n2 = function (n) {
return n < 10 ? '0' + n : n;
};
var replace = {
a: dates[0], // week day
A: dateS[0],
b: dates[1], // month
B: dateS[1],
c: time.toLocaleString(),
d: dates[2],
H: times[0], // 24 hour
I: times[0] % 12, // 12 hour
j: dates[2], // date
m: n2(time.getMonth() + 1), // month
M: times[1], // minute
p: Number(times[0]) < 12 ? 'AM' : 'PM',
S: times[2], // second
U: weekNo(time), // start on Sunday
W: weekNo(time, true), // start on Monday
w: time.getDay(), // week day (0-6)
x: time.toDateString(),
X: time.toTimeString(),
y: dates[3].substr(-2), // year
Y: dates[3],
Z: times[4] // time zone
};
var ret = toString(format);
for (var i in replace) {
ret = ret.replace(new RegExp('%' + i, 'g'), replace[i]);
}
return ret;
};
function weekNo (now, mondayFirst) {
var totalDays = 0;
var years = now.getFullYear();
var days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (years % 100 === 0) {
if (years % 400 === 0) days[1] = 29;
} else if (years % 4 === 0) {
days[1] = 29;
}
if (now.getMonth() === 0) {
totalDays = totalDays + now.getDate();
} else {
var curMonth = now.getMonth();
for (var count = 1; count <= curMonth; count++) {
totalDays = totalDays + days[count - 1];
}
totalDays = totalDays + now.getDate();
}
// default to start on Sunday
var week = Math.round(totalDays / 7);
if (mondayFirst && new Date(toString(years)).getDay() === 0) week += 1;
return week;
}
/*---------------------------Strings Filters-----------------------------*/
/**
* Append to the end of string
*
* @param {String} input
* @param {String} characters
* @return {String}
*/
exports.append = function (input, characters) {
if (!characters) return toString(input);
return toString(input) + toString(characters);
};
/**
* Prepend to the begining
*
* @param {String} input
* @param {String} characters
* @return {String}
*/
exports.prepend = function (input, characters) {
if (!characters) return toString(input);
return toString(characters) + toString(input);
};
/**
* Combine to one camelized name
*
* @param {String} input
* @return {String}
*/
exports.camelize = function (input) {
input = toString(input);
return input.replace(/[^a-zA-Z0-9]+(\w)/g, function(_, ch) {
return ch.toUpperCase();
});
};
/**
* Combine to one capitalized name
*
* @param {String} input
* @return {String}
*/
exports.capitalize = function (input) {
input = toString(input);
if (input.length < 1) return input;
return input[0].toUpperCase() + input.substr(1);
};
/**
* To lowercase
*
* @param {String} input
* @return {String}
*/
exports.downcase = function (input) {
return toString(input).toLowerCase();
};
/**
* To uppercase
*
* @param {String} input
* @return {String}
*/
exports.upcase = function (input) {
return toString(input).toUpperCase();
};
/**
* Escape for use in HTML
*
* @param {String} input
* @return {String}
*/
exports.escape = function (input) {
return toString(input)
.replace(/&(?!\w+;)/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
};
/**
* Unescape HTML
*
* @param {String} input
* @return {String}
*/
exports.unescape = function (input) {
return toString(input)
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/&/g, '&');
};
/**
* Combine to hyphen separated word: 'this-is-a-book'
*
* @param {String} input
* @return {String}
*/
exports.handleize = function (input) {
return toString(input).replace(/[^0-9a-zA-Z ]/g, '').replace(/[ ]+/g, '-').toLowerCase();
};
/**
* Replace the first occurrence of substring with replacement
*
* @param {String} input
* @param {String} substring
* @param {String} replacement
* @return {String}
*/
exports.replace_first = function (input, substring, replacement) {
return toString(input).replace(substring, replacement);
};
/**
* Replace all occurrences of substring with replacement
*
* @param {String} input
* @param {String} substring
* @param {String} replacement
* @return {String}
*/
exports.replace = function (input, substring, replacement) {
input = toString(input);
while (input.indexOf(substring) > -1) {
input = input.replace(substring, replacement);
}
return input;
};
/**
* Remove all occurrences of substring
*
* @param {String} input
* @param {String} substring
* @return {String}
*/
exports.remove = function (input, substring) {
return exports.replace(input, substring, '');
};
/**
* Remove the first occurrence of substring
*
* @param {String} input
* @param {String} substring
* @return {String}
*/
exports.remove_first = function (input, substring) {
return exports.replace_first(input, substring, '');
};
/**
* Replace all newline characters with "<br>"
*
* @param {String} input
* @return {String}
*/
exports.newline_to_br = function (input) {
return toString(input).replace(/\n/img, '<br>');
};
/**
* Split the string at each occurrence of '-' (returns an array)
*
* @param {String} input
* @param {String} delimiter
* @return {String}
*/
exports.split = function (input, delimiter) {
if (!delimiter) delimiter = '';
return toString(input).split(delimiter);
};
/**
* Return the string length
*
* @param {array|string} input
* @return {String}
*/
exports.size = function (input) {
if (!input) return 0;
var len = input.length;
return len > 0 ? len : 0;
};
/**
* Remove all HTML tags
*
* @param {String} text
* @return {String}
*/
exports.strip_html = function (text) {
return toString(text).replace(/<[^>]*>/img, '');
};
/**
* Remove all newline characters
*
* @param {String} input
* @return {String}
*/
exports.strip_newlines = function (input) {
return toString(input).replace(/[\r\n]+/g, '');
};
/**
* Return only the first N characters
*
* @param {String} input
* @param {Number} n
* @return {String}
*/
exports.truncate = function (input, n) {
n = parseInt(n, 10);
if (!isFinite(n) || n < 0) n = 100;
return toString(input).substr(0, n);
};
/**
* Return only the first N words
*
* @param {String} input
* @param {Number} n
* @return {String}
*/
exports.truncatewords = function (input, n) {
n = parseInt(n, 10);
if (!isFinite(n) || n < 0) n = 15;
return toString(input).trim().split(/ +/).slice(0, n).join(' ');
};
/**
* Reverse the characters in the string
*
* @param {string|array} arr
* @return {string|array}
*/
exports.reverse = function (arr) {
return Array.isArray(arr) ? arr.reverse() : toString(arr).split('').reverse().join('');
};
/**
* Extracts parts of a string, beginning at the character at the specified posistion 'start',
* and returns the specified number of characters 'length'.
*
* @param {String} input
* @param {Number} start
* @param {Number} length
* @return {String}
*/
exports.substr = function (input, start, length) {
return toString(input).substr(start, length);
};
/**
* Search a substring, return its index position
*
* @param {string|array} arr
* @param {Object} searchvalue
* @param {Number} fromindex
* @return {Number}
*/
exports.indexOf = function (arr, searchvalue, fromindex) {
if (!Array.isArray(arr)) arr = toString(arr);
return arr.indexOf(searchvalue, fromindex);
};
/**
* If input is empty, default returns value, otherwise, the input.
* Can be used with strings, arrays, and hashes.
*
* @param {string|array|object} input
* @param {string|array|object} value
* @return {string|array|object}
*/
exports.default = function(input, value) {
return (input && input.length > 0)
? toString(input)
: toString(value);
};
/*----------------------- Arrays and Objects Filters -------------------------*/
function objectGetKeys (obj) {
return ((obj && typeof obj === 'object') ? Object.keys(obj) : []);
}
function getFirstKey (obj) {
if (Array.isArray(obj)) {
return 0;
} else {
var keys = objectGetKeys(obj);
return keys[0] || '';
}
};
function getLastKey (obj) {
if (Array.isArray(obj)) {
return obj.length - 1;
} else {
var keys = objectGetKeys(obj);
return keys.pop() || '';
}
};
/**
* Return an array of the object's keys
*
* @param {Object} input
* @return {Array}
*/
exports.keys = function (input) {
try {
return objectGetKeys(input);
} catch (err) {
return [];
}
};
/**
* Return the first element of an array
*
* @param {Array} array
* @return {Object}
*/
exports.first = function (array) {
return array && array[getFirstKey(array)];
};
/**
* Return the last element of an array
*
* @param {Array} array
* @return {Object}
*/
exports.last = function (array) {
return array && array[getLastKey(array)];
};
/**
* Join the array's elements into a string
*
* @param {Array} input
* @param {String} segmenter
* @return {String}
*/
exports.join = function (input, segmenter) {
if (!segmenter) segmenter = ' ';
if (Array.isArray(input)) {
return input.join(segmenter);
} else {
return '';
}
};
/**
* Return a JSON string of the object
*
* @param {Object} input
* @return {String}
*/
exports.json = function (input) {
try {
var ret = JSON.stringify(input);
} catch (err) {
return '{}';
}
return typeof ret !== 'string' ? '{}' : ret;
};
/**
* Get an item of the Object by property name
*
* @param {Object} obj
* @param {String} prop
* @return {Object}
*/
exports.get = function(obj, prop){
if (!obj) obj = {};
return obj[prop];
};
/**
* Take the specified property of each element in the array, returning a new array
*
* @param {Array} arr
* @param {String} prop
* @return {Array}
*/
exports.map = function (arr, prop) {
if (!Array.isArray(arr)) return [];
return arr.map(function(obj){
return obj && obj[prop];
});
};
/**
* Sort the array's elements by asc or desc order
*
* @param {Array} arr
* @param {Number} order
* @return {Array}
*/
exports.sort = function (arr, order) {
if (!Array.isArray(arr)) return [];
order = toString(order).trim().toLowerCase();
var ret1 = order === 'desc' ? -1 : 1;
var ret2 = 0 - ret1;
return arr.sort(function (a, b) {
if (a > b) return ret1;
if (a < b) return ret2;
return 0;
});
};
/**
* Sort the array's elements by each element's specified property
*
* @param {Array} obj
* @param {String} prop
* @param {Number} order
* @return {Array}
*/
exports.sort_by = function (obj, prop, order) {
if (!Array.isArray(obj)) return [];
order = toString(order).trim().toLowerCase();
var ret1 = order === 'desc' ? -1 : 1;
var ret2 = 0 - ret1;
return Object.create(obj).sort(function (a, b) {
a = a[prop];
b = b[prop];
if (a > b) return ret1;
if (a < b) return ret2;
return 0;
});
};
/*------------------------------- Other Filters ------------------------------*/
/**
* Get page count of the items when paginated
*
* @param {Number} count
* @param {Number} size
* @param {Number} page
* @listurn {Array}
*/
exports.pagination = function (count, size, page) {
if (count % size === 0) {
var maxPage = parseInt(count / size, 10);
} else {
var maxPage = parseInt(count / size, 10) + 1;
}
if (isNaN(page) || page < 1) {
page = 1;
}
page = parseInt(page);
var list = [page - 2, page - 1, page, page + 1, page + 2];
for (var i = 0; i < list.length;) {
if (list[i] < 1 || list[i] > maxPage) {
list.splice(i, 1);
} else {
i++;
}
}
if (list[0] !== 1) {
list.unshift('...');
list.unshift(1);
}
if (list[list.length - 1] < maxPage) {
list.push('...');
list.push(maxPage);
}
var ret = {
current: page,
next: page + 1,
previous: page - 1,
list: list
};
if (ret.next > maxPage) ret.next = maxPage;
if (ret.previous < 1) ret.previous = 1;
return ret;
};
},{}],3:[function(require,module,exports){
/**
* TinyLiquid
*
* @author Zongmin Lei<leizongmin@gmail.com>
*/
var packageInfo = require('../package.json');
var parser = require('./parser');
var vm = require('./vm');
var Context = require('./context');
var filters = require('./filters');
var utils = require('./utils');
var OPCODE = require('./opcode');
// TinyLiquid version
exports.version = packageInfo.version;
// AST parser
exports.parser = parser;
/**
* Parse template
*
* @param {String} tpl
* @param {Object} options
* @return {Array}
*/
exports.parse = function (tpl, options) {
return parser.apply(null, arguments);
};
/**
* Run AST code
*
* @param {Array} astList
* @param {Object} context
* @param {Function} callback
*/
exports.run = function (astList, context, callback) {
if (arguments.length < 3) {
var callback = arguments[arguments.length - 1];
var err = new Error('Not enough arguments.')
if (typeof callback === 'function') {
return callback(err);
} else {
throw err;
}
}
// if astList is not an AST array, then parse it firstly
if (!Array.isArray(astList)) {
try {
astList = exports.parse(astList);
} catch (err) {
return callback(err);
}
}
// ensure that the callback function is called only once
var originCallback = callback;
var hasCallback = false;
var callback = function (err) {
if (hasCallback) {
if (err) throw err;
return;
}
hasCallback = true;
clearTimeout(tid);
originCallback.apply(null, arguments);
};
// timeout
if (context.options && context.options.timeout > 0) {
var tid = setTimeout(function () {
callback(new Error('Timeout.'));
}, context.options.timeout);
}
// if it throws an error, catch it
try {
vm.run(astList, context, function (err, ret) {
if (err) return callback(err);
if (!context._layout) {
return callback(err, ret);
}
// if layout was set, then render the layout template
var c = exports.newContext();
c.from(context);
c._isLayout = true;
c.extends(c._layout, function (err, astList) {
if (err) return callback(err);
delete c._layout;
vm.run(astList, c, function (err) {
context.setBuffer(c.getBuffer());
callback(err);
});
});
});
} catch (err) {
return callback(err);
}
};
/**
* Compile to a function
*
* @param {String} tpl
* @param {Object} options
* @return {Function}
*/
exports.compile = function (tpl, options) {
var ast = exports.parse(tpl, options);
return function (context, callback) {
exports.run(ast, context, function (err) {
callback(err, context.getBuffer());
});
};
};
// Context
exports.Context = Context;
/**
* Create a new context
*
* @param {Object} options
* @return {Object}
*/
exports.newContext = function (options) {
return new Context(options);
};
// Utils
exports.utils = utils;
// Default filters
exports.filters = filters;
// OPCODE define
exports.OPCODE = OPCODE;
/**
* Insert filename
*
* @param {Array} astList
* @param {String} filename
* @return {Array}
*/
exports.insertFilename = function (astList, filename) {
astList.unshift([0, 0, OPCODE.TEMPLATE_FILENAME_PUSH, filename]);
astList.push([0, 0, OPCODE.TEMPLATE_FILENAME_POP]);
return astList;
};
},{"../package.json":10,"./context":1,"./filters":2,"./opcode":5,"./parser":6,"./utils":7,"./vm":8}],4:[function(require,module,exports){
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
var hexcase=0;function hex_md5(a){return rstr2hex(rstr_md5(str2rstr_utf8(a)))}function hex_hmac_md5(a,b){return rstr2hex(rstr_hmac_md5(str2rstr_utf8(a),str2rstr_utf8(b)))}function md5_vm_test(){return hex_md5("abc").toLowerCase()=="900150983cd24fb0d6963f7d28e17f72"}function rstr_md5(a){return binl2rstr(binl_md5(rstr2binl(a),a.length*8))}function rstr_hmac_md5(c,f){var e=rstr2binl(c);if(e.length>16){e=binl_md5(e,c.length*8)}var a=Array(16),d=Array(16);for(var b=0;b<16;b++){a[b]=e[b]^909522486;d[b]=e[b]^1549556828}var g=binl_md5(a.concat(rstr2binl(f)),512+f.length*8);return binl2rstr(binl_md5(d.concat(g),512+128))}function rstr2hex(c){try{hexcase}catch(g){hexcase=0}var f=hexcase?"0123456789ABCDEF":"0123456789abcdef";var b="";var a;for(var d=0;d<c.length;d++){a=c.charCodeAt(d);b+=f.charAt((a>>>4)&15)+f.charAt(a&15)}return b}function str2rstr_utf8(c){var b="";var d=-1;var a,e;while(++d<c.length){a=c.charCodeAt(d);e=d+1<c.length?c.charCodeAt(d+1):0;if(55296<=a&&a<=56319&&56320<=e&&e<=57343){a=65536+((a&1023)<<10)+(e&1023);d++}if(a<=127){b+=String.fromCharCode(a)}else{if(a<=2047){b+=String.fromCharCode(192|((a>>>6)&31),128|(a&63))}else{if(a<=65535){b+=String.fromCharCode(224|((a>>>12)&15),128|((a>>>6)&63),128|(a&63))}else{if(a<=2097151){b+=String.fromCharCode(240|((a>>>18)&7),128|((a>>>12)&63),128|((a>>>6)&63),128|(a&63))}}}}}return b}function rstr2binl(b){var a=Array(b.length>>2);for(var c=0;c<a.length;c++){a[c]=0}for(var c=0;c<b.length*8;c+=8){a[c>>5]|=(b.charCodeAt(c/8)&255)<<(c%32)}return a}function binl2rstr(b){var a="";for(var c=0;c<b.length*32;c+=8){a+=String.fromCharCode((b[c>>5]>>>(c%32))&255)}return a}function binl_md5(p,k){p[k>>5]|=128<<((k)%32);p[(((k+64)>>>9)<<4)+14]=k;var o=1732584193;var n=-271733879;var m=-1732584194;var l=271733878;for(var g=0;g<p.length;g+=16){var j=o;var h=n;var f=m;var e=l;o=md5_ff(o,n,m,l,p[g+0],7,-680876936);l=md5_ff(l,o,n,m,p[g+1],12,-389564586);m=md5_ff(m,l,o,n,p[g+2],17,606105819);n=md5_ff(n,m,l,o,p[g+3],22,-1044525330);o=md5_ff(o,n,m,l,p[g+4],7,-176418897);l=md5_ff(l,o,n,m,p[g+5],12,1200080426);m=md5_ff(m,l,o,n,p[g+6],17,-1473231341);n=md5_ff(n,m,l,o,p[g+7],22,-45705983);o=md5_ff(o,n,m,l,p[g+8],7,1770035416);l=md5_ff(l,o,n,m,p[g+9],12,-1958414417);m=md5_ff(m,l,o,n,p[g+10],17,-42063);n=md5_ff(n,m,l,o,p[g+11],22,-1990404162);o=md5_ff(o,n,m,l,p[g+12],7,1804603682);l=md5_ff(l,o,n,m,p[g+13],12,-40341101);m=md5_ff(m,l,o,n,p[g+14],17,-1502002290);n=md5_ff(n,m,l,o,p[g+15],22,1236535329);o=md5_gg(o,n,m,l,p[g+1],5,-165796510);l=md5_gg(l,o,n,m,p[g+6],9,-1069501632);m=md5_gg(m,l,o,n,p[g+11],14,643717713);n=md5_gg(n,m,l,o,p[g+0],20,-373897302);o=md5_gg(o,n,m,l,p[g+5],5,-701558691);l=md5_gg(l,o,n,m,p[g+10],9,38016083);m=md5_gg(m,l,o,n,p[g+15],14,-660478335);n=md5_gg(n,m,l,o,p[g+4],20,-405537848);o=md5_gg(o,n,m,l,p[g+9],5,568446438);l=md5_gg(l,o,n,m,p[g+14],9,-1019803690);m=md5_gg(m,l,o,n,p[g+3],14,-187363961);n=md5_gg(n,m,l,o,p[g+8],20,1163531501);o=md5_gg(o,n,m,l,p[g+13],5,-1444681467);l=md5_gg(l,o,n,m,p[g+2],9,-51403784);m=md5_gg(m,l,o,n,p[g+7],14,1735328473);n=md5_gg(n,m,l,o,p[g+12],20,-1926607734);o=md5_hh(o,n,m,l,p[g+5],4,-378558);l=md5_hh(l,o,n,m,p[g+8],11,-2022574463);m=md5_hh(m,l,o,n,p[g+11],16,1839030562);n=md5_hh(n,m,l,o,p[g+14],23,-35309556);o=md5_hh(o,n,m,l,p[g+1],4,-1530992060);l=md5_hh(l,o,n,m,p[g+4],11,1272893353);m=md5_hh(m,l,o,n,p[g+7],16,-155497632);n=md5_hh(n,m,l,o,p[g+10],23,-1094730640);o=md5_hh(o,n,m,l,p[g+13],4,681279174);l=md5_hh(l,o,n,m,p[g+0],11,-358537222);m=md5_hh(m,l,o,n,p[g+3],16,-722521979);n=md5_hh(n,m,l,o,p[g+6],23,76029189);o=md5_hh(o,n,m,l,p[g+9],4,-640364487);l=md5_hh(l,o,n,m,p[g+12],11,-421815835);m=md5_hh(m,l,o,n,p[g+15],16,530742520);n=md5_hh(n,m,l,o,p[g+2],23,-995338651);o=md5_ii(o,n,m,l,p[g+0],6,-198630844);l=md5_ii(l,o,n,m,p[g+7],10,1126891415);m=md5_ii(m,l,o,n,p[g+14],15,-1416354905);n=md5_ii(n,m,l,o,p[g+5],21,-57434055);o=md5_ii(o,n,m,l,p[g+12],6,1700485571);l=md5_ii(l,o,n,m,p[g+3],10,-1894986606);m=md5_ii(m,l,o,n,p[g+10],15,-1051523);n=md5_ii(n,m,l,o,p[g+1],21,-2054922799);o=md5_ii(o,n,m,l,p[g+8],6,1873313359);l=md5_ii(l,o,n,m,p[g+15],10,-30611744);m=md5_ii(m,l,o,n,p[g+6],15,-1560198380);n=md5_ii(n,m,l,o,p[g+13],21,1309151649);o=md5_ii(o,n,m,l,p[g+4],6,-145523070);l=md5_ii(l,o,n,m,p[g+11],10,-1120210379);m=md5_ii(m,l,o,n,p[g+2],15,718787259);n=md5_ii(n,m,l,o,p[g+9],21,-343485551);o=safe_add(o,j);n=safe_add(n,h);m=safe_add(m,f);l=safe_add(l,e)}return Array(o,n,m,l)}function md5_cmn(h,e,d,c,g,f){return safe_add(bit_rol(safe_add(safe_add(e,h),safe_add(c,f)),g),d)}function md5_ff(g,f,k,j,e,i,h){return md5_cmn((f&k)|((~f)&j),g,f,e,i,h)}function md5_gg(g,f,k,j,e,i,h){return md5_cmn((f&j)|(k&(~j)),g,f,e,i,h)}function md5_hh(g,f,k,j,e,i,h){return md5_cmn(f^k^j,g,f,e,i,h)}function md5_ii(g,f,k,j,e,i,h){return md5_cmn(k^(f|(~j)),g,f,e,i,h)}function safe_add(a,d){var c=(a&65535)+(d&65535);var b=(a>>16)+(d>>16)+(c>>16);return(b<<16)|(c&65535)}function bit_rol(a,b){return(a<<b)|(a>>>(32-b))};
module.exports = hex_md5;
},{}],5:[function(require,module,exports){
/**
* Define OPCODE
*
* @author Zongmin Lei<leizongmin@gmail.com>
*/
var OPCODE = {
// unknown opcode
UNKNOWN: 0,
// base opcode
AND: 1,
ASSIGN: 2,
CAPTURE: 3,
CASE: 4,
COMMENT: 5,
COMPILER_VERSION: 6,
CONTAINS: 7,
CYCLE: 8,
DEBUG: 9,
ED: 10,
EQ: 11,
EXISTS: 12,
FILTER: 13,
FOR: 14,
FORLOOPITEM: 15,
FORLOOPLOCALS: 16,
GE: 17,
GT: 18,
HASKEY: 19,
HASVALUE: 20,
IF: 21,
INCLUDE: 22,
LE: 23,
LIST: 24,
LOCALS: 25,
LT: 26,
NE: 27,
NOT: 28,
OBJECT: 29,
OR: 30,
RANGE: 31,
PRINT: 32,
PRINTLOCALS: 33,
PRINTSTRING: 34,
TABLEROW: 35,
TABLEROWITEM: 36,
TABLEROWLOOPLOCALS: 37,
UNKNOWN_TAG: 38,
WHEN: 39,
// forloop/tablerow attribute
LOOPLOCALS_LENGTH: 50,
LOOPLOCALS_NAME: 51,
LOOPLOCALS_INDEX0: 52,
LOOPLOCALS_INDEX: 53,
LOOPLOCALS_RINDEX0: 54,
LOOPLOCALS_RINDEX: 55,
LOOPLOCALS_FIRST: 56,
LOOPLOCALS_LAST: 57,
LOOPLOCALS_COL0: 58,
LOOPLOCALS_COL: 59,
LOOPLOCALS_COL_FIRST: 60,
LOOPLOCALS_COL_LAST: 61,
LOOPLOCALS_UNKNOWN: 62,
// extension instruction
TEMPLATE_FILENAME_PUSH: 80,
TEMPLATE_FILENAME_POP: 81,
// this "assign" will only affected current context
WEAK_ASSIGN: 82,
// extends and block
EXTENDS: 83,
BLOCK: 84
};
module.exports = exports = OPCODE;
// just for test
// for (var i in OPCODE) OPCODE[i] = i;
},{}],6:[function(require,module,exports){
/**
* Parse template
*
* @author Zongmin Lei<leizongmin@gmail.com>
*/
var utils = require('./utils');
var OPCODE = require('./opcode');
var merge = utils.merge;
var ASTStack = utils.ASTStack;
var localsAstNode = utils.localsAstNode;
var isQuoteWrapString = utils.isQuoteWrapString;
var textIndexOf = utils.textIndexOf;
var splitText = utils.splitText;
var stripQuoteWrap = utils.stripQuoteWrap;
var jsonStringify = utils.jsonStringify;
var md5 = utils.md5;
var arrayRemoveEmptyString = utils.arrayRemoveEmptyString;
var genRandomName = utils.genRandomName;
/**
* Parser context object
*
* @param {Object} options
*/
var Context = function (options) {
this.astStack = new ASTStack();
this.tags = options.customTags;
this.raw = '';
this.disableParseTag = false;
this.line = 1;
this.lineStart = 0;
this.position = 0;
this.parseTagStack = [];
this.forItems = [];
this.tablerowItems = [];
this.forItems.test = this.tablerowItems.test = function (name) {
var name = name.split('.')[0];
return this.indexOf(name) === -1 ? false : true;
};
};
/**
* Enable parse tag
*/
Context.prototype.enableParseTag = function () {
var parseTagStack = this.parseTagStack;
if (parseTagStack.length < 1) {
return true;
} else {
return parseTagStack[parseTagStack.length - 1].apply(null, arguments);
}
};
/**
* Get current position
*
* @return {Object}
*/
Context.prototype.getPosition = function () {
return {
line: this.line,
column: this.position - this.lineStart + 2
};
};
/**
* Generate a new AST node
*
* @return {Array}
*/
Context.prototype.astNode = function () {
var pos = this.getPosition();
var ast = [pos.line, pos.column];
for (var i = 0, len = arguments.length; i < len; i++) {
ast.push(arguments[i]);
}
return ast;
};
/**
* Parse template, return AST array
*
* @param {String} tpl
* @param {Object} options
* - {Object} customTags
* @return {Array}
*/
var parser = exports = module.exports = function (tpl, options) {
options =options || {};
var customTags = options.customTags = merge(baseTags, options.customTags);
// parser context
var context = new Context(options);
// compiler version
context.astStack.push(context.astNode(OPCODE.COMPILER_VERSION, 1));
var mainAst = context.astNode(OPCODE.LIST);
var strTmp = '';
function flush () {
context.astStack.push(context.astNode(OPCODE.PRINTSTRING, strTmp));
strTmp = '';
}
for (var i = 0, len = tpl.length; i < len; i++) {
context.position = i;
var c = tpl[i];
if (c === '\n') {
context.line++;
context.lineStart = i;
}
var text = tpl.substr(i, 2);
if (context.disableParseTag) {
// -----------------------------------------------------------------------
// raw
if (text === '{%') {
var e = textIndexOf(tpl, '%}', i);
var body = tpl.slice(i + 2, e).trim();
context.raw = strTmp;
if (e > i && context.enableParseTag(context, body, body)) {
context.disableParseTag = false;
strTmp = '';
context.raw = '';
i = e + 1;
} else {
strTmp += c;
}
} else {
strTmp += c;
}
} else { // ----------------------------------------------------------------
// normal
if (text === '{{') {
var e = textIndexOf(tpl, '}}', i);
if (e > i) {
flush();
context.astStack.push(parseOutput(tpl.slice(i + 2, e).trim(), context));
i = e + 1;
}
} else if (text === '{%') {
var e = textIndexOf(tpl, '%}', i);
if (e > i) {
// optimize: trim left
var e2 = strTmp.lastIndexOf('\n');
if (e2 !== -1) {
if (strTmp.slice(e2 + 1).trim() === '') {
strTmp = strTmp.slice(0, e2 + 1);
}
}
// parse tag
flush();
parseTag(context, tpl.slice(i + 2, e).trim());
i = e + 1;
// optimize: trim right
var e3 = tpl.indexOf('\n', i + 1);
if (e3 !== -1) {
if ((tpl.slice(i + 1, e3 + 1).trim() === '')) {
i = e3;
context.line++;
context.lineStart = i;
}
}
}
} else {
strTmp += c;
}
// -----------------------------------------------------------------------
}
}
flush();
return mainAst.concat(context.astStack.result());
};
// Default parser component
var baseTags = {
'if': function (context, name,