From 1a1b87789c428e91603048d3d1161ea444cf8927 Mon Sep 17 00:00:00 2001
From: Sean McArthur <sean.monstar@gmail.com>
Date: Tue, 4 Sep 2012 17:25:04 -0700
Subject: [PATCH] /common/js/templates.js gets built dynamically in dev

The list of templates gets pushed onto a single file, with the EJS pre-compiled
into functions. Then, in the Renderer, the functions only need to be invoked,
meaning we no longer need the EJS client lib, and don't eval code.
---
 lib/static/views.js                      |  13 +
 lib/static_resources.js                  |   1 -
 resources/static/common/js/lib/ejs.js    | 514 -----------------------
 resources/static/common/js/renderer.js   |  30 +-
 resources/static/test/mocks/templates.js |   5 +-
 scripts/create_templates.js              |  39 +-
 6 files changed, 53 insertions(+), 549 deletions(-)
 delete mode 100644 resources/static/common/js/lib/ejs.js

diff --git a/lib/static/views.js b/lib/static/views.js
index cd498b0f4..1c1272b91 100644
--- a/lib/static/views.js
+++ b/lib/static/views.js
@@ -230,6 +230,19 @@ exports.setup = function(app) {
     });
   }
 
+  // /common/js/templates.js is dynamically built each time
+  if (!config.get('use_minified_resources')) {
+    var generateTemplates = require('../../scripts/create_templates');
+    var templatesPath = path.join(__dirname, '../../resources/static/dialog/views')
+    var templatesData;
+    app.get('/common/js/templates.js', function(req, res) {
+        var str = generateTemplates(generateTemplates.RETURN, templatesPath);
+        if (str) templatesData = str;
+
+        res.send(templatesData);
+    });
+  }
+
   // REDIRECTS
   const REDIRECTS = {
     "/developers" : "https://developer.mozilla.org/docs/persona"
diff --git a/lib/static_resources.js b/lib/static_resources.js
index 72d9ea339..b4f013d03 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -20,7 +20,6 @@ var common_js = [
   '/common/js/lib/winchan.js',
   '/common/js/lib/underscore.js',
   '/common/js/lib/bidbundle.js',
-  '/common/js/lib/ejs.js',
   '/common/js/lib/micrajax.js',
   '/common/js/lib/urlparse.js',
   '/common/js/lib/gobbledygook.js',
diff --git a/resources/static/common/js/lib/ejs.js b/resources/static/common/js/lib/ejs.js
deleted file mode 100644
index d2396e0bd..000000000
--- a/resources/static/common/js/lib/ejs.js
+++ /dev/null
@@ -1,514 +0,0 @@
-(function(){
-
-
-var rsplit = function(string, regex) {
-	var result = regex.exec(string),retArr = new Array(), first_idx, last_idx, first_bit;
-	while (result != null)
-	{
-		first_idx = result.index; last_idx = regex.lastIndex;
-		if ((first_idx) != 0)
-		{
-			first_bit = string.substring(0,first_idx);
-			retArr.push(string.substring(0,first_idx));
-			string = string.slice(first_idx);
-		}
-		retArr.push(result[0]);
-		string = string.slice(result[0].length);
-		result = regex.exec(string);
-	}
-	if (! string == '')
-	{
-		retArr.push(string);
-	}
-	return retArr;
-},
-chop =  function(string){
-    return string.substr(0, string.length - 1);
-},
-extend = function(d, s){
-    for(var n in s){
-        if(s.hasOwnProperty(n))  d[n] = s[n]
-    }
-}
-
-
-window.EJS = function( options ){
-	options = typeof options == "string" ? {view: options} : options
-    this.set_options(options);
-	if(options.precompiled){
-		this.template = {};
-		this.template.process = options.precompiled;
-		EJS.update(this.name, this);
-		return;
-	}
-    if(options.element)
-	{
-		if(typeof options.element == 'string'){
-			var name = options.element
-			options.element = document.getElementById(  options.element )
-			if(options.element == null) throw name+'does not exist!'
-		}
-		if(options.element.value){
-			this.text = options.element.value
-		}else{
-			this.text = options.element.innerHTML
-		}
-		this.name = options.element.id
-		this.type = '['
-	}else if(options.url){
-        options.url = EJS.endExt(options.url, this.extMatch);
-		this.name = this.name ? this.name : options.url;
-        var url = options.url
-        //options.view = options.absolute_url || options.view || options.;
-		var template = EJS.get(this.name /*url*/, this.cache);
-		if (template) return template;
-	    if (template == EJS.INVALID_PATH) return null;
-        try{
-            this.text = EJS.request( url+(this.cache ? '' : '?'+Math.random() ));
-        }catch(e){}
-
-		if(this.text == null){
-            throw( {type: 'EJS', message: 'There is no template at '+url}  );
-		}
-		//this.name = url;
-	}
-	var template = new EJS.Compiler(this.text, this.type);
-
-	template.compile(options, this.name);
-
-
-	EJS.update(this.name, this);
-	this.template = template;
-};
-/* @Prototype*/
-EJS.prototype = {
-	/**
-	 * Renders an object with extra view helpers attached to the view.
-	 * @param {Object} object data to be rendered
-	 * @param {Object} extra_helpers an object with additonal view helpers
-	 * @return {String} returns the result of the string
-	 */
-    render : function(object, extra_helpers){
-        object = object || {};
-        this._extra_helpers = extra_helpers;
-		var v = new EJS.Helpers(object, extra_helpers || {});
-		return this.template.process.call(object, object,v);
-	},
-    update : function(element, options){
-        if(typeof element == 'string'){
-			element = document.getElementById(element)
-		}
-		if(options == null){
-			_template = this;
-			return function(object){
-				EJS.prototype.update.call(_template, element, object)
-			}
-		}
-		if(typeof options == 'string'){
-			params = {}
-			params.url = options
-			_template = this;
-			params.onComplete = function(request){
-				var object = eval( request.responseText )
-				EJS.prototype.update.call(_template, element, object)
-			}
-			EJS.ajax_request(params)
-		}else
-		{
-			element.innerHTML = this.render(options)
-		}
-    },
-	out : function(){
-		return this.template.out;
-	},
-    /**
-     * Sets options on this view to be rendered with.
-     * @param {Object} options
-     */
-	set_options : function(options){
-        this.type = options.type || EJS.type;
-		this.cache = options.cache != null ? options.cache : EJS.cache;
-		this.text = options.text || null;
-		this.name =  options.name || null;
-		this.ext = options.ext || EJS.ext;
-		this.extMatch = new RegExp(this.ext.replace(/\./, '\.'));
-	}
-};
-EJS.endExt = function(path, match){
-	if(!path) return null;
-	match.lastIndex = 0
-	return path+ (match.test(path) ? '' : this.ext )
-}
-
-
-
-
-/* @Static*/
-EJS.Scanner = function(source, left, right) {
-
-    extend(this,
-        {left_delimiter: 	left +'%',
-         right_delimiter: 	'%'+right,
-         double_left: 		left+'%%',
-         double_right:  	'%%'+right,
-         left_equal: 		left+'%=',
-         // set - Persona addition. The backend understands <%-, which acts
-         // identical to the frontend's <%=.  <%= on the backend escapes
-         // characters to their HTML code equivalents.  For unit testing, we
-         // write backend templates on the front end, so we have to be able to
-         // process <%-.  Creating an alias here.  Using it wherever
-         // left_equal is found.
-         left_dash: 		left+'%-',
-         left_comment: 	left+'%#'})
-
-	this.SplitRegexp = left=='[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('('+this.double_left+')|(%%'+this.double_right+')|('+this.left_equal+')|('+this.left_dash+')|('+this.left_comment+')|('+this.left_delimiter+')|('+this.right_delimiter+'\n)|('+this.right_delimiter+')|(\n)') ;
-
-	this.source = source;
-	this.stag = null;
-	this.lines = 0;
-};
-
-EJS.Scanner.to_text = function(input){
-	if(input == null || input === undefined)
-        return '';
-    if(input instanceof Date)
-		return input.toDateString();
-	if(input.toString)
-        return input.toString();
-	return '';
-};
-
-EJS.Scanner.prototype = {
-  scan: function(block) {
-   var scanline = this.scanline,
-       regex = this.SplitRegexp;
-	 if (! this.source == '')
-	 {
-	 	 var source_split = rsplit(this.source, /\n/);
-	 	 for(var i=0; i<source_split.length; i++) {
-		 	 var item = source_split[i];
-			 this.scanline(item, regex, block);
-		 }
-	 }
-  },
-  scanline: function(line, regex, block) {
-	 this.lines++;
-	 var line_split = rsplit(line, regex);
- 	 for(var i=0; i<line_split.length; i++) {
-	   var token = line_split[i];
-       if (token != null) {
-		   	try{
-	         	block(token, this);
-		 	}catch(e){
-				throw {type: 'EJS.Scanner', line: this.lines};
-			}
-       }
-	 }
-  }
-};
-
-
-EJS.Buffer = function(pre_cmd, post_cmd) {
-	this.line = new Array();
-	this.script = "";
-	this.pre_cmd = pre_cmd;
-	this.post_cmd = post_cmd;
-	for (var i=0; i<this.pre_cmd.length; i++)
-	{
-		this.push(pre_cmd[i]);
-	}
-};
-EJS.Buffer.prototype = {
-
-  push: function(cmd) {
-	this.line.push(cmd);
-  },
-
-  cr: function() {
-	this.script = this.script + this.line.join('; ');
-	this.line = new Array();
-	this.script = this.script + "\n";
-  },
-
-  close: function() {
-	if (this.line.length > 0)
-	{
-		for (var i=0; i<this.post_cmd.length; i++){
-			this.push(pre_cmd[i]);
-		}
-		this.script = this.script + this.line.join('; ');
-		this.line = null;
-	}
-  }
-
-};
-
-
-EJS.Compiler = function(source, left) {
-    this.pre_cmd = ['var ___ViewO = [];'];
-	this.post_cmd = new Array();
-	this.source = ' ';
-	if (source != null)
-	{
-		if (typeof source == 'string')
-		{
-		    source = source.replace(/\r\n/g, "\n");
-            source = source.replace(/\r/g,   "\n");
-			this.source = source;
-		}else if (source.innerHTML){
-			this.source = source.innerHTML;
-		}
-		if (typeof this.source != 'string'){
-			this.source = "";
-		}
-	}
-	left = left || '<';
-	var right = '>';
-	switch(left) {
-		case '[':
-			right = ']';
-			break;
-		case '<':
-			break;
-		default:
-			throw left+' is not a supported deliminator';
-			break;
-	}
-	this.scanner = new EJS.Scanner(this.source, left, right);
-	this.out = '';
-};
-EJS.Compiler.prototype = {
-  compile: function(options, name) {
-  	options = options || {};
-	this.out = '';
-	var put_cmd = "___ViewO.push(";
-	var insert_cmd = put_cmd;
-	var buff = new EJS.Buffer(this.pre_cmd, this.post_cmd);
-	var content = '';
-	var clean = function(content)
-	{
-	    content = content.replace(/\\/g, '\\\\');
-        content = content.replace(/\n/g, '\\n');
-        content = content.replace(/"/g,  '\\"');
-        return content;
-	};
-	this.scanner.scan(function(token, scanner) {
-		if (scanner.stag == null)
-		{
-			switch(token) {
-				case '\n':
-					content = content + "\n";
-					buff.push(put_cmd + '"' + clean(content) + '");');
-					buff.cr();
-					content = '';
-					break;
-				case scanner.left_delimiter:
-				case scanner.left_equal:
-				case scanner.left_dash:
-				case scanner.left_comment:
-					scanner.stag = token;
-					if (content.length > 0)
-					{
-						buff.push(put_cmd + '"' + clean(content) + '")');
-					}
-					content = '';
-					break;
-				case scanner.double_left:
-					content = content + scanner.left_delimiter;
-					break;
-				default:
-					content = content + token;
-					break;
-			}
-		}
-		else {
-			switch(token) {
-				case scanner.right_delimiter:
-					switch(scanner.stag) {
-						case scanner.left_delimiter:
-							if (content[content.length - 1] == '\n')
-							{
-								content = chop(content);
-								buff.push(content);
-								buff.cr();
-							}
-							else {
-								buff.push(content);
-							}
-							break;
-            case scanner.left_dash:
-						case scanner.left_equal:
-							buff.push(insert_cmd + "(EJS.Scanner.to_text(" + content + ")))");
-							break;
-					}
-					scanner.stag = null;
-					content = '';
-					break;
-				case scanner.double_right:
-					content = content + scanner.right_delimiter;
-					break;
-				default:
-					content = content + token;
-					break;
-			}
-		}
-	});
-	if (content.length > 0)
-	{
-		// Chould be content.dump in Ruby
-		buff.push(put_cmd + '"' + clean(content) + '")');
-	}
-	buff.close();
-	this.out = buff.script + ";";
-	var to_be_evaled = '/*'+name+'*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {'+this.out+" return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};";
-
-	try{
-		eval(to_be_evaled);
-	}catch(e){
-		if(typeof JSLINT != 'undefined'){
-			JSLINT(this.out);
-			for(var i = 0; i < JSLINT.errors.length; i++){
-				var error = JSLINT.errors[i];
-				if(error.reason != "Unnecessary semicolon."){
-					error.line++;
-					var e = new Error();
-					e.lineNumber = error.line;
-					e.message = error.reason;
-					if(options.view)
-						e.fileName = options.view;
-					throw e;
-				}
-			}
-		}else{
-			throw e;
-		}
-	}
-  }
-};
-
-
-//type, cache, folder
-/**
- * Sets default options for all views
- * @param {Object} options Set view with the following options
- * <table class="options">
-				<tbody><tr><th>Option</th><th>Default</th><th>Description</th></tr>
-				<tr>
-					<td>type</td>
-					<td>'<'</td>
-					<td>type of magic tags.  Options are '&lt;' or '['
-					</td>
-				</tr>
-				<tr>
-					<td>cache</td>
-					<td>true in production mode, false in other modes</td>
-					<td>true to cache template.
-					</td>
-				</tr>
-	</tbody></table>
- *
- */
-EJS.config = function(options){
-	EJS.cache = options.cache != null ? options.cache : EJS.cache;
-	EJS.type = options.type != null ? options.type : EJS.type;
-	EJS.ext = options.ext != null ? options.ext : EJS.ext;
-
-	var templates_directory = EJS.templates_directory || {}; //nice and private container
-	EJS.templates_directory = templates_directory;
-	EJS.get = function(path, cache){
-		if(cache == false) return null;
-		if(templates_directory[path]) return templates_directory[path];
-  		return null;
-	};
-
-	EJS.update = function(path, template) {
-		if(path == null) return;
-		templates_directory[path] = template ;
-	};
-
-	EJS.INVALID_PATH =  -1;
-};
-EJS.config( {cache: true, type: '<', ext: '.ejs' } );
-
-
-
-/**
- * @constructor
- * By adding functions to EJS.Helpers.prototype, those functions will be available in the
- * views.
- * @init Creates a view helper.  This function is called internally.  You should never call it.
- * @param {Object} data The data passed to the view.  Helpers have access to it through this._data
- */
-EJS.Helpers = function(data, extras){
-	this._data = data;
-    this._extras = extras;
-    extend(this, extras );
-};
-/* @prototype*/
-EJS.Helpers.prototype = {
-    /**
-     * Renders a new view.  If data is passed in, uses that to render the view.
-     * @param {Object} options standard options passed to a new view.
-     * @param {optional:Object} data
-     * @return {String}
-     */
-	view: function(options, data, helpers){
-        if(!helpers) helpers = this._extras
-		if(!data) data = this._data;
-		return new EJS(options).render(data, helpers);
-	},
-    /**
-     * For a given value, tries to create a human representation.
-     * @param {Object} input the value being converted.
-     * @param {Object} null_text what text should be present if input == null or undefined, defaults to ''
-     * @return {String}
-     */
-	to_text: function(input, null_text) {
-	    if(input == null || input === undefined) return null_text || '';
-	    if(input instanceof Date) return input.toDateString();
-		if(input.toString) return input.toString().replace(/\n/g, '<br />').replace(/''/g, "'");
-		return '';
-	}
-};
-    EJS.newRequest = function(){
-	   var factories = [function() { return new ActiveXObject("Msxml2.XMLHTTP"); },function() { return new XMLHttpRequest(); },function() { return new ActiveXObject("Microsoft.XMLHTTP"); }];
-	   for(var i = 0; i < factories.length; i++) {
-	        try {
-	            var request = factories[i]();
-	            if (request != null)  return request;
-	        }
-	        catch(e) { continue;}
-	   }
-	}
-
-	EJS.request = function(path){
-	   var request = new EJS.newRequest()
-	   request.open("GET", path, false);
-
-	   try{request.send(null);}
-	   catch(e){return null;}
-
-	   if ( request.status == 404 || request.status == 2 ||(request.status == 0 && request.responseText == '') ) return null;
-
-	   return request.responseText
-	}
-	EJS.ajax_request = function(params){
-		params.method = ( params.method ? params.method : 'GET')
-
-		var request = new EJS.newRequest();
-		request.onreadystatechange = function(){
-			if(request.readyState == 4){
-				if(request.status == 200){
-					params.onComplete(request)
-				}else
-				{
-					params.onComplete(request)
-				}
-			}
-		}
-		request.open(params.method, params.url)
-		request.send(null)
-	}
-
-
-})();
diff --git a/resources/static/common/js/renderer.js b/resources/static/common/js/renderer.js
index 4f74c0c41..25282c1fc 100644
--- a/resources/static/common/js/renderer.js
+++ b/resources/static/common/js/renderer.js
@@ -7,34 +7,14 @@ BrowserID.Renderer = (function() {
   "use strict";
 
   var bid = BrowserID,
-      dom = bid.DOM,
-      templateCache = {};
+      dom = bid.DOM;
 
   function getTemplateHTML(templateName, vars) {
-    var config,
-        templateText = bid.Templates[templateName],
-        vars = vars || {};
+    var templateFn = bid.Templates[templateName];
+    vars = vars || {};
 
-    if(templateText) {
-      config = {
-        text: templateText
-      };
-    }
-    else {
-      // TODO - be able to set the directory
-      config = {
-        url: "/dialog/views/" + templateName + ".ejs"
-      };
-    }
-
-    var template = templateCache[templateName];
-    if(!template) {
-      template = new EJS(config);
-      templateCache[templateName] = template;
-    }
-
-    var html = template.render(vars);
-    return html;
+    // arguments are: locals, filters (which cant be used client-side), escapeFn
+    return templateFn.call(null, vars);
   }
 
   function render(target, templateName, vars) {
diff --git a/resources/static/test/mocks/templates.js b/resources/static/test/mocks/templates.js
index b41be3c78..5cccd45ee 100644
--- a/resources/static/test/mocks/templates.js
+++ b/resources/static/test/mocks/templates.js
@@ -3,6 +3,7 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 BrowserID.Templates = {
-  inMemoryTemplate: "<div id='templateInput'></div>"
+  inMemoryTemplate: function (locals, filters, escape) {
+    return '<div id="templateInput"></div>'
+  }
 };
-
diff --git a/scripts/create_templates.js b/scripts/create_templates.js
index 831fd61ec..b98009904 100755
--- a/scripts/create_templates.js
+++ b/scripts/create_templates.js
@@ -6,19 +6,26 @@
 
 const
 fs = require("fs"),
-path = require('path');
+path = require('path'),
+ejs = require('ejs');
 
 var dir = process.env.TEMPLATE_DIR || process.cwd();
 var output_dir = process.env.BUILD_DIR || dir;
 
 var templates = {};
 
-function generateTemplates() {
+var lastGen = 0;
+var templateData;
+
+function generateTemplates(outputType, templatesDir) {
+  if (templatesDir) dir = templatesDir;
   var fileNames = fs.readdirSync(dir)
 
   // is a regen even neccesary?
   try {
-    var lastGen = fs.statSync(path.join(output_dir, "templates.js")).mtime;
+    if (outputType !== generateTemplates.RETURN) {
+      lastGen = fs.statSync(path.join(output_dir, "templates.js")).mtime;
+    }
     for (var i = 0; i < fileNames.length; i++) {
       if (lastGen < fs.statSync(path.join(dir, fileNames[i])).mtime) {
         throw "newer";
@@ -26,7 +33,7 @@ function generateTemplates() {
     };
     // no rebuild needed
     console.log("templates.js is up to date");
-    return;
+    return templateData;
   } catch (e) {
     console.log("creating templates.js");
   }
@@ -35,15 +42,33 @@ function generateTemplates() {
     var fileName = fileNames[index];
     if(fileName.match(/\.ejs$/)) {
       var templateName = fileName.replace(/\.ejs/, '');
-      templates[templateName] = fs.readFileSync(dir + "/" + fileName, "utf8")
+      var templateText = fs.readFileSync(dir + "/" + fileName, "utf8");
+
+      templates[templateName] = ejs.compile(templateText, {
+        client: true,
+        compileDebug: true // TODO: make this depend on config
+      });
     }
   }
 
-  var templateData = "BrowserID.Templates =" + JSON.stringify(templates) + ";";
+  var templateData = "BrowserID.Templates = {};";
+  for (var t in templates) {
+    if (templates.hasOwnProperty(t)) {
+      templateData += "\nBrowserID.Templates['" + t + "'] = " + String(templates[t]);
+    }
+  }
 
-  fs.writeFileSync(output_dir + "/templates.js", templateData, "utf8");
+  if (outputType === generateTemplates.RETURN) {
+    lastGen = Date.now();
+    return templateData;
+  } else {
+    fs.writeFileSync(output_dir + "/templates.js", templateData, "utf8");
+  }
 };
 
+generateTemplates.FILE = 0;
+generateTemplates.RETURN = 1;
+
 // run or export the function
 if (process.argv[1] === __filename) generateTemplates();
 else module.exports = generateTemplates;
-- 
GitLab