зеркало из https://github.com/mozilla/gecko-dev.git
Bug 723431 - DOMTemplate should allow customisation of display of null/undefined values; r=dcamp
This commit is contained in:
Родитель
f8d8d51eba
Коммит
6533f5149f
|
@ -42,6 +42,11 @@ var EXPORTED_SYMBOLS = [ "Templater", "template" ];
|
|||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
const Node = Components.interfaces.nsIDOMNode;
|
||||
|
||||
/**
|
||||
* For full documentation, see:
|
||||
* https://github.com/mozilla/domtemplate/blob/master/README.md
|
||||
*/
|
||||
|
||||
// WARNING: do not 'use_strict' without reading the notes in _envEval();
|
||||
|
||||
/**
|
||||
|
@ -50,8 +55,16 @@ const Node = Components.interfaces.nsIDOMNode;
|
|||
* @param data Data to use in filling out the template
|
||||
* @param options Options to customize the template processing. One of:
|
||||
* - allowEval: boolean (default false) Basic template interpolations are
|
||||
* either property paths (e.g. ${a.b.c.d}), however if allowEval=true then we
|
||||
* allow arbitrary JavaScript
|
||||
* either property paths (e.g. ${a.b.c.d}), or if allowEval=true then we
|
||||
* allow arbitrary JavaScript
|
||||
* - stack: string or array of strings (default empty array) The template
|
||||
* 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
|
||||
*/
|
||||
function template(node, data, options) {
|
||||
var template = new Templater(options || {});
|
||||
|
@ -68,7 +81,15 @@ function Templater(options) {
|
|||
options = { allowEval: true };
|
||||
}
|
||||
this.options = options;
|
||||
this.stack = [];
|
||||
if (options.stack && Array.isArray(options.stack)) {
|
||||
this.stack = options.stack;
|
||||
}
|
||||
else if (typeof options.stack === 'string') {
|
||||
this.stack = [ options.stack ];
|
||||
}
|
||||
else {
|
||||
this.stack = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,7 +111,7 @@ Templater.prototype._splitSpecial = /\uF001|\uF002/;
|
|||
* Cached regex used to detect if a script is capable of being interpreted
|
||||
* using Template._property() or if we need to use Template._envEval()
|
||||
*/
|
||||
Templater.prototype._isPropertyScript = /^[a-zA-Z0-9.]*$/;
|
||||
Templater.prototype._isPropertyScript = /^[_a-zA-Z0-9.]*$/;
|
||||
|
||||
/**
|
||||
* Recursive function to walk the tree processing the attributes as it goes.
|
||||
|
@ -153,7 +174,11 @@ Templater.prototype.processNode = function(node, data) {
|
|||
} else {
|
||||
// Replace references in all other attributes
|
||||
var newValue = value.replace(this._templateRegion, function(path) {
|
||||
return this._envEval(path.slice(2, -1), data, value);
|
||||
var insert = this._envEval(path.slice(2, -1), data, value);
|
||||
if (this.options.blankNullUndefined && insert == null) {
|
||||
insert = '';
|
||||
}
|
||||
return insert;
|
||||
}.bind(this));
|
||||
// Remove '_' prefix of attribute names so the DOM won't try
|
||||
// to use them before we've processed the template
|
||||
|
@ -177,7 +202,7 @@ Templater.prototype.processNode = function(node, data) {
|
|||
this.processNode(childNodes[j], data);
|
||||
}
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
|
||||
this._processTextNode(node, data);
|
||||
}
|
||||
} finally {
|
||||
|
@ -347,8 +372,27 @@ Templater.prototype._processTextNode = function(node, data) {
|
|||
part = this._envEval(part.slice(1), data, node.data);
|
||||
}
|
||||
this._handleAsync(part, node, function(reply, siblingNode) {
|
||||
reply = this._toNode(reply, siblingNode.ownerDocument);
|
||||
siblingNode.parentNode.insertBefore(reply, siblingNode);
|
||||
var doc = siblingNode.ownerDocument;
|
||||
if (reply == null) {
|
||||
reply = this.options.blankNullUndefined ? '' : '' + reply;
|
||||
}
|
||||
if (typeof reply.cloneNode === 'function') {
|
||||
// i.e. if (reply instanceof Element) { ...
|
||||
reply = this._maybeImportNode(reply, doc);
|
||||
siblingNode.parentNode.insertBefore(reply, siblingNode);
|
||||
} else if (typeof reply.item === 'function' && reply.length) {
|
||||
// if thing is a NodeList, then import the children
|
||||
for (var i = 0; i < reply.length; i++) {
|
||||
var child = this._maybeImportNode(reply.item(i), doc);
|
||||
siblingNode.parentNode.insertBefore(child, siblingNode);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}.bind(this));
|
||||
}, this);
|
||||
node.parentNode.removeChild(node);
|
||||
|
@ -356,21 +400,13 @@ Templater.prototype._processTextNode = function(node, data) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Helper to convert a 'thing' to a DOM Node.
|
||||
* This is (obviously) a no-op for DOM Elements (which are detected using
|
||||
* 'typeof thing.cloneNode !== "function"' (is there a better way that will
|
||||
* work in all environments, including a .jsm?)
|
||||
* Non DOM elements are converted to a string and wrapped in a TextNode.
|
||||
* Return node or a import of node, if it's not in the given document
|
||||
* @param node The node that we want to be properly owned
|
||||
* @param doc The document that the given node should belong to
|
||||
* @return A node that belongs to the given document
|
||||
*/
|
||||
Templater.prototype._toNode = function(thing, document) {
|
||||
if (thing == null) {
|
||||
thing = '' + thing;
|
||||
}
|
||||
// if thing isn't a DOM element then wrap its string value in one
|
||||
if (typeof thing.cloneNode !== 'function') {
|
||||
thing = document.createTextNode(thing.toString());
|
||||
}
|
||||
return thing;
|
||||
Templater.prototype._maybeImportNode = function(node, doc) {
|
||||
return node.ownerDocument === doc ? node : doc.importNode(node, true);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -429,7 +465,6 @@ Templater.prototype._stripBraces = function(str) {
|
|||
* <tt>newValue</tt> is applied.
|
||||
*/
|
||||
Templater.prototype._property = function(path, data, newValue) {
|
||||
this.stack.push(path);
|
||||
try {
|
||||
if (typeof path === 'string') {
|
||||
path = path.split('.');
|
||||
|
@ -445,12 +480,13 @@ Templater.prototype._property = function(path, data, newValue) {
|
|||
return value;
|
||||
}
|
||||
if (!value) {
|
||||
this._handleError('Can\'t find path=' + path);
|
||||
this._handleError('"' + path[0] + '" is undefined');
|
||||
return null;
|
||||
}
|
||||
return this._property(path.slice(1), value, newValue);
|
||||
} finally {
|
||||
this.stack.pop();
|
||||
} catch (ex) {
|
||||
this._handleError('Path error with \'' + path + '\'', ex);
|
||||
return '${' + path + '}';
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -469,7 +505,7 @@ Templater.prototype._property = function(path, data, newValue) {
|
|||
*/
|
||||
Templater.prototype._envEval = function(script, data, frame) {
|
||||
try {
|
||||
this.stack.push(frame);
|
||||
this.stack.push(frame.replace(/\s+/g, ' '));
|
||||
if (this._isPropertyScript.test(script)) {
|
||||
return this._property(script, data);
|
||||
} else {
|
||||
|
@ -483,8 +519,7 @@ Templater.prototype._envEval = function(script, data, frame) {
|
|||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
this._handleError('Template error evaluating \'' + script + '\'' +
|
||||
' environment=' + Object.keys(data).join(', '), ex);
|
||||
this._handleError('Template error evaluating \'' + script + '\'', ex);
|
||||
return '${' + script + '}';
|
||||
} finally {
|
||||
this.stack.pop();
|
||||
|
@ -498,8 +533,7 @@ Templater.prototype._envEval = function(script, data, frame) {
|
|||
* @param ex optional associated exception.
|
||||
*/
|
||||
Templater.prototype._handleError = function(message, ex) {
|
||||
this._logError(message);
|
||||
this._logError('In: ' + this.stack.join(' > '));
|
||||
this._logError(message + ' (In: ' + this.stack.join(' > ') + ')');
|
||||
if (ex) {
|
||||
this._logError(ex);
|
||||
}
|
||||
|
|
|
@ -3,11 +3,15 @@
|
|||
|
||||
// Tests that the DOM Template engine works properly
|
||||
|
||||
let tempScope = {};
|
||||
Cu.import("resource:///modules/devtools/Templater.jsm", tempScope);
|
||||
Cu.import("resource:///modules/devtools/Promise.jsm", tempScope);
|
||||
let template = tempScope.template;
|
||||
let Promise = tempScope.Promise;
|
||||
/*
|
||||
* These tests run both in Mozilla/Mochitest and plain browsers (as does
|
||||
* domtemplate)
|
||||
* We should endevour to keep the source in sync.
|
||||
*/
|
||||
|
||||
var imports = {};
|
||||
Cu.import("resource:///modules/devtools/Templater.jsm", imports);
|
||||
Cu.import("resource:///modules/devtools/Promise.jsm", imports);
|
||||
|
||||
function test() {
|
||||
addTab("http://example.com/browser/browser/devtools/shared/test/browser_templater_basic.html", function() {
|
||||
|
@ -25,7 +29,7 @@ function runTest(index) {
|
|||
holder.innerHTML = options.template;
|
||||
|
||||
info('Running ' + options.name);
|
||||
template(holder, options.data, options.options);
|
||||
imports.template(holder, options.data, options.options);
|
||||
|
||||
if (typeof options.result == 'string') {
|
||||
is(holder.innerHTML, options.result, options.name);
|
||||
|
@ -238,11 +242,43 @@ var tests = [
|
|||
name: 'propertyFail',
|
||||
template: '<p>${Math.max(1, 2)}</p>',
|
||||
result: '<p>${Math.max(1, 2)}</p>'
|
||||
};},
|
||||
|
||||
// Bug 723431: DOMTemplate should allow customisation of display of
|
||||
// null/undefined values
|
||||
function() { return {
|
||||
name: 'propertyUndefAttrFull',
|
||||
template: '<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>',
|
||||
data: { nullvar: null, undefinedvar1: undefined },
|
||||
result: '<p>null|undefined|undefined</p>'
|
||||
};},
|
||||
|
||||
function() { return {
|
||||
name: 'propertyUndefAttrBlank',
|
||||
template: '<p>${nullvar}|${undefinedvar1}|${undefinedvar2}</p>',
|
||||
data: { nullvar: null, undefinedvar1: undefined },
|
||||
options: { blankNullUndefined: true },
|
||||
result: '<p>||</p>'
|
||||
};},
|
||||
|
||||
function() { return {
|
||||
name: 'propertyUndefAttrFull',
|
||||
template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
|
||||
data: { nullvar: null, undefinedvar1: undefined },
|
||||
result: '<div><p value="null"></p><p value="undefined"></p><p value="undefined"></p></div>'
|
||||
};},
|
||||
|
||||
function() { return {
|
||||
name: 'propertyUndefAttrBlank',
|
||||
template: '<div><p value="${nullvar}"></p><p value="${undefinedvar1}"></p><p value="${undefinedvar2}"></p></div>',
|
||||
data: { nullvar: null, undefinedvar1: undefined },
|
||||
options: { blankNullUndefined: true },
|
||||
result: '<div><p value=""></p><p value=""></p><p value=""></p></div>'
|
||||
};}
|
||||
];
|
||||
|
||||
function delayReply(data) {
|
||||
var p = new Promise();
|
||||
var p = new imports.Promise();
|
||||
executeSoon(function() {
|
||||
p.resolve(data);
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче