Template.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /*
  2. ---
  3. script: template.js
  4. license: MIT-style license.
  5. description: Template - context aware template engine with conditional replacement, iterations and filters.
  6. copyright: Copyright (c) 2011 Thierry Bela
  7. authors: [Thierry Bela]
  8. requires:
  9. core:1.3:
  10. - Object
  11. - Array
  12. - Elements.from
  13. provides: [Template]
  14. ...
  15. */
  16. !function (window, undef) {
  17. "use strict";
  18. var log = (function () { return window.console && console.log ? function () { console.log.apply(console, arguments) } : function () { } })(),
  19. cache = {},
  20. Object = window.Object,
  21. Elements = window.Elements,
  22. append = 'append',
  23. filters = 'filters',
  24. indexOf = 'indexOf',
  25. join = 'join',
  26. modifiers = 'modifiers',
  27. push = 'push',
  28. replace = 'replace',
  29. shift = 'shift',
  30. split = 'split',
  31. substring = 'substring',
  32. Template = window.Template = function (options) {
  33. reset(this);
  34. this.initialize(options);
  35. return this
  36. },
  37. UID = 0,
  38. esc = {
  39. '&': '&',
  40. '<': '&lt;',
  41. '>': '&gt;',
  42. '"': '&quot;',
  43. "'": '&apos;'
  44. };
  45. // from mootools
  46. function reset(object) {
  47. for (var key in object) {
  48. var value = object[key];
  49. if(value instanceof Array) object[key] = value.clone();
  50. else if(typeof value == 'object') {
  51. var F = function(){};
  52. F.prototype = value;
  53. object[key] = reset(new F);
  54. }
  55. }
  56. return object
  57. }
  58. Template.prototype = {
  59. options: {
  60. /*
  61. begin: '{',
  62. end: '}',
  63. debug: false,
  64. // parse: function () {},
  65. escape: false, // escape strings by default
  66. quote: false // escape quotes
  67. */
  68. },
  69. filters: {},
  70. modifiers: {
  71. /* explicitely escape string */
  72. escape: function (context, property) {
  73. return property.indexOf('.') == -1 ? evaluate(context, property, true) : nestedeval(context, property.split('.'), true)
  74. },
  75. /* do not escape string */
  76. raw: function (context, property) {
  77. return property.indexOf('.') == -1 ? evaluate(context, property) : nestedeval(context, property.split('.'))
  78. }
  79. },
  80. /* UID: 0, */
  81. initialize: function (options) {
  82. this.options = Object[append]({}, options);
  83. this.UID = ++UID + '';
  84. cache[this.UID] = {}
  85. },
  86. setOptions: function (options) {
  87. if(options) {
  88. Object[append](this.options, options);
  89. cache[this.UID] = {};
  90. }
  91. return this
  92. },
  93. addFilter: function (name, fn) {
  94. if(typeof name == 'object') Object[append](this[filters], name);
  95. else this[filters][name] = fn;
  96. cache[this.UID] = {};
  97. return this
  98. },
  99. addModifier: function (name, fn) {
  100. if(typeof name == 'object') Object[append](this[modifiers], name);
  101. else this[modifiers][name] = fn;
  102. cache[this.UID] = {};
  103. return this
  104. },
  105. html: function (template, data) { return Elements.from(this.substitute(template, data)) },
  106. compile: function (template, options) {
  107. if(options && options != this.options) this.setOptions(options);
  108. if(cache[this.UID][template] && this.fn) return this.fn;
  109. this.fn = compile(template, Object[append]({}, this.options, {filters: this[filters], modifiers: this[modifiers]}), this.UID);
  110. return this.fn
  111. },
  112. substitute: function (template, data) {
  113. if(!cache[this.UID][template]) this.compile(template);
  114. return this.fn(data)
  115. }
  116. };
  117. function escapeHTML(string) {
  118. return ('' + string).replace(/[&<>'"]/g , function (c) { return esc[c] })
  119. }
  120. function compile(template, options, UID) {
  121. if(cache[UID][template]) return cache[UID][template];
  122. cache[UID][template] = parse(template, options, UID);
  123. return cache[UID][template]
  124. }
  125. function parse(template, options, UID) {
  126. var state = {}, fn, result = inline(template, options, UID, [], [], state);
  127. if(!state.iterable) {
  128. // log('Template: return ' + result[join]('+\n')[replace](/html\+=/g, ''))
  129. fn = new Function('data,options,evaluate,nestedeval,tmp,undef', 'return data==undef?"": ' + result[join]('+')[replace](/html\+=/g, ''));
  130. return function (data, html) { return html ? Elements.from(fn(data, options, evaluate, nestedeval)) : fn(data, options, evaluate, nestedeval) }
  131. }
  132. //log(('var html = "";' + result[join](';') + ';return html')[replace]('html = "";html+="', 'html = "'));
  133. fn = new Function('data,options,compile,test,evaluate,nestedeval,log,stack,buffer,filters,tmp,undef', ('var html = "";' + result[join](';') + ';return html')[replace]('html = "";html+="', 'html = "'));
  134. return function (data, html) {
  135. return html ? Elements.from(fn(data,options,compile,test,evaluate,nestedeval,log,[],[],[])) : fn(data,options,compile,test,evaluate,nestedeval,log,[],[],[])
  136. }
  137. }
  138. function inline(template, options, UID, stack, buffer, state) {
  139. var level = stack.length,
  140. index,
  141. cIndex,
  142. cTagIndex,
  143. string = '',
  144. match,
  145. oTag,
  146. cTag,
  147. tag,
  148. name,
  149. _filters,
  150. substr,
  151. escape = options.escape,
  152. original = template,
  153. begin = options.begin || '{',
  154. end = options.end || '}',
  155. length = begin.length,
  156. _modifiers = options[modifiers] || {};
  157. do {
  158. _filters = [];
  159. index = template[indexOf](begin);
  160. cIndex = template[indexOf](end, index);
  161. if(index != -1 && cIndex != -1) {
  162. if(template.charAt(index - 1) == '\\') {
  163. string = template[substring](0, cIndex + length);
  164. buffer[push]('html+=' + quote(template[substring](0, index - 1) + template[substring](index, cIndex + length)));
  165. template = template[replace](string, '');
  166. continue;
  167. }
  168. oTag = template[substring](index, cIndex + length);
  169. string = template[substring](0, index);
  170. match = template[substring](index + length, cIndex);
  171. if(match[indexOf](':') == -1) {
  172. name = match;
  173. if(match[indexOf](' ') != -1) {
  174. _filters = match[split](' ');
  175. name = _filters[shift]();
  176. }
  177. match = quote(name);
  178. stack[append](name == '.' || name[indexOf]('.') == -1 ? [name] : name[split]('.'));
  179. buffer[push]('html+=' + quote(string));
  180. if(_modifiers[name]) buffer[push]('html+=' + '((tmp=options.modifiers[' + match + '](data' + (_filters && _filters.length > 0 ? ',' + quote(_filters) : '') + '))||tmp!=undef?tmp:"")');
  181. else buffer[push]('html+=' + (stack.length <= 1 ? 'evaluate' : 'nestedeval') + '(data,' + (stack.length <= 1 ? quote(stack) : '[' + quote(stack) + ']') + (escape ? ',true' : '') + ')');
  182. template = template[replace](string + oTag, '');
  183. stack = stack.slice(0, level)
  184. }
  185. else {
  186. buffer[push]('html+=' + quote(string));
  187. _filters = match[split](':');
  188. tag = _filters[shift]();
  189. if(_filters[0].charAt(0) == ' ') {
  190. name = '';
  191. _filters = _filters[join](' ')[split](' ');
  192. }
  193. else {
  194. _filters = _filters[join](' ')[split](' ');
  195. name = _filters[shift]();
  196. }
  197. if(_filters.length > 0) _filters = _filters.filter(function (filter) { return filter !== '' });
  198. match = quote(name);
  199. cTag = begin + '/' + tag + ':' + name + end;
  200. cTagIndex = template[indexOf](cTag);
  201. if(cTagIndex == -1) {
  202. // log or throw an error ?
  203. log('token ' + oTag + ' is not closed properly: "' + template[substring](0, cIndex + 1) + '"\n' + original);
  204. template = template[replace](oTag, '');
  205. continue;
  206. }
  207. state.iterable = true;
  208. substr = template[substring](index + oTag.length, cTagIndex);
  209. template = template[replace](template[substring](0, cTagIndex + cTag.length), '');
  210. var context = template[indexOf](begin) != -1;
  211. switch(tag) {
  212. case 'if':
  213. case 'defined':
  214. case 'empty':
  215. case 'not-empty':
  216. var elseif = begin + 'else:' + name + end;
  217. name = name[split]('.');
  218. buffer[push]('stack.push(data)',
  219. 'data=' + (name.length <= 1 ? 'evaluate(data,' + quote(name) + (escape ? ',true' : ',false') + ',true)' : 'nestedeval(data,[' + quote(name) + ']' + (escape ? ',true' : ',false') + ',true)'),
  220. 'filters=[' + quote(_filters) + ']',
  221. 'var templates=[' + quote(substr[split](elseif)) + '],t=test(' + quote(tag) + ',data),i,swap=' + (tag == 'if' ? 'typeof data == "object"' : 'false') +
  222. ',context=stack[stack.length-1],value=templates.length==2?(t&&swap?data:context):(!t?undef:(swap?data:context))'
  223. );
  224. if(_filters.length >= 1) buffer.push('for(i=0;i<filters.length;i++)value=options[' + quote(filters) + '][filters[i]](value)');
  225. buffer.push(
  226. 'if(templates.length==2) html+=compile(templates[!t?1:0],options,' + quote(UID) + ')(value);' +
  227. 'else if(t)html+=compile(templates[0],options,' + quote(UID) + ')(value)',
  228. 'if(' + (!!context) + ') data=stack.pop()'
  229. );
  230. break;
  231. case 'loop':
  232. case 'repeat':
  233. if(tag == 'repeat') {
  234. name = name[split]('.');
  235. // save the context
  236. if(context) buffer[push]('stack.push(data)');
  237. buffer[push]('data=' + (name.length <= 1 ? 'evaluate(data,' + quote(name) + (escape ? ',true' : '') + ')' : 'nestedeval(data,[' + quote(name) + ']' + (escape ? ',true' : '') + ')'));
  238. }
  239. if(_filters.length > 0) buffer[push](
  240. 'filters=[' + quote(_filters) + ']',
  241. 'for(tmp=0;tmp<filters.length;tmp++)if(options.filters[filters[tmp]]!=undef)data=options.filters[filters[tmp]](data)'
  242. );
  243. buffer[push]('if(typeof data == "object") {' +
  244. 'var render = compile(' + quote(substr) + ',options, ' + quote(UID) + '),key',
  245. 'if(data instanceof Array) for(key = 0; key < data.length; key++) html+=render(evaluate(data,key' + (escape ? ',true' : '') + '))',
  246. 'else for(key in data) if(data.hasOwnProperty(key)) html+=render(evaluate(data,key' + (escape ? ',true' : '') + '))' +
  247. '}');
  248. if(tag == 'repeat') buffer[push]('if(' + (!!context) + ') data=stack.pop()');
  249. break;
  250. default:
  251. buffer[push]('tmp=options.parse!=undef?options.parse(' + quote(tag, name, substr) + ',data,options,filters) : ' + quote(substr),
  252. 'if(options.parse==undef) log("unknown tag: ",' + quote(tag, name, substr, original) + ')',
  253. 'html+=tmp==undef?"":tmp'
  254. );
  255. break;
  256. }
  257. }
  258. }
  259. }
  260. while(index != -1 && cIndex != -1);
  261. if(template !== '') buffer[push]('html+=' + quote(template));
  262. return buffer.filter(function (string) { return string !== '' && string != '""' && string != 'html+=""' });
  263. }
  264. function evaluate (object, property, escape, raw) {
  265. var value;
  266. if(property == '.' || property === '') value = object;
  267. else value = typeof object[property] == 'function' ? object[property]() : object[property];
  268. if(raw) return value;
  269. if(value != undef) return escape ? escapeHTML(value) : value;
  270. return raw ? object : ''
  271. }
  272. function nestedeval (object, paths, escape, raw) {
  273. var value = object, key, i;
  274. for(i = 0; i < paths.length; i++) {
  275. key = paths[i];
  276. if(value[key] == undef) return raw ? undef : '';
  277. value = typeof value[key] == 'function' ? value[key]() : value[key]
  278. }
  279. if(raw) return value;
  280. if(value != undef) return escape ? escapeHTML(value) : value;
  281. return raw ? object : ''
  282. }
  283. function test (tag, value) {
  284. switch(tag) {
  285. case 'if':
  286. case 'not-empty':
  287. return value != false && value != undef;
  288. case 'defined':
  289. return value != undef;
  290. case 'empty':
  291. return value == false || value == undef;
  292. case 'loop':
  293. case 'repeat':
  294. return typeof value == 'object';
  295. }
  296. return true
  297. }
  298. function quote() {
  299. return Array.flatten(arguments).map(function (string) {
  300. return '"' + ('' + string)[replace](/(["\\])/g, '\\$1')[replace](/\n/g, '\\n') + '"'
  301. })
  302. }
  303. }(this, null);