--- a/devtools/shared/gcli/Templater.jsm
+++ b/devtools/shared/gcli/Templater.jsm
@@ -9,23 +9,25 @@
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+"use strict";
+
+/* globals document */
+
this.EXPORTED_SYMBOLS = [ "template" ];
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/Console.jsm");
-'use strict';
-
/**
* For full documentation, see:
* https://github.com/mozilla/domtemplate/blob/master/README.md
*/
/**
* Begin a new templating process.
* @param node A DOM element or string referring to an element's id
@@ -38,39 +40,38 @@ XPCOMUtils.defineLazyModuleGetter(this,
* engine maintains a stack of tasks to help debug where it is. This allows
* this stack to be prefixed with a template name
* - blankNullUndefined: By default DOMTemplate exports null and undefined
* values using the strings 'null' and 'undefined', which can be helpful for
* debugging, but can introduce unnecessary extra logic in a template to
* convert null/undefined to ''. By setting blankNullUndefined:true, this
* conversion is handled by DOMTemplate
*/
-var template = function(node, data, options) {
- var state = {
+var template = function (node, data, options) {
+ let state = {
options: options || {},
// We keep a track of the nodes that we've passed through so we can keep
// data.__element pointing to the correct node
nodes: []
};
state.stack = state.options.stack;
if (!Array.isArray(state.stack)) {
- if (typeof state.stack === 'string') {
+ if (typeof state.stack === "string") {
state.stack = [ options.stack ];
- }
- else {
+ } else {
state.stack = [];
}
}
processNode(state, node, data);
};
-if (typeof exports !== 'undefined') {
+if (typeof exports !== "undefined") {
exports.template = template;
}
this.template = template;
/**
* Helper for the places where we need to act asynchronously and keep track of
* where we are right now
*/
@@ -93,221 +94,209 @@ var TEMPLATE_REGION = /\$\{([^}]*)\}/g;
/**
* Recursive function to walk the tree processing the attributes as it goes.
* @param node the node to process. If you pass a string in instead of a DOM
* element, it is assumed to be an id for use with document.getElementById()
* @param data the data to use for node processing.
*/
function processNode(state, node, data) {
- if (typeof node === 'string') {
+ if (typeof node === "string") {
node = document.getElementById(node);
}
if (data == null) {
data = {};
}
- state.stack.push(node.nodeName + (node.id ? '#' + node.id : ''));
- var pushedNode = false;
+ state.stack.push(node.nodeName + (node.id ? "#" + node.id : ""));
+ let pushedNode = false;
try {
// Process attributes
if (node.attributes && node.attributes.length) {
// We need to handle 'foreach' and 'if' first because they might stop
// some types of processing from happening, and foreach must come first
// because it defines new data on which 'if' might depend.
- if (node.hasAttribute('foreach')) {
+ if (node.hasAttribute("foreach")) {
processForEach(state, node, data);
return;
}
- if (node.hasAttribute('if')) {
+ if (node.hasAttribute("if")) {
if (!processIf(state, node, data)) {
return;
}
}
// Only make the node available once we know it's not going away
state.nodes.push(data.__element);
data.__element = node;
pushedNode = true;
// It's good to clean up the attributes when we've processed them,
// but if we do it straight away, we mess up the array index
- var attrs = Array.prototype.slice.call(node.attributes);
- for (var i = 0; i < attrs.length; i++) {
- var value = attrs[i].value;
- var name = attrs[i].name;
+ let attrs = Array.prototype.slice.call(node.attributes);
+ for (let i = 0; i < attrs.length; i++) {
+ let value = attrs[i].value;
+ let name = attrs[i].name;
state.stack.push(name);
try {
- if (name === 'save') {
+ if (name === "save") {
// Save attributes are a setter using the node
value = stripBraces(state, value);
property(state, value, data, node);
- node.removeAttribute('save');
- }
- else if (name.substring(0, 2) === 'on') {
+ node.removeAttribute("save");
+ } else if (name.substring(0, 2) === "on") {
// If this attribute value contains only an expression
- if (value.substring(0, 2) === '${' && value.slice(-1) === '}' &&
- value.indexOf('${', 2) === -1) {
+ if (value.substring(0, 2) === "${" && value.slice(-1) === "}" &&
+ value.indexOf("${", 2) === -1) {
value = stripBraces(state, value);
- var func = property(state, value, data);
- if (typeof func === 'function') {
+ let func = property(state, value, data);
+ if (typeof func === "function") {
node.removeAttribute(name);
- var capture = node.hasAttribute('capture' + name.substring(2));
+ let capture = node.hasAttribute("capture" + name.substring(2));
node.addEventListener(name.substring(2), func, capture);
if (capture) {
- node.removeAttribute('capture' + name.substring(2));
+ node.removeAttribute("capture" + name.substring(2));
}
- }
- else {
+ } else {
// Attribute value is not a function - use as a DOM-L0 string
node.setAttribute(name, func);
}
- }
- else {
+ } else {
// Attribute value is not a single expression use as DOM-L0
node.setAttribute(name, processString(state, value, data));
}
- }
- else {
+ } else {
node.removeAttribute(name);
// Remove '_' prefix of attribute names so the DOM won't try
// to use them before we've processed the template
- if (name.charAt(0) === '_') {
+ if (name.charAt(0) === "_") {
name = name.substring(1);
}
// Async attributes can only work if the whole attribute is async
- var replacement;
- if (value.indexOf('${') === 0 &&
- value.charAt(value.length - 1) === '}') {
+ let replacement;
+ if (value.indexOf("${") === 0 &&
+ value.charAt(value.length - 1) === "}") {
replacement = envEval(state, value.slice(2, -1), data, value);
- if (replacement && typeof replacement.then === 'function') {
- node.setAttribute(name, '');
+ if (replacement && typeof replacement.then === "function") {
+ node.setAttribute(name, "");
/* jshint loopfunc:true */
- replacement.then(function(newValue) {
+ replacement.then(function (newValue) {
node.setAttribute(name, newValue);
}).then(null, console.error);
- }
- else {
+ } else {
if (state.options.blankNullUndefined && replacement == null) {
- replacement = '';
+ replacement = "";
}
node.setAttribute(name, replacement);
}
- }
- else {
+ } else {
node.setAttribute(name, processString(state, value, data));
}
}
- }
- finally {
+ } finally {
state.stack.pop();
}
}
}
// Loop through our children calling processNode. First clone them, so the
// set of nodes that we visit will be unaffected by additions or removals.
- var childNodes = Array.prototype.slice.call(node.childNodes);
- for (var j = 0; j < childNodes.length; j++) {
+ let childNodes = Array.prototype.slice.call(node.childNodes);
+ for (let j = 0; j < childNodes.length; j++) {
processNode(state, childNodes[j], data);
}
- if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
+ /* 3 === Node.TEXT_NODE */
+ if (node.nodeType === 3) {
processTextNode(state, node, data);
}
- }
- finally {
+ } finally {
if (pushedNode) {
data.__element = state.nodes.pop();
}
state.stack.pop();
}
}
/**
* Handle attribute values where the output can only be a string
*/
function processString(state, value, data) {
- return value.replace(TEMPLATE_REGION, function(path) {
- var insert = envEval(state, path.slice(2, -1), data, value);
- return state.options.blankNullUndefined && insert == null ? '' : insert;
+ return value.replace(TEMPLATE_REGION, function (path) {
+ let insert = envEval(state, path.slice(2, -1), data, value);
+ return state.options.blankNullUndefined && insert == null ? "" : insert;
});
}
/**
* Handle <x if="${...}">
* @param node An element with an 'if' attribute
* @param data The data to use with envEval()
* @returns true if processing should continue, false otherwise
*/
function processIf(state, node, data) {
- state.stack.push('if');
+ state.stack.push("if");
try {
- var originalValue = node.getAttribute('if');
- var value = stripBraces(state, originalValue);
- var recurse = true;
+ let originalValue = node.getAttribute("if");
+ let value = stripBraces(state, originalValue);
+ let recurse = true;
try {
- var reply = envEval(state, value, data, originalValue);
+ let reply = envEval(state, value, data, originalValue);
recurse = !!reply;
- }
- catch (ex) {
- handleError(state, 'Error with \'' + value + '\'', ex);
+ } catch (ex) {
+ handleError(state, "Error with '" + value + "'", ex);
recurse = false;
}
if (!recurse) {
node.parentNode.removeChild(node);
}
- node.removeAttribute('if');
+ node.removeAttribute("if");
return recurse;
- }
- finally {
+ } finally {
state.stack.pop();
}
}
/**
* Handle <x foreach="param in ${array}"> and the special case of
* <loop foreach="param in ${array}">.
* This function is responsible for extracting what it has to do from the
* attributes, and getting the data to work on (including resolving promises
* in getting the array). It delegates to processForEachLoop to actually
* unroll the data.
* @param node An element with a 'foreach' attribute
* @param data The data to use with envEval()
*/
function processForEach(state, node, data) {
- state.stack.push('foreach');
+ state.stack.push("foreach");
try {
- var originalValue = node.getAttribute('foreach');
- var value = originalValue;
+ let originalValue = node.getAttribute("foreach");
+ let value = originalValue;
- var paramName = 'param';
- if (value.charAt(0) === '$') {
+ let paramName = "param";
+ if (value.charAt(0) === "$") {
// No custom loop variable name. Use the default: 'param'
value = stripBraces(state, value);
- }
- else {
+ } else {
// Extract the loop variable name from 'NAME in ${ARRAY}'
- var nameArr = value.split(' in ');
+ let nameArr = value.split(" in ");
paramName = nameArr[0].trim();
value = stripBraces(state, nameArr[1].trim());
}
- node.removeAttribute('foreach');
+ node.removeAttribute("foreach");
try {
- var evaled = envEval(state, value, data, originalValue);
- var cState = cloneState(state);
- handleAsync(evaled, node, function(reply, siblingNode) {
+ let evaled = envEval(state, value, data, originalValue);
+ let cState = cloneState(state);
+ handleAsync(evaled, node, function (reply, siblingNode) {
processForEachLoop(cState, reply, node, siblingNode, data, paramName);
});
node.parentNode.removeChild(node);
+ } catch (ex) {
+ handleError(state, "Error with " + value + "'", ex);
}
- catch (ex) {
- handleError(state, 'Error with \'' + value + '\'', ex);
- }
- }
- finally {
+ } finally {
state.stack.pop();
}
}
/**
* Called by processForEach to handle looping over the data in a foreach loop.
* This works with both arrays and objects.
* Calls processForEachMember() for each member of 'set'
@@ -315,23 +304,22 @@ function processForEach(state, node, dat
* @param templNode The node to copy for each set member
* @param sibling The sibling node to which we add things
* @param data the data to use for node processing
* @param paramName foreach loops have a name for the parameter currently being
* processed. The default is 'param'. e.g. <loop foreach="param in ${x}">...
*/
function processForEachLoop(state, set, templNode, sibling, data, paramName) {
if (Array.isArray(set)) {
- set.forEach(function(member, i) {
+ set.forEach(function (member, i) {
processForEachMember(state, member, templNode, sibling,
- data, paramName, '' + i);
+ data, paramName, "" + i);
});
- }
- else {
- for (var member in set) {
+ } else {
+ for (let member in set) {
if (set.hasOwnProperty(member)) {
processForEachMember(state, member, templNode, sibling,
data, paramName, member);
}
}
}
}
@@ -342,102 +330,99 @@ function processForEachLoop(state, set,
* clone the template node, and pass the processing on to processNode().
* @param member The data item to use in templating
* @param templNode The node to copy for each set member
* @param siblingNode The parent node to which we add things
* @param data the data to use for node processing
* @param paramName The name given to 'member' by the foreach attribute
* @param frame A name to push on the stack for debugging
*/
-function processForEachMember(state, member, templNode, siblingNode, data, paramName, frame) {
+function processForEachMember(state, member, templNode, siblingNode, data,
+ paramName, frame) {
state.stack.push(frame);
try {
- var cState = cloneState(state);
- handleAsync(member, siblingNode, function(reply, node) {
+ let cState = cloneState(state);
+ handleAsync(member, siblingNode, function (reply, node) {
// Clone data because we can't be sure that we can safely mutate it
- var newData = Object.create(null);
- Object.keys(data).forEach(function(key) {
+ let newData = Object.create(null);
+ Object.keys(data).forEach(function (key) {
newData[key] = data[key];
});
newData[paramName] = reply;
if (node.parentNode != null) {
- var clone;
- if (templNode.nodeName.toLowerCase() === 'loop') {
- for (var i = 0; i < templNode.childNodes.length; i++) {
+ let clone;
+ if (templNode.nodeName.toLowerCase() === "loop") {
+ for (let i = 0; i < templNode.childNodes.length; i++) {
clone = templNode.childNodes[i].cloneNode(true);
node.parentNode.insertBefore(clone, node);
processNode(cState, clone, newData);
}
- }
- else {
+ } else {
clone = templNode.cloneNode(true);
- clone.removeAttribute('foreach');
+ clone.removeAttribute("foreach");
node.parentNode.insertBefore(clone, node);
processNode(cState, clone, newData);
}
}
});
- }
- finally {
+ } finally {
state.stack.pop();
}
}
/**
* Take a text node and replace it with another text node with the ${...}
* sections parsed out. We replace the node by altering node.parentNode but
* we could probably use a DOM Text API to achieve the same thing.
* @param node The Text node to work on
* @param data The data to use in calls to envEval()
*/
function processTextNode(state, node, data) {
// Replace references in other attributes
- var value = node.data;
+ let value = node.data;
// We can't use the string.replace() with function trick (see generic
// attribute processing in processNode()) because we need to support
// functions that return DOM nodes, so we can't have the conversion to a
// string.
// Instead we process the string as an array of parts. In order to split
// the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
// We can then split using \uF001 or \uF002 to get an array of strings
// where scripts are prefixed with $.
// \uF001 and \uF002 are just unicode chars reserved for private use.
- value = value.replace(TEMPLATE_REGION, '\uF001$$$1\uF002');
+ value = value.replace(TEMPLATE_REGION, "\uF001$$$1\uF002");
// Split a string using the unicode chars F001 and F002.
- var parts = value.split(/\uF001|\uF002/);
+ let parts = value.split(/\uF001|\uF002/);
if (parts.length > 1) {
- parts.forEach(function(part) {
- if (part === null || part === undefined || part === '') {
+ parts.forEach(function (part) {
+ if (part === null || part === undefined || part === "") {
return;
}
- if (part.charAt(0) === '$') {
+ if (part.charAt(0) === "$") {
part = envEval(state, part.slice(1), data, node.data);
}
- var cState = cloneState(state);
- handleAsync(part, node, function(reply, siblingNode) {
- var doc = siblingNode.ownerDocument;
+ let cState = cloneState(state);
+ handleAsync(part, node, function (reply, siblingNode) {
+ let doc = siblingNode.ownerDocument;
if (reply == null) {
- reply = cState.options.blankNullUndefined ? '' : '' + reply;
+ reply = cState.options.blankNullUndefined ? "" : "" + reply;
}
- if (typeof reply.cloneNode === 'function') {
+ if (typeof reply.cloneNode === "function") {
// i.e. if (reply instanceof Element) { ...
reply = maybeImportNode(cState, reply, doc);
siblingNode.parentNode.insertBefore(reply, siblingNode);
- }
- else if (typeof reply.item === 'function' && reply.length) {
+ } else if (typeof reply.item === "function" && reply.length) {
// NodeLists can be live, in which case maybeImportNode can
// remove them from the document, and thus the NodeList, which in
// turn breaks iteration. So first we clone the list
- var list = Array.prototype.slice.call(reply, 0);
- list.forEach(function(child) {
- var imported = maybeImportNode(cState, child, doc);
+ let list = Array.prototype.slice.call(reply, 0);
+ list.forEach(function (child) {
+ let imported = maybeImportNode(cState, child, doc);
siblingNode.parentNode.insertBefore(imported, siblingNode);
});
- }
- else {
+ } else {
// if thing isn't a DOM element then wrap its string value in one
reply = doc.createTextNode(reply.toString());
siblingNode.parentNode.insertBefore(reply, siblingNode);
}
});
});
node.parentNode.removeChild(node);
}
@@ -459,42 +444,41 @@ function maybeImportNode(state, node, do
* an inserter function.
* @param thing The object which could be real data or a promise of real data
* we use it directly if it's not a promise, or resolve it if it is.
* @param siblingNode The element before which we insert new elements.
* @param inserter The function to to the insertion. If thing is not a promise
* then handleAsync() is just 'inserter(thing, siblingNode)'
*/
function handleAsync(thing, siblingNode, inserter) {
- if (thing != null && typeof thing.then === 'function') {
+ if (thing != null && typeof thing.then === "function") {
// Placeholder element to be replaced once we have the real data
- var tempNode = siblingNode.ownerDocument.createElement('span');
+ let tempNode = siblingNode.ownerDocument.createElement("span");
siblingNode.parentNode.insertBefore(tempNode, siblingNode);
- thing.then(function(delayed) {
+ thing.then(function (delayed) {
inserter(delayed, tempNode);
if (tempNode.parentNode != null) {
tempNode.parentNode.removeChild(tempNode);
}
- }).then(null, function(error) {
+ }).then(null, function (error) {
console.error(error.stack);
});
- }
- else {
+ } else {
inserter(thing, siblingNode);
}
}
/**
* Warn of string does not begin '${' and end '}'
* @param str the string to check.
* @return The string stripped of ${ and }, or untouched if it does not match
*/
function stripBraces(state, str) {
if (!str.match(TEMPLATE_REGION)) {
- handleError(state, 'Expected ' + str + ' to match ${...}');
+ handleError(state, "Expected " + str + " to match ${...}");
return str;
}
return str.slice(2, -1);
}
/**
* Combined getter and setter that works with a path through some data set.
* For example:
@@ -509,111 +493,108 @@ function stripBraces(state, str) {
* @param data the data to use for node processing
* @param newValue (optional) If defined, this value will replace the
* original value for the data at the path specified.
* @return The value pointed to by <tt>path</tt> before any
* <tt>newValue</tt> is applied.
*/
function property(state, path, data, newValue) {
try {
- if (typeof path === 'string') {
- path = path.split('.');
+ if (typeof path === "string") {
+ path = path.split(".");
}
- var value = data[path[0]];
+ let value = data[path[0]];
if (path.length === 1) {
if (newValue !== undefined) {
data[path[0]] = newValue;
}
- if (typeof value === 'function') {
+ if (typeof value === "function") {
return value.bind(data);
}
return value;
}
if (!value) {
- handleError(state, '"' + path[0] + '" is undefined');
+ handleError(state, "\"" + path[0] + "\" is undefined");
return null;
}
return property(state, path.slice(1), value, newValue);
- }
- catch (ex) {
- handleError(state, 'Path error with \'' + path + '\'', ex);
- return '${' + path + '}';
+ } catch (ex) {
+ handleError(state, "Path error with '" + path + "'", ex);
+ return "${" + path + "}";
}
}
/**
* Like eval, but that creates a context of the variables in <tt>env</tt> in
* which the script is evaluated.
* @param script The string to be evaluated.
* @param data The environment in which to eval the script.
* @param frame Optional debugging string in case of failure.
* @return The return value of the script, or the error message if the script
* execution failed.
*/
function envEval(state, script, data, frame) {
try {
- state.stack.push(frame.replace(/\s+/g, ' '));
+ state.stack.push(frame.replace(/\s+/g, " "));
// Detect if a script is capable of being interpreted using property()
if (/^[_a-zA-Z0-9.]*$/.test(script)) {
return property(state, script, data);
}
- else {
- if (!state.options.allowEval) {
- handleError(state, 'allowEval is not set, however \'' + script + '\'' +
- ' can not be resolved using a simple property path.');
- return '${' + script + '}';
- }
+ if (!state.options.allowEval) {
+ handleError(state, "allowEval is not set, however '" + script + "'" +
+ " can not be resolved using a simple property path.");
+ return "${" + script + "}";
+ }
- // What we're looking to do is basically:
- // with(data) { return eval(script); }
- // except in strict mode where 'with' is banned.
- // So we create a function which has a parameter list the same as the
- // keys in 'data' and with 'script' as its function body.
- // We then call this function with the values in 'data'
- var keys = allKeys(data);
- var func = Function.apply(null, keys.concat("return " + script));
+ // What we're looking to do is basically:
+ // with(data) { return eval(script); }
+ // except in strict mode where 'with' is banned.
+ // So we create a function which has a parameter list the same as the
+ // keys in 'data' and with 'script' as its function body.
+ // We then call this function with the values in 'data'
+ let keys = allKeys(data);
+ let func = Function.apply(null, keys.concat("return " + script));
- var values = keys.map(function(key) { return data[key]; });
- return func.apply(null, values);
+ let values = keys.map((key) => data[key]);
+ return func.apply(null, values);
- // TODO: The 'with' method is different from the code above in the value
- // of 'this' when calling functions. For example:
- // envEval(state, 'foo()', { foo: function() { return this; } }, ...);
- // The global for 'foo' when using 'with' is the data object. However the
- // code above, the global is null. (Using 'func.apply(data, values)'
- // changes 'this' in the 'foo()' frame, but not in the inside the body
- // of 'foo', so that wouldn't help)
- }
- }
- catch (ex) {
- handleError(state, 'Template error evaluating \'' + script + '\'', ex);
- return '${' + script + '}';
- }
- finally {
+ // TODO: The 'with' method is different from the code above in the value
+ // of 'this' when calling functions. For example:
+ // envEval(state, 'foo()', { foo: function () { return this; } }, ...);
+ // The global for 'foo' when using 'with' is the data object. However the
+ // code above, the global is null. (Using 'func.apply(data, values)'
+ // changes 'this' in the 'foo()' frame, but not in the inside the body
+ // of 'foo', so that wouldn't help)
+ } catch (ex) {
+ handleError(state, "Template error evaluating '" + script + "'", ex);
+ return "${" + script + "}";
+ } finally {
state.stack.pop();
}
}
/**
* Object.keys() that respects the prototype chain
*/
function allKeys(data) {
- var keys = [];
- for (var key in data) { keys.push(key); }
+ let keys = [];
+ for (let key in data) {
+ keys.push(key);
+ }
return keys;
}
/**
* A generic way of reporting errors, for easy overloading in different
* environments.
* @param message the error message to report.
* @param ex optional associated exception.
*/
function handleError(state, message, ex) {
- logError(message + ' (In: ' + state.stack.join(' > ') + ')');
+ logError(message + " (In: " + state.stack.join(" > ") + ")");
if (ex) {
logError(ex);
}
}
/**
* A generic way of reporting errors, for easy overloading in different
* environments.