razor-tmpl
Version:
razor style template engine for JavaScript
812 lines (707 loc) • 20 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.razor = 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){
/**
* module dependencies
*/
var Tokens = require('./parser').Tokens;
var format = require('./util').format;
var escapeInNewFunction = require('./util').escapeInNewFunction;
/**
* compile `tokens` to `codes`
*
* returns Array
* in case someone want to hook the code before new Function
*/
exports.toCodes = function(tokens) {
var codes = ['var $result = "";'];
tokens.forEach(function(token) {
var data = token.val;
switch (token.type) {
case Tokens.TK_CODE_BLOCK:
/**
* @{ var data=10; }
*/
codes.push(data);
break;
case Tokens.TK_VAR:
/**
* @(data)
* 不允许空值,就是值不存在的情况下会报错
*/
var inner = format("try{$result+={0};}catch(e){ $result+= 'undefined';}", data);
codes.push(inner);
break;
case Tokens.TK_STRING:
/**
* div -> result+='div';
* "div" -> result+='\"div\"';
*/
var inner = format("$result+='{0}';", escapeInNewFunction(data));
codes.push(inner);
break;
default:
break;
}
});
codes.push('return $result');
return codes;
};
/**
* compile to function
*/
exports.compile = function(tokens, localsName) {
localsName = localsName || 'locals';
var codes = exports.toCodes(tokens);
var code = codes.join('\n');
return new Function(localsName, code);
};
},{"./parser":5,"./util":6}],2:[function(require,module,exports){
/**
* module dependencies
*/
var Parser = require('./parser');
var Compiler = require('./compiler');
var $ = require('./util');
/**
* expose Engine
*/
exports = module.exports = Engine;
/**
* Engine class
*/
function Engine(options) {
options = options || {};
/**
* @{}
* @for/while/each{}
*/
this.symbol = options.symbol || '@';
/**
* render(tmpl,{ a:'a',b:'b' })
* reference with `locals.a`
*/
this.localsName = options.localsName || 'locals';
/**
* easyLocals
*
* if true, locals = { a:'a',b:'b' } , new Function(locals,code)
* code will define
* var a = locals.a;
* var b = locals.b;
*
* enabled by default
*/
this.easyLocals = options.easyLocals || true;
}
/**
* settings
*/
Engine.prototype.set = function(name, val) {
this[name] = val;
return this;
};
/**
* reset settings
*/
Engine.prototype.reset = function() {
return this
.set('symbol', '@')
.set('localsName', 'locals')
.set('easyLocals', true);
};
/**
* add Variable def if `easyLocals` is true
*/
Engine.prototype.addVarDef = function(tmpl, locals) {
// generate def code
if (this.easyLocals && locals) {
var varDefs = '@{ ';
var keys = Object.keys(locals);
keys.forEach(function(key) {
varDefs += $.format('var {0} = locals["{0}"];', key);
});
varDefs += '}';
tmpl = varDefs + tmpl;
}
return tmpl;
};
/**
* compile the given template to a function
*
* if `easyLocals = true` & `locals` present
* generate var def code.
*/
Engine.prototype.compile = function(tmpl, locals) {
tmpl = this.addVarDef(tmpl, locals);
var tokens = Parser.parse(this.symbol, tmpl);
var fn = Compiler.compile(tokens, this.localsName);
return fn;
};
/**
* render the given `tmpl` with `locals`
*/
Engine.prototype.render = function(tmpl, locals) {
return this.compile(tmpl, locals)(locals);
};
},{"./compiler":1,"./parser":5,"./util":6}],3:[function(require,module,exports){
/**
* first check the required functions
*/
var requires = [
Array.prototype.forEach,
Array.prototype.sort,
String.prototype.trim,
Object.keys
];
for (var idx = 0; idx < requires.length; idx++) {
var req = requires[idx];
if (!req) {
console.error('ES5 requireed. See es5-shim');
throw new Error('requirements@' + idx + ' not satisified.');
}
};
/**
* module dependencies
*/
var Engine = require('./engine');
var Parser = require('./parser');
var Compiler = require('./compiler');
var util = require('./util');
/**
* expose default razor Engine
*/
var razor = exports = module.exports = new Engine;
/**
* expose Engine/Parser/Compiler/util
*/
exports.Engine = Engine;
exports.Parser = Parser;
exports.Compiler = Compiler;
exports.util = util;
/**
* expose version data
*/
exports.version = require('../package.json').version;
/**
* load jQuery extension
*/
require('./jqueryExt');
},{"../package.json":7,"./compiler":1,"./engine":2,"./jqueryExt":4,"./parser":5,"./util":6}],4:[function(require,module,exports){
/**
* decide whether jQuery exists
*/
var $;
if (typeof jQuery !== 'undefined' && jQuery) {
$ = jQuery;
}
// not work for browserify
// else {
// try {
// $ = require('jquery');
// }
// catch (e) {
// // module not found
// return;
// }
// }
if (!$) return;
/**
* module dependencies
*/
var razor = require('./index');
var format = require('./util').format;
var unescape = require('./util').unescape;
/**
* hide all element with `razor-template` attribute
*/
$(function() {
$("[razor-template]").hide();
});
/**
* getLoopHeader on a jQuery Wrapper of a dom element
*/
function getLoopHeader($el) {
var attr = $el.attr("razor-for") || $el.attr("data-razor-for");
if (attr) {
return format('for({0}){', attr.trim());
}
attr = $el.attr("razor-if") || $el.attr("data-razor-if");
if (attr) {
return format('if({0}){', attr.trim());
}
attr = $el.attr("razor-while") || $el.attr("data-razor-while");
if (attr) {
return format('while({0}){', attr.trim());
}
attr = $el.attr("razor-each") || $el.attr("data-razor-each");
if (attr) {
return format("each({0}){", attr.trim());
}
//啥都不是
return '';
}
function getTemplate($el) {
var el = $el.get(0);
if (!el) return '';
var ret;
if (el.tagName === 'SCRIPT') {
return ret = $el.html().trim();
}
else {
// see `razor-template` exists ?
ret = $el.attr('razor-template');
if (ret) return ret;
// not exists
var ret = $el.html().trim();
var header = getLoopHeader($el);
if (header) {
ret = razor.symbol + header + ret + '}';
}
ret = unescape(ret);
// cachesd to `razor-tmpl` attribute
$el.attr('razor-template', ret);
return ret;
}
}
/**
* compile a element to function
*/
$.prototype.compile = function() {
return razor.compile(getTemplate(this));
};
/**
* jQuery#render
*
* on script tag : return the result
* on normal tag(such as div) :
* 1. show the div
* 2. cache the template
* 3. return the result
*/
$.prototype.render = function(locals) {
var el = this.get(0);
var tmpl = getTemplate(this);
var result = razor.render(tmpl, locals);
if (el.tagName !== 'SCRIPT') {
this.html(result);
this.show();
}
return result;
};
},{"./index":3,"./util":6}],5:[function(require,module,exports){
/**
* module dependencies
*/
var $ = require('./util');
/**
* parse `template` to `tokens` use `symbol`
*/
exports = module.exports = Parser;
function Parser(symbol, input) {
this.symbol = symbol;
this.input = String(input);
this.consumed = -1;
this.tokens = [];
}
/**
* Parser.parse(symbol,input)
* @type {[type]}
*/
Parser.parse = parse;
function parse(symbol, input) {
var parser = new Parser(symbol, input);
return parser.parse();
}
/**
* Tokens type
*/
Parser.Tokens = {
"TK_VAR": 0,
"TK_CODE_BLOCK": 1,
"TK_STRING": 2
// for good look
// "TK_VAR": 'var',
// "TK_CODE_BLOCK": 'code',
// "TK_STRING": 'string'
};
/**
* make a new Token(type,val)
*/
Parser.prototype.tok = function(type, val) {
this.tokens.push({
type: type,
val: val
});
};
/**
* Parser#parse , main parse loop
*/
Parser.prototype.parse = function() {
for (var index = 0; index < this.input.length; index++) {
var cur = this.input[index];
var next = '';
if (cur == this.symbol) //'@'
{
//handle string before handle symbol @xxx
this.handleString(index);
//2. @之后的判断,不允许空白
next = this.input[index + 1];
//@@
if (next == this.symbol) {
index = this.handleEscapeSymbol(index);
continue;
}
//@* comment *@
else if (next == "*") {
index = this.handleComment(index);
continue;
}
else {
var tokenIndex = index + 1;
//@ if ( name == 'zhangsan' )
while (next == ' ') {
tokenIndex++;
next = this.input[tokenIndex];
}
switch (next) {
case '{': //@{code block}
index = this.handleCodeBlock(index, tokenIndex);
continue;
case '(': //@(var)
index = this.handleExplicitVariable(index, tokenIndex);
continue;
default: //可能有@if @for @while等
var remain = this.input.substring(tokenIndex);
//each - for/while/if/else - 普通 @...{}
if (/^each\s*\([\s\S]*\)\s*\{/.test(remain)) {
//@each
index = this.handleEach(index, tokenIndex);
continue;
}
else if (/^if\s*\([\s\S]*\)\s*\{/.test(remain)) {
index = this.handleIfElse(index, tokenIndex);
continue;
}
else if (/^(for|while)\s*\([\s\S]*\)\s*\{/.test(remain)) {
//@ for/while {}
index = this.handleControlFlow(index, tokenIndex);
continue;
}
break;
}
// 防止@each 等 被识别为 implicitVariable, 放在后面
var match = /^([\w\._\[\]])+/.exec(this.input.substring(index + 1));
if (match && match[0]) {
// @locals.name
index = this.handleImplicitVariable(index, match[0]);
continue;
}
}
}
}
//for退出后,还有一段string
//handleString取 [handleedIndex+1,atIndex)就是atIndex前面一个
//(template.length-1)+1 如length=10,0-9,9+1,包括9
this.handleString(this.input.length);
return this.tokens;
};
/*----------------------------------------------------*
* handleType *
* *
* i -> @ *
* returns the `index` in Parser#parse should be *
* after current handle operation *
*----------------------------------------------------*/
/**
* normal string
*
* i -> @
*/
Parser.prototype.handleString = function(i) {
var content = this.input.substring(this.consumed + 1, i);
if (content) {
this.tok(Parser.Tokens.TK_STRING, content);
}
this.consumed = i - 1;
};
Parser.prototype.handleComment = function(i) {
// @* comment *@
var remain = this.input.substr(i);
var star_index = remain.indexOf('*' + this.symbol);
if (star_index > -1) { // *@ exists
var commentEnd = star_index + 1 + i;
return this.consumed = commentEnd;
}
else { // no *@ found
// just ignore it , treat @* as normal string
return i;
// throw error
// var before = this.input.substring(0, i + 2); // start...@*
// var line = before.split('\n').length + 1;
// var chr = (i + 2) - before.split('\n').reduce(function(sum, line) {
// return sum += line.length; // '\r\n'.length = 2
// }, 0);
// var msg = $.format("line : {0},column : {1} no comment-end(*{3}) found",
// line, chr, this.symbol);
// throw new Error(msg);
}
};
Parser.prototype.handleEscapeSymbol = function(i) {
//@@ i i+1
this.tok(Parser.Tokens.TK_STRING, this.symbol);
return this.consumed = i + 1;
};
/**
* @{ ... } code block
*
* i -> @
* fi -> {
*/
Parser.prototype.handleCodeBlock = function(i, fi) {
var sec = $.getRight(this.input, fi);
var content = this.input.substring(fi + 1, sec);
content = content.trim();
if (content) {
this.tok(Parser.Tokens.TK_CODE_BLOCK, content);
}
return this.consumed = sec;
};
/**
* explicit variable @(var)
*
* i -> '@'
* fi -> (
*/
Parser.prototype.handleExplicitVariable = function(i, fi) {
// razor-tmpl not only used for generating html
// so default not escape html
// use @(- ) to escape
var sec = $.getRight(this.input, fi); // sec -> )
var content = this.input.substring(fi + 1, sec);
if (content) {
content = $.unescape(content); //like @( p.age >= 10)
// @(- data) escape html entity
if (content[0] === '-') {
content = content.substring(1).trim();
content += ".replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')";
// content += ".replace(/'/g,''')";
// content += '.replace(/"/g,""")';
// content += ".replace(/\\//g,'/')";
}
//@(data)
this.tok(Parser.Tokens.TK_VAR, content);
}
return this.consumed = sec;
};
/**
* @name implicit variable
*
* i -> @
* variable -> name
*/
Parser.prototype.handleImplicitVariable = function(i, variable) {
this.tok(Parser.Tokens.TK_VAR, variable);
return this.consumed = i + variable.length;
};
/**
* @each(item in items) {
* <div>@item.name</div>
* }
*
* i -> @
* fi -> e , each's first letter
*/
Parser.prototype.handleEach = function(i, fi) {
// '(' ')'
var remain = this.input.substring(i); //@xxxxx
var fi_small = remain.indexOf('(') + i;
var sec_small = $.getRight(this.input, fi_small);
//'{' '}'
remain = this.input.substring(sec_small);
var fi_big = remain.indexOf('{') + sec_small;
var sec_big = $.getRight(this.input, fi_big);
//1.for(var i in items){ item = items[i];
var loop = this.input.substring(fi_small + 1, sec_small); //item in items
var inIndex = loop.indexOf('in');
var item = loop.substring(0, inIndex).trim()
var items = loop.substring(inIndex + 2).trim();
var loop_head = $.format(
"for(var $index = 0,$length = {1}.length;$index<$length;$index++){var {0}={1}[$index];",
item, items
);
this.tok(Parser.Tokens.TK_CODE_BLOCK, loop_head);
//2.循环体
//{ <div>@(data)</div> }
var loop_body = this.input.substring(fi_big + 1, sec_big).trim() + '\n';
var inner_tokens = parse(this.symbol, loop_body);
this.tokens = this.tokens.concat(inner_tokens);
//3.}
this.tok(Parser.Tokens.TK_CODE_BLOCK, '}');
return this.consumed = sec_big;
};
/**
* @if(condition){ ... }
*
* i -> @
* fi -> if's first letter `i`
*/
Parser.prototype.handleIfElse = function(i, fi) {
// lastRightIndex : 上一个block 结尾的右大括号 index , // if(){ } <- lastRightIndex
var lastRightIndex, remain;
do {
lastRightIndex = this.handleControlFlow(i, fi);
// see whether `else [if]` exists
remain = this.input.substring(lastRightIndex + 1);
// decide else's e index
fi = remain.indexOf('else') + lastRightIndex + 1;
} while (/^\s*else/.test(remain));
return this.consumed = lastRightIndex;
};
/**
* handle @for/while control flow
*
* _ -> @ , not important
* fi -> [for,while]'s first letter
*/
Parser.prototype.handleControlFlow = function(_, fi) {
var remain = this.input.substring(fi);
var fi_big = remain.indexOf('{') + fi;
var sec_big = $.getRight(this.input, fi_big);
var part1 = this.input.substring(fi, fi_big + 1); // for(xxx){
var part2 = this.input.substring(fi_big + 1, sec_big); // <div>@(data)</div>
var part3 = '}'; //}
//1.part1
this.tok(Parser.Tokens.TK_CODE_BLOCK, part1);
//2.part2
part2 = part2.trim() + '\n';
var inner_tokens = parse(this.symbol, part2);
this.tokens = this.tokens.concat(inner_tokens);
//3.part3
this.tok(Parser.Tokens.TK_CODE_BLOCK, part3);
return this.consumed = sec_big;
};
},{"./util":6}],6:[function(require,module,exports){
/**
* some util functions
*/
/**
* format string like `String.Format` in C#
*
* e.g
* format('{0}+{1}={2}',1,1,2) => 1+1=2
*/
exports.format = function(s) {
var paras = [].slice.call(arguments, 1);
paras.forEach(function(para, index) {
s = s.replace(new RegExp('\\{' + index + '\\}', 'gi'), para.toString());
});
return s;
};
/**
* getRightIndex respond to fi from input
*
* input : input string
* fi : first index
*/
exports.getRight = function(input, fi) {
var pair = {
'{': '}',
'(': ')',
'[': ']'
};
var left = input[fi]; //'{' or '('
var right = pair[left];
var count = 1; //input[fi]
for (var i = fi + 1; i < input.length; i++) {
var cur = input[i];
if (cur == right) {
count--;
if (count == 0) {
return i;
}
}
else if (cur == left) {
count++;
}
}
return -1; //not found
};
/**
* unescape
*
* escape means :
* < <
* > >
* & &
*
* 在浏览器中,html()等方法会将特殊字符encode,导致处理之前是@while(a > 10) { }
* http://www.w3school.com.cn/html/html_entities.asp
*/
exports.unescape = function(encoded) {
return encoded
.replace(/</gi, '<')
.replace(/>/gi, '>')
.replace(/&/gi, '&');
};
/**
* if a string contains "abcd\nabcd",so
* `$result += "abcd
* abcd";`
*
* Error in new Function
*
* ' => \'
* " => \"
* \n => \\n
*/
exports.escapeInNewFunction = function(s) {
if (!s) return s;
return s
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/(\r?\n)/g, "\\n");
};
exports.getDate = function(d) {
d = d || new Date;
// year
var year = d.getFullYear();
// mon
var mon = d.getMonth() + 1 + '';
mon.length === 1 && (mon = '0' + mon);
// day
var day = d.getDate() + '';
day.length === 1 && (day = '0' + day);
return exports.format('{0}-{1}-{2}', year, mon, day);
}
},{}],7:[function(require,module,exports){
module.exports={
"name": "razor-tmpl",
"version": "1.3.0",
"description": "razor style template engine for JavaScript",
"main": "./lib/node/index.js",
"browser": "./lib/index.js",
"bin": {
"razor": "./lib/node/bin/razor"
},
"scripts": {
"test": "./node_modules/.bin/mocha --recursive",
"browser": "browserify lib/index.js --standalone razor > browser/razor-tmpl.js",
"min": "uglifyjs browser/razor-tmpl.js > browser/razor-tmpl.min.js"
},
"repository": {
"type": "git",
"url": "https://github.com/magicdawn/razor-tmpl"
},
"keywords": [
"razor",
"template"
],
"author": "magicdawn",
"license": "ISC",
"devDependencies": {
"browserify": "^9.0.8",
"mocha": "^2.1.0",
"uglifyjs": "^2.4.10"
}
}
},{}]},{},[3])(3)
});