template.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. /**
  2. * TrimPath Template. Release 1.0.5.
  3. * Copyright (C) 2004, 2005 Metaha.
  4. *
  5. * This program is free software; you can redistribute it and/or
  6. * modify it under the terms of the GNU General Public License
  7. * as published by the Free Software Foundation; either version 2
  8. * of the License, or (at your option) any later version.
  9. *
  10. * This program is distributed WITHOUT ANY WARRANTY; without even the
  11. * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  12. * See the GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program; if not, write to the Free Software
  16. * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  17. */
  18. var TrimPath;
  19. // TODO: Debugging mode vs stop-on-error mode - runtime flag.
  20. // TODO: Handle || (or) characters and backslashes.
  21. // TODO: Add more modifiers.
  22. (function() { // Using a closure to keep global namespace clean.
  23. var theEval = eval; // Security, to ensure eval cleanliness.
  24. if (TrimPath == null)
  25. TrimPath = new Object();
  26. if (TrimPath.evalEx == null)
  27. TrimPath.evalEx = function(src) { return theEval(src); };
  28. TrimPath.parseTemplate = function(tmplContent, optTmplName, optEtc) {
  29. if (optEtc == null)
  30. optEtc = TrimPath.parseTemplate_etc;
  31. var funcSrc = parse(tmplContent, optTmplName, optEtc);
  32. var func = TrimPath.evalEx(funcSrc, optTmplName, 1);
  33. if (func != null)
  34. return new optEtc.Template(optTmplName, tmplContent, funcSrc, func, optEtc);
  35. return null;
  36. }
  37. try {
  38. String.prototype.process = function(context, optFlags) {
  39. var template = TrimPath.parseTemplate(this, null);
  40. if (template != null)
  41. return template.process(context, optFlags);
  42. return this;
  43. }
  44. } catch (e) { // Swallow exception, such as when String.prototype is sealed.
  45. }
  46. TrimPath.parseTemplate_etc = {}; // Exposed for extensibility.
  47. TrimPath.parseTemplate_etc.statementTag = "forelse|for|if|elseif|else|var|macro";
  48. TrimPath.parseTemplate_etc.statementDef = { // Lookup table for statement tags.
  49. "if" : { delta: 1, prefix: "if (", suffix: ") {", paramMin: 1 },
  50. "else" : { delta: 0, prefix: "} else {" },
  51. "elseif" : { delta: 0, prefix: "} else { if (", suffix: ") {", paramDefault: "true" },
  52. "/if" : { delta: -1, prefix: "}" },
  53. "for" : { delta: 1, paramMin: 3,
  54. prefixFunc : function(stmtParts, state, tmplName, etc) {
  55. if (stmtParts[2] != "in")
  56. throw new etc.ParseError(tmplName, state.line, "bad for loop statement: " + stmtParts.join(' '));
  57. var iterVar = stmtParts[1];
  58. var listVar = "__LIST__" + iterVar;
  59. return [ "var ", listVar, " = ", stmtParts[3], ";",
  60. "if ((", listVar, ") != null && (", listVar, ").length > 0) { for (var ",
  61. iterVar, "_index in ", listVar, ") { var ",
  62. iterVar, " = ", listVar, "[", iterVar, "_index];" ].join("");
  63. } },
  64. "forelse" : { delta: 0, prefix: "} } else { if (", suffix: ") {", paramDefault: "true" },
  65. "/for" : { delta: -1, prefix: "} }" },
  66. "var" : { delta: 0, prefix: "var ", suffix: ";" },
  67. "macro" : { delta: 1, prefix: "function ", suffix: "{ var _OUT_arr = []; var _OUT = { write: function(m) { if (m) _OUT_arr.push(m); }, }; " },
  68. "/macro" : { delta: -1, prefix: " return _OUT_arr.join(''); }" }
  69. }
  70. TrimPath.parseTemplate_etc.modifierDef = {
  71. "eat" : function(v) { return ""; },
  72. "escape" : function(s) { return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); },
  73. "capitalize" : function(s) { return s.toUpperCase(); },
  74. "default" : function(s, d) { return s != null ? s : d; }
  75. }
  76. TrimPath.parseTemplate_etc.modifierDef.h = TrimPath.parseTemplate_etc.modifierDef.escape;
  77. TrimPath.parseTemplate_etc.Template = function(tmplName, tmplContent, funcSrc, func, etc) {
  78. this.process = function(context, flags) {
  79. if (context == null)
  80. context = {};
  81. if (context._MODIFIERS == null)
  82. context._MODIFIERS = {};
  83. for (var k in etc.modifierDef) {
  84. if (context._MODIFIERS[k] == null)
  85. context._MODIFIERS[k] = etc.modifierDef[k];
  86. }
  87. if (flags == null)
  88. flags = {};
  89. var resultArr = [];
  90. var resultOut = { write: function(m) { if (m) resultArr.push(m); } };
  91. try {
  92. func(resultOut, context, flags);
  93. } catch (e) {
  94. if (flags.throwExceptions == true)
  95. throw e;
  96. var result = new String(resultArr.join("") + "[ERROR: " + e.toString() + "]");
  97. result["exception"] = e;
  98. return result;
  99. }
  100. return resultArr.join("");
  101. }
  102. this.name = tmplName;
  103. this.source = tmplContent;
  104. this.sourceFunc = funcSrc;
  105. this.toString = function() { return "TrimPath.Template [" + tmplName + "]"; }
  106. }
  107. TrimPath.parseTemplate_etc.ParseError = function(name, line, message) {
  108. this.name = name;
  109. this.line = line;
  110. this.message = message;
  111. }
  112. TrimPath.parseTemplate_etc.ParseError.prototype.toString = function() {
  113. return ("TrimPath template ParseError in " + this.name + ": line " + this.line + ", " + this.msg);
  114. }
  115. var parse = function(body, tmplName, etc) {
  116. body = cleanWhiteSpace(body);
  117. var funcText = [ "var TrimPath_Template_TEMP = function(_OUT, _CONTEXT, _FLAGS) { with (_CONTEXT) {" ];
  118. var state = { stack: [], line: 1 }; // TODO: Fix line number counting.
  119. var endStmtPrev = -1;
  120. while (endStmtPrev + 1 < body.length) {
  121. var begStmt = endStmtPrev;
  122. // Scan until we find some statement markup.
  123. begStmt = body.indexOf("{", begStmt + 1);
  124. while (begStmt >= 0) {
  125. if (body.charAt(begStmt - 1) != '$' && // Not an expression or backslashed,
  126. body.charAt(begStmt - 1) != '\\') { // so we assume it must be a statement tag.
  127. var offset = (body.charAt(begStmt + 1) == '/' ? 2 : 1); // Close tags offset of 2 skips '/'.
  128. // 10 is larger than maximum statement tag length.
  129. if (body.substring(begStmt + offset, begStmt + 10 + offset).search(TrimPath.parseTemplate_etc.statementTag) == 0)
  130. break; // Found a match.
  131. }
  132. begStmt = body.indexOf("{", begStmt + 1);
  133. }
  134. if (begStmt < 0) // In "a{for}c", begStmt will be 1.
  135. break;
  136. var endStmt = body.indexOf("}", begStmt + 1); // In "a{for}c", endStmt will be 5.
  137. if (endStmt < 0)
  138. break;
  139. emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
  140. emitStatement(body.substring(begStmt, endStmt +1), state, funcText, tmplName, etc);
  141. endStmtPrev = endStmt;
  142. }
  143. emitSectionText(body.substring(endStmtPrev + 1), funcText);
  144. if (state.stack.length != 0)
  145. throw new etc.ParseError(tmplName, state.line, "unclosed, unmatched statement(s): " + state.stack.join(","));
  146. funcText.push("}}; TrimPath_Template_TEMP");
  147. return funcText.join("");
  148. }
  149. var emitStatement = function(stmtStr, state, funcText, tmplName, etc) {
  150. var parts = stmtStr.slice(1, -1).split(' ');
  151. var stmt = etc.statementDef[parts[0]]; // Here, parts[0] == for/if/else/...
  152. if (stmt == null) { // Not a real statement.
  153. emitSectionText(stmtStr, funcText);
  154. return;
  155. }
  156. if (stmt.delta < 0) {
  157. if (state.stack.length <= 0)
  158. throw new etc.ParseError(tmplName, state.line, "close tag does not match any previous statement: " + stmtStr);
  159. state.stack.pop();
  160. }
  161. if (stmt.delta > 0)
  162. state.stack.push(stmtStr);
  163. if (stmt.paramMin != null &&
  164. stmt.paramMin >= parts.length)
  165. throw new etc.ParseError(tmplName, state.line, "statement needs more parameters: " + stmtStr);
  166. if (stmt.prefixFunc != null)
  167. funcText.push(stmt.prefixFunc(parts, state, tmplName, etc));
  168. else
  169. funcText.push(stmt.prefix);
  170. if (stmt.suffix != null) {
  171. if (parts.length <= 1) {
  172. if (stmt.paramDefault != null)
  173. funcText.push(stmt.paramDefault);
  174. } else {
  175. for (var i = 1; i < parts.length; i++) {
  176. if (i > 1)
  177. funcText.push(' ');
  178. funcText.push(parts[i]);
  179. }
  180. }
  181. funcText.push(stmt.suffix);
  182. }
  183. }
  184. var emitSectionText = function(text, funcText) {
  185. if (text.length <= 0)
  186. return;
  187. var nlPrefix = 0; // Index to first non-newline in prefix.
  188. var nlSuffix = text.length - 1; // Index to first non-space/tab in suffix.
  189. while (nlPrefix < text.length && (text.charAt(nlPrefix) == '\n'))
  190. nlPrefix++;
  191. while (nlSuffix >= 0 && (text.charAt(nlSuffix) == ' ' || text.charAt(nlSuffix) == '\t'))
  192. nlSuffix--;
  193. if (nlSuffix < nlPrefix)
  194. nlSuffix = nlPrefix;
  195. if (nlPrefix > 0) {
  196. funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');
  197. funcText.push(text.substring(0, nlPrefix).replace('\n', '\\n'));
  198. funcText.push('");');
  199. }
  200. var lines = text.substring(nlPrefix, nlSuffix + 1).split('\n');
  201. for (var i = 0; i < lines.length; i++) {
  202. emitSectionTextLine(lines[i], funcText);
  203. if (i < lines.length - 1)
  204. funcText.push('_OUT.write("\\n");\n');
  205. }
  206. if (nlSuffix + 1 < text.length) {
  207. funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');
  208. funcText.push(text.substring(nlSuffix + 1).replace('\n', '\\n'));
  209. funcText.push('");');
  210. }
  211. }
  212. var emitSectionTextLine = function(line, funcText) {
  213. var endExprPrev = -1;
  214. while (endExprPrev + 1 < line.length) {
  215. var begExpr = line.indexOf("${", endExprPrev + 1); // In "a${b}c", begExpr == 1
  216. if (begExpr < 0)
  217. break;
  218. var endExpr = line.indexOf("}", begExpr + 2); // In "a${b}c", endExpr == 4; 2 == "${".length
  219. if (endExpr < 0)
  220. break;
  221. emitText(line.substring(endExprPrev + 1, begExpr), funcText);
  222. // Example: exprs == 'firstName|default:"John Doe"|capitalize'.split('|')
  223. var exprArr = line.substring(begExpr + 2, endExpr).split('|');
  224. funcText.push('_OUT.write(');
  225. emitExpression(exprArr, exprArr.length - 1, funcText);
  226. funcText.push(');');
  227. endExprPrev = endExpr;
  228. }
  229. emitText(line.substring(endExprPrev + 1), funcText);
  230. }
  231. var emitText = function(text, funcText) {
  232. if (text == null ||
  233. text.length <= 0)
  234. return;
  235. text = text.replace(/\\/g, '\\\\');
  236. text = text.replace(/"/g, '\\"');
  237. funcText.push('_OUT.write("');
  238. funcText.push(text);
  239. funcText.push('");');
  240. }
  241. var emitExpression = function(exprArr, index, funcText) {
  242. // Ex: foo|a:x|b:y1,y2|c:z1,z2 is emitted as c(b(a(foo,x),y1,y2),z1,z2)
  243. var expr = exprArr[index]; // Ex: exprArr == [firstName,capitalize,default:"John Doe"]
  244. if (index <= 0) { // Ex: expr == 'default:"John Doe"'
  245. funcText.push(expr);
  246. return;
  247. }
  248. var parts = expr.split(':');
  249. funcText.push('_MODIFIERS["');
  250. funcText.push(parts[0]); // The parts[0] is a modifier function name, like capitalize.
  251. funcText.push('"](');
  252. emitExpression(exprArr, index - 1, funcText);
  253. if (parts.length > 1) {
  254. funcText.push(',');
  255. funcText.push(parts[1]);
  256. }
  257. funcText.push(')');
  258. }
  259. var cleanWhiteSpace = function(result) {
  260. result = result.replace(/\t/g, " ");
  261. result = result.replace(/\r\n/g, "\n");
  262. result = result.replace(/\r/g, "\n");
  263. result = result.replace(/^(.*\S)[ \t]+$/gm, "$1"); // Right trim.
  264. return result;
  265. }
  266. // The DOM helper functions depend on DOM/DHTML, so they only work in a browser.
  267. // However, these are not considered core to the engine.
  268. //
  269. TrimPath.parseDOMTemplate = function(elementId, optDocument, optEtc) {
  270. if (optDocument == null)
  271. optDocument = document;
  272. var element = optDocument.getElementById(elementId);
  273. var content = element.value; // Like textarea.value.
  274. if (content == null)
  275. content = element.innerHTML; // Like textarea.innerHTML.
  276. content = content.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
  277. return TrimPath.parseTemplate(content, elementId, optEtc);
  278. }
  279. TrimPath.processDOMTemplate = function(elementId, context, optFlags, optDocument, optEtc) {
  280. return TrimPath.parseDOMTemplate(elementId, optDocument, optEtc).process(context, optFlags);
  281. }
  282. }) ();