1 /*
  2  * Geddy JavaScript Web development framework
  3  * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
  4  *
  5  * Licensed under the Apache License, Version 2.0 (the "License");
  6  * you may not use this file except in compliance with the License.
  7  * You may obtain a copy of the License at
  8  *
  9  *         http://www.apache.org/licenses/LICENSE-2.0
 10  *
 11  * Unless required by applicable law or agreed to in writing, software
 12  * distributed under the License is distributed on an "AS IS" BASIS,
 13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  * See the License for the specific language governing permissions and
 15  * limitations under the License.
 16  *
 17 */
 18 
 19 var ejs = {};
 20 
 21 ejs.Template = function (p) {
 22   var UNDEF;
 23   var params = p || {};
 24 
 25   this.mode = null;
 26   this.templateText = params.text ||
 27     // If you don't want to use Fleegix.js,
 28     // override getTemplateTextFromNode to use
 29     // textarea node value for template text
 30     this.getTemplateTextFromNode(params.node);
 31   this.afterLoaded = params.afterLoaded;
 32   this.source = '';
 33   this.markup = UNDEF;
 34   // Try to get from URL
 35   if (typeof this.templateText == 'undefined') {
 36     // If you don't want to use Fleegix.js,
 37     // override getTemplateTextFromUrl to use
 38     // files for template text
 39     this.getTemplateTextFromUrl(params);
 40   }
 41 };
 42 
 43 ejs.Template.prototype = new function () {
 44   var _REGEX = /(<%%)|(%%>)|(<%=)|(<%#)|(<%)|(%>\n)|(%>)|(\n)/;
 45   this.modes = {
 46     EVAL: 'eval',
 47     OUTPUT: 'output',
 48     APPEND: 'append',
 49     COMMENT: 'comment',
 50     LITERAL: 'literal'
 51   };
 52   this.getTemplateTextFromNode = function (node) {
 53     // Requires the fleegix.xhr module
 54     if (typeof fleegix.string == 'undefined') {
 55       throw('Requires fleegix.string module.'); }
 56     var ret;
 57     if (node) {
 58       ret = node.value;
 59       ret = fleegix.string.unescapeXML(ret);
 60       ret = fleegix.string.trim(ret);
 61     }
 62     return ret;
 63   };
 64   this.getTemplateTextFromUrl = function (params) {
 65     // Requires the fleegix.xhr module
 66     if (typeof fleegix.xhr == 'undefined') {
 67       throw('Requires fleegix.xhr module.'); }
 68     var _this = this;
 69     var url = params.url;
 70     var noCache = params.preventCache || false;
 71     // Found text in cache, and caching is turned on
 72     if (text && !noCache) {
 73       this.templateText = text;
 74     }
 75     // Otherwise go grab the text
 76     else {
 77       // Callback for setting templateText and caching --
 78       // used for both sync and async loading
 79       var callback = function (s) {
 80         _this.templateText = s;
 81         ejs.templateTextCache[url] = s;
 82         // Use afterLoaded hook if set
 83         if (typeof _this.afterLoaded == 'function') {
 84           _this.afterLoaded();
 85         }
 86       };
 87       var opts;
 88       if (params.async) {
 89         opts = {
 90           url: url,
 91           method: 'GET',
 92           preventCache: noCache,
 93           async: true,
 94           handleSuccess: callback
 95         };
 96         // Get templ text asynchronously, wait for
 97         // loading to exec the callback
 98         fleegix.xhr.send(opts);
 99       }
100       else {
101         opts = {
102           url: url,
103           method: 'GET',
104           preventCache: noCache,
105           async: false
106         };
107         // Get the templ text inline and pass directly to
108         // the callback
109         text = fleegix.xhr.send(opts);
110         callback(text);
111       }
112     }
113   };
114   this.process = function (p) {
115     var params = p || {};
116     this.data = params.data || {};
117     var domNode = params.node;
118     // Cache/reuse the generated template source for speed
119     this.source = this.source || '';
120     if (!this.source) { this.generateSource(); }
121     
122     // Eval the template with the passed data
123     // Use 'with' to give local scoping to data obj props
124     // ========================
125     var _output = ''; // Inner scope var for eval output
126     with (this.data) {
127       eval(this.source); 
128     }
129     this.markup = _output;
130 
131     if (domNode) {
132       domNode.innerHTML = this.markup;
133     }
134     return this.markup;
135   };
136   this.generateSource = function () {
137     var line = '';
138     var matches = this.parseTemplateText();
139     if (matches) {
140       for (var i = 0; i < matches.length; i++) {
141         line = matches[i];
142         if (line) {
143           this.scanLine(line);
144         }
145       }
146     }
147   };
148   this.parseTemplateText = function() {
149     var str = this.templateText;
150     var pat = _REGEX;
151     var result = pat.exec(str);
152     var arr = [];
153     while (result) {
154       var firstPos = result.index;
155       var lastPos = pat.lastIndex;
156       if (firstPos !== 0) {
157         arr.push(str.substring(0, firstPos));
158         str = str.slice(firstPos);
159       }
160       arr.push(result[0]);
161       str = str.slice(result[0].length);
162       result = pat.exec(str);
163     }
164     if (str !== '') {
165       arr.push(str);
166     }
167     return arr;
168   };
169   this.scanLine = function (line) {
170     var _this = this;
171     var _addOutput = function () {
172       line = line.replace(/\n/, '\\n');
173 174       line = line.replace(/"/g, '\\"');
175       _this.source += '_output += "' + line + '";';
176     };
177     switch (line) {
178       case '<%':
179         this.mode = this.modes.EVAL;
180         break;
181       case '<%=':
182         this.mode = this.modes.OUTPUT;
183         break;
184       case '<%#':
185         this.mode = this.modes.COMMENT;
186         break;
187       case '<%%':
188         this.mode = this.modes.LITERAL;
189         this.source += '_output += "' + line + '";';
190         break;
191       case '%>':
192       case '%>\n':
193         if (this.mode == this.modes.LITERAL) {
194           _addOutput();
195         }
196         this.mode = null;
197         break;
198       default:
199         // In script mode, depends on type of tag
200         if (this.mode) {
201           switch (this.mode) {
202             // Just executing code
203             case this.modes.EVAL:
204               this.source += line;
205               break;
206             // Exec and output
207             case this.modes.OUTPUT:
208               // Add the exec'd result to the output
209               this.source += '_output += ' + line + ';';
210               break;
211             case this.modes.COMMENT:
212               // Do nothing
213               break;
214             // Literal <%% mode, append as raw output
215             case this.modes.LITERAL:
216               _addOutput();
217               break;
218           }
219         }
220         // In string mode, just add the output
221         else {
222           _addOutput();
223         }
224     }
225   };
226 };
227 
228 exports.Template = ejs.Template;
229