trimpath junction like model
[hyena.git] / app / ext / trimpath-template-1.0.38.js
blobffc40e9f4339570cda2662a832e32a3691844090
1 /**
2  * TrimPath Template. Release 1.0.38.
3  * Copyright (C) 2004, 2005 Metaha.
4  * 
5  * TrimPath Template is licensed under the GNU General Public License
6  * and the Apache License, Version 2.0, as follows:
7  *
8  * This program is free software; you can redistribute it and/or 
9  * modify it under the terms of the GNU General Public License
10  * as published by the Free Software Foundation; either version 2
11  * of the License, or (at your option) any later version.
12  * 
13  * This program is distributed WITHOUT ANY WARRANTY; without even the 
14  * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
15  * See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20  *
21  * Licensed under the Apache License, Version 2.0 (the "License");
22  * you may not use this file except in compliance with the License.
23  * You may obtain a copy of the License at
24  * 
25  * http://www.apache.org/licenses/LICENSE-2.0
26  * 
27  * Unless required by applicable law or agreed to in writing, software
28  * distributed under the License is distributed on an "AS IS" BASIS,
29  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30  * See the License for the specific language governing permissions and
31  * limitations under the License.
32  */
33 var TrimPath;\r
35 // TODO: Debugging mode vs stop-on-error mode - runtime flag.\r
36 // TODO: Handle || (or) characters and backslashes.
37 // TODO: Add more modifiers.
39 (function() {               // Using a closure to keep global namespace clean.\r
40     if (TrimPath == null)\r
41         TrimPath = new Object();\r
42     if (TrimPath.evalEx == null)\r
43         TrimPath.evalEx = function(src) { return eval(src); };\r
45     var UNDEFINED;
46     if (Array.prototype.pop == null)  // IE 5.x fix from Igor Poteryaev.
47         Array.prototype.pop = function() {
48             if (this.length === 0) {return UNDEFINED;}
49             return this[--this.length];
50         };
51     if (Array.prototype.push == null) // IE 5.x fix from Igor Poteryaev.
52         Array.prototype.push = function() {
53             for (var i = 0; i < arguments.length; ++i) {this[this.length] = arguments[i];}
54             return this.length;
55         };
57     TrimPath.parseTemplate = function(tmplContent, optTmplName, optEtc) {\r
58         if (optEtc == null)\r
59             optEtc = TrimPath.parseTemplate_etc;\r
60         var funcSrc = parse(tmplContent, optTmplName, optEtc);
61         var func = TrimPath.evalEx(funcSrc, optTmplName, 1);
62         if (func != null)
63             return new optEtc.Template(optTmplName, tmplContent, funcSrc, func, optEtc);
64         return null;
65     }\r
66     \r
67     try {\r
68         String.prototype.process = function(context, optFlags) {\r
69             var template = TrimPath.parseTemplate(this, null);
70             if (template != null)
71                 return template.process(context, optFlags);
72             return this;\r
73         }\r
74     } catch (e) { // Swallow exception, such as when String.prototype is sealed.\r
75     }\r
76     \r
77     TrimPath.parseTemplate_etc = {};            // Exposed for extensibility.\r
78     TrimPath.parseTemplate_etc.statementTag = "forelse|for|if|elseif|else|var|macro";\r
79     TrimPath.parseTemplate_etc.statementDef = { // Lookup table for statement tags.
80         "if"     : { delta:  1, prefix: "if (", suffix: ") {", paramMin: 1 },
81         "else"   : { delta:  0, prefix: "} else {" },
82         "elseif" : { delta:  0, prefix: "} else if (", suffix: ") {", paramDefault: "true" },
83         "/if"    : { delta: -1, prefix: "}" },
84         "for"    : { delta:  1, paramMin: 3, 
85                      prefixFunc : function(stmtParts, state, tmplName, etc) {
86                         if (stmtParts[2] != "in")
87                             throw new etc.ParseError(tmplName, state.line, "bad for loop statement: " + stmtParts.join(' '));
88                         var iterVar = stmtParts[1];
89                         var listVar = "__LIST__" + iterVar;
90                         return [ "var ", listVar, " = ", stmtParts[3], ";",
91                              // Fix from Ross Shaull for hash looping, make sure that we have an array of loop lengths to treat like a stack.
92                              "var __LENGTH_STACK__;",
93                              "if (typeof(__LENGTH_STACK__) == 'undefined' || !__LENGTH_STACK__.length) __LENGTH_STACK__ = new Array();", 
94                              "__LENGTH_STACK__[__LENGTH_STACK__.length] = 0;", // Push a new for-loop onto the stack of loop lengths.
95                              "if ((", listVar, ") != null) { ",
96                              "var ", iterVar, "_ct = 0;",       // iterVar_ct variable, added by B. Bittman     
97                              "for (var ", iterVar, "_index in ", listVar, ") { ",
98                              iterVar, "_ct++;",
99                              "if (typeof(", listVar, "[", iterVar, "_index]) == 'function') {continue;}", // IE 5.x fix from Igor Poteryaev.
100                              "__LENGTH_STACK__[__LENGTH_STACK__.length - 1]++;",
101                              "var ", iterVar, " = ", listVar, "[", iterVar, "_index];" ].join("");
102                      } },
103         "forelse" : { delta:  0, prefix: "} } if (__LENGTH_STACK__[__LENGTH_STACK__.length - 1] == 0) { if (", suffix: ") {", paramDefault: "true" },
104         "/for"    : { delta: -1, prefix: "} }; delete __LENGTH_STACK__[__LENGTH_STACK__.length - 1];" }, // Remove the just-finished for-loop from the stack of loop lengths.
105         "var"     : { delta:  0, prefix: "var ", suffix: ";" },
106         "macro"   : { delta:  1, 
107                       prefixFunc : function(stmtParts, state, tmplName, etc) {
108                           var macroName = stmtParts[1].split('(')[0];
109                           return [ "var ", macroName, " = function", 
110                                    stmtParts.slice(1).join(' ').substring(macroName.length),
111                                    "{ var _OUT_arr = []; var _OUT = { write: function(m) { if (m) _OUT_arr.push(m); } }; " ].join('');
112                      } }, 
113         "/macro"  : { delta: -1, prefix: " return _OUT_arr.join(''); };" }
114     }\r
115     TrimPath.parseTemplate_etc.modifierDef = {\r
116         "eat"        : function(v)    { return ""; },\r
117         "escape"     : function(s)    { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); },\r
118         "capitalize" : function(s)    { return String(s).toUpperCase(); },\r
119         "default"    : function(s, d) { return s != null ? s : d; }\r
120     }
121     TrimPath.parseTemplate_etc.modifierDef.h = TrimPath.parseTemplate_etc.modifierDef.escape;
123     TrimPath.parseTemplate_etc.Template = function(tmplName, tmplContent, funcSrc, func, etc) {
124         this.process = function(context, flags) {\r
125             if (context == null)\r
126                 context = {};\r
127             if (context._MODIFIERS == null)\r
128                 context._MODIFIERS = {};
129             if (context.defined == null)
130                 context.defined = function(str) { return (context[str] != undefined); };\r
131             for (var k in etc.modifierDef) {\r
132                 if (context._MODIFIERS[k] == null)\r
133                     context._MODIFIERS[k] = etc.modifierDef[k];\r
134             }\r
135             if (flags == null)\r
136                 flags = {};\r
137             var resultArr = [];
138             var resultOut = { write: function(m) { resultArr.push(m); } };\r
139             try {
140                 func(resultOut, context, flags);\r
141             } catch (e) {\r
142                 if (flags.throwExceptions == true)\r
143                     throw e;\r
144                 var result = new String(resultArr.join("") + "[ERROR: " + e.toString() + (e.message ? '; ' + e.message : '') + "]");
145                 result["exception"] = e;
146                 return result;\r
147             }
148             return resultArr.join("");
149         }\r
150         this.name       = tmplName;\r
151         this.source     = tmplContent; \r
152         this.sourceFunc = funcSrc;\r
153         this.toString   = function() { return "TrimPath.Template [" + tmplName + "]"; }
154     }
155     TrimPath.parseTemplate_etc.ParseError = function(name, line, message) {\r
156         this.name    = name;
157         this.line    = line;
158         this.message = message;
159     }
160     TrimPath.parseTemplate_etc.ParseError.prototype.toString = function() { 
161         return ("TrimPath template ParseError in " + this.name + ": line " + this.line + ", " + this.message);
162     }\r
163     
164     var parse = function(body, tmplName, etc) {
165         body = cleanWhiteSpace(body);
166         var funcText = [ "var TrimPath_Template_TEMP = function(_OUT, _CONTEXT, _FLAGS) { with (_CONTEXT) {" ];
167         var state    = { stack: [], line: 1 };                              // TODO: Fix line number counting.
168         var endStmtPrev = -1;
169         while (endStmtPrev + 1 < body.length) {
170             var begStmt = endStmtPrev;
171             // Scan until we find some statement markup.
172             begStmt = body.indexOf("{", begStmt + 1);
173             while (begStmt >= 0) {
174                 var endStmt = body.indexOf('}', begStmt + 1);
175                 var stmt = body.substring(begStmt, endStmt);
176                 var blockrx = stmt.match(/^\{(cdata|minify|eval)/); // From B. Bittman, minify/eval/cdata implementation.
177                 if (blockrx) {
178                     var blockType = blockrx[1]; 
179                     var blockMarkerBeg = begStmt + blockType.length + 1;
180                     var blockMarkerEnd = body.indexOf('}', blockMarkerBeg);
181                     if (blockMarkerEnd >= 0) {
182                         var blockMarker;
183                         if( blockMarkerEnd - blockMarkerBeg <= 0 ) {
184                             blockMarker = "{/" + blockType + "}";
185                         } else {
186                             blockMarker = body.substring(blockMarkerBeg + 1, blockMarkerEnd);
187                         }                        
188                         
189                         var blockEnd = body.indexOf(blockMarker, blockMarkerEnd + 1);
190                         if (blockEnd >= 0) {                            
191                             emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
192                             
193                             var blockText = body.substring(blockMarkerEnd + 1, blockEnd);
194                             if (blockType == 'cdata') {
195                                 emitText(blockText, funcText);
196                             } else if (blockType == 'minify') {
197                                 emitText(scrubWhiteSpace(blockText), funcText);
198                             } else if (blockType == 'eval') {
199                                 if (blockText != null && blockText.length > 0) // From B. Bittman, eval should not execute until process().
200                                     funcText.push('_OUT.write( (function() { ' + blockText + ' })() );');
201                             }
202                             begStmt = endStmtPrev = blockEnd + blockMarker.length - 1;
203                         }
204                     }                        
205                 } else if (body.charAt(begStmt - 1) != '$' &&               // Not an expression or backslashed,
206                            body.charAt(begStmt - 1) != '\\') {              // so check if it is a statement tag.
207                     var offset = (body.charAt(begStmt + 1) == '/' ? 2 : 1); // Close tags offset of 2 skips '/'.
208                                                                             // 10 is larger than maximum statement tag length.
209                     if (body.substring(begStmt + offset, begStmt + 10 + offset).search(TrimPath.parseTemplate_etc.statementTag) == 0) 
210                         break;                                              // Found a match.
211                 }
212                 begStmt = body.indexOf("{", begStmt + 1);
213             }
214             if (begStmt < 0)                              // In "a{for}c", begStmt will be 1.
215                 break;
216             var endStmt = body.indexOf("}", begStmt + 1); // In "a{for}c", endStmt will be 5.
217             if (endStmt < 0)
218                 break;
219             emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
220             emitStatement(body.substring(begStmt, endStmt + 1), state, funcText, tmplName, etc);
221             endStmtPrev = endStmt;
222         }
223         emitSectionText(body.substring(endStmtPrev + 1), funcText);
224         if (state.stack.length != 0)
225             throw new etc.ParseError(tmplName, state.line, "unclosed, unmatched statement(s): " + state.stack.join(","));
226         funcText.push("}}; TrimPath_Template_TEMP");
227         return funcText.join("");
228     }
229     
230     var emitStatement = function(stmtStr, state, funcText, tmplName, etc) {
231         var parts = stmtStr.slice(1, -1).split(' ');
232         var stmt = etc.statementDef[parts[0]]; // Here, parts[0] == for/if/else/...
233         if (stmt == null) {                    // Not a real statement.
234             emitSectionText(stmtStr, funcText);
235             return;
236         }
237         if (stmt.delta < 0) {
238             if (state.stack.length <= 0)
239                 throw new etc.ParseError(tmplName, state.line, "close tag does not match any previous statement: " + stmtStr);
240             state.stack.pop();
241         } 
242         if (stmt.delta > 0)
243             state.stack.push(stmtStr);
245         if (stmt.paramMin != null &&
246             stmt.paramMin >= parts.length)
247             throw new etc.ParseError(tmplName, state.line, "statement needs more parameters: " + stmtStr);
248         if (stmt.prefixFunc != null)
249             funcText.push(stmt.prefixFunc(parts, state, tmplName, etc));
250         else 
251             funcText.push(stmt.prefix);
252         if (stmt.suffix != null) {
253             if (parts.length <= 1) {
254                 if (stmt.paramDefault != null)
255                     funcText.push(stmt.paramDefault);
256             } else {
257                 for (var i = 1; i < parts.length; i++) {
258                     if (i > 1)
259                         funcText.push(' ');
260                     funcText.push(parts[i]);
261                 }
262             }
263             funcText.push(stmt.suffix);
264         }
265     }
267     var emitSectionText = function(text, funcText) {
268         if (text.length <= 0)
269             return;
270         var nlPrefix = 0;               // Index to first non-newline in prefix.
271         var nlSuffix = text.length - 1; // Index to first non-space/tab in suffix.\r
272         while (nlPrefix < text.length && (text.charAt(nlPrefix) == '\n'))
273             nlPrefix++;
274         while (nlSuffix >= 0 && (text.charAt(nlSuffix) == ' ' || text.charAt(nlSuffix) == '\t'))
275             nlSuffix--;
276         if (nlSuffix < nlPrefix)
277             nlSuffix = nlPrefix;
278         if (nlPrefix > 0) {
279             funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');\r
280             var s = text.substring(0, nlPrefix).replace('\n', '\\n'); // A macro IE fix from BJessen.
281             if (s.charAt(s.length - 1) == '\n')
282                 s = s.substring(0, s.length - 1);
283             funcText.push(s);
284             funcText.push('");');
285         }
286         var lines = text.substring(nlPrefix, nlSuffix + 1).split('\n');
287         for (var i = 0; i < lines.length; i++) {
288             emitSectionTextLine(lines[i], funcText);
289             if (i < lines.length - 1)
290                 funcText.push('_OUT.write("\\n");\n');
291         }\r
292         if (nlSuffix + 1 < text.length) {
293             funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');
294             var s = text.substring(nlSuffix + 1).replace('\n', '\\n');
295             if (s.charAt(s.length - 1) == '\n')
296                 s = s.substring(0, s.length - 1);
297             funcText.push(s);
298             funcText.push('");');
299         }
300     }
301     
302     var emitSectionTextLine = function(line, funcText) {
303         var endMarkPrev = '}';
304         var endExprPrev = -1;
305         while (endExprPrev + endMarkPrev.length < line.length) {
306             var begMark = "${", endMark = "}";
307             var begExpr = line.indexOf(begMark, endExprPrev + endMarkPrev.length); // In "a${b}c", begExpr == 1
308             if (begExpr < 0)
309                 break;
310             if (line.charAt(begExpr + 2) == '%') {
311                 begMark = "${%";
312                 endMark = "%}";
313             }
314             var endExpr = line.indexOf(endMark, begExpr + begMark.length);         // In "a${b}c", endExpr == 4;
315             if (endExpr < 0)
316                 break;
317             emitText(line.substring(endExprPrev + endMarkPrev.length, begExpr), funcText);                
318             // Example: exprs == 'firstName|default:"John Doe"|capitalize'.split('|')
319             var exprArr = line.substring(begExpr + begMark.length, endExpr).replace(/\|\|/g, "#@@#").split('|');
320             for (var k in exprArr) {
321                 if (exprArr[k].replace) // IE 5.x fix from Igor Poteryaev.
322                     exprArr[k] = exprArr[k].replace(/#@@#/g, '||');
323             }
324             funcText.push('_OUT.write(');
325             emitExpression(exprArr, exprArr.length - 1, funcText); 
326             funcText.push(');');
327             endExprPrev = endExpr;
328             endMarkPrev = endMark;
329         }
330         emitText(line.substring(endExprPrev + endMarkPrev.length), funcText); 
331     }
332     
333     var emitText = function(text, funcText) {
334         if (text == null ||
335             text.length <= 0)
336             return;
337         text = text.replace(/\\/g, '\\\\');
338         text = text.replace(/\n/g, '\\n');
339         text = text.replace(/"/g,  '\\"');
340         funcText.push('_OUT.write("');
341         funcText.push(text);
342         funcText.push('");');
343     }
344     
345     var emitExpression = function(exprArr, index, funcText) {
346         // Ex: foo|a:x|b:y1,y2|c:z1,z2 is emitted as c(b(a(foo,x),y1,y2),z1,z2)
347         var expr = exprArr[index]; // Ex: exprArr == [firstName,capitalize,default:"John Doe"]
348         if (index <= 0) {          // Ex: expr    == 'default:"John Doe"'\r
349             funcText.push(expr);
350             return;
351         }
352         var parts = expr.split(':');
353         funcText.push('_MODIFIERS["');
354         funcText.push(parts[0]); // The parts[0] is a modifier function name, like capitalize.
355         funcText.push('"](');
356         emitExpression(exprArr, index - 1, funcText);
357         if (parts.length > 1) {
358             funcText.push(',');
359             funcText.push(parts[1]);
360         }
361         funcText.push(')');
362     }\r
364     var cleanWhiteSpace = function(result) {
365         result = result.replace(/\t/g,   "    ");
366         result = result.replace(/\r\n/g, "\n");
367         result = result.replace(/\r/g,   "\n");
368         result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev.
369         return result;
370     }
372     var scrubWhiteSpace = function(result) {
373         result = result.replace(/^\s+/g,   "");
374         result = result.replace(/\s+$/g,   "");
375         result = result.replace(/\s+/g,   " ");
376         result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev.
377         return result;
378     }
380     // The DOM helper functions depend on DOM/DHTML, so they only work in a browser.
381     // However, these are not considered core to the engine.
382     //
383     TrimPath.parseDOMTemplate = function(elementId, optDocument, optEtc) {
384         if (optDocument == null)
385             optDocument = document;
386         var element = optDocument.getElementById(elementId);
387         var content = element.value;     // Like textarea.value.
388         if (content == null)
389             content = element.innerHTML; // Like textarea.innerHTML.
390         content = content.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
391         return TrimPath.parseTemplate(content, elementId, optEtc);
392     }
394     TrimPath.processDOMTemplate = function(elementId, context, optFlags, optDocument, optEtc) {
395         return TrimPath.parseDOMTemplate(elementId, optDocument, optEtc).process(context, optFlags);
396     }
397 }) ();