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 '<' 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