diff --git a/plugins/cockpit/lib/cli.js b/plugins/cockpit/lib/cli.js
index 027ba9f8..24a25a35 100644
--- a/plugins/cockpit/lib/cli.js
+++ b/plugins/cockpit/lib/cli.js
@@ -70,6 +70,50 @@ function Hint(status, message, start, end) {
}
Hint.prototype = {
};
+/**
+ * Loop over the array of hints finding the one we should display.
+ * @param hints array of hints
+ */
+Hint.worst = function(hints, cursor) {
+ if (hints.length === 0) {
+ return undefined;
+ }
+ // Calculate 'distance from cursor'
+ if (cursor !== undefined) {
+ hints.forEach(function(hint) {
+ if (cursor < hint.start) {
+ hint.distance = hint.start - cursor;
+ }
+ else {
+ if (cursor > hint.end) {
+ hint.distance = cursor - hint.end;
+ }
+ else {
+ hint.distance = 0;
+ }
+ }
+ }, this);
+ }
+ // Sort
+ hints.sort(function(hint1, hint2) {
+ // Compare first based on distance from cursor
+ if (cursor !== undefined) {
+ var diff = hint1.distance - hint2.distance;
+ if (diff != 0) {
+ return diff;
+ }
+ }
+ // otherwise go with hint severity
+ return hint1 - hint2;
+ });
+ // tidy-up
+ if (cursor !== undefined) {
+ hints.forEach(function(hint) {
+ delete hint.distance;
+ }, this);
+ }
+ return hints[0];
+};
exports.Hint = Hint;
/**
@@ -816,6 +860,4 @@ function documentCommand(command) {
exports.documentCommand = documentCommand;
-
-
});
diff --git a/plugins/cockpit/lib/ui/plain.css b/plugins/cockpit/lib/ui/plain.css
index 85c55ff1..d6a59f54 100644
--- a/plugins/cockpit/lib/ui/plain.css
+++ b/plugins/cockpit/lib/ui/plain.css
@@ -1,2 +1,181 @@
-#cockpit { color: red; }
+/* Command line completion */
+.cptCompletion {
+ color: #666;
+ padding: 0 0 5px 2px;
+ position: absolute;
+ z-index: -1000;
+}
+
+.cptHints {
+ color: #000;
+ position: absolute;
+ border: 1px solid rgba(230, 230, 230, 0.8);
+ background: rgba(250, 250, 250, 0.8);
+ -moz-border-radius-topleft: 10px;
+ -moz-border-radius-topright: 10px;
+ border-top-left-radius: 10px; border-top-right-radius: 10px;
+ z-index: 1000;
+ padding: 8px;
+}
+
+.cptCompletion.VALID { background: rgba(210, 255, 210, 1); }
+.cptCompletion.INCOMPLETE { background: rgba(255, 230, 210, 1); }
+.cptCompletion.INVALID { background: rgba(255, 210, 210, 1); }
+
+.cptCompletion span { color: #FFF; }
+.cptCompletion span.INCOMPLETE { text-shadow: 0px 0px 2px #ff8800; }
+.cptCompletion span.INVALID { text-shadow: 0px 0px 5px #ff0000; }
+
+
+#cockpit, .cptCompletion {
+ font-family: consolas, courier, monospace;
+ font-size: 120%;
+}
+
+#cockpit {
+ background: transparent;
+ border: none; outline: none;
+}
+
+
+.cptHints article {
+ margin-left: 25px;
+ padding: 8px;
+ z-index: 1;
+ color: @text;
+ display: block;
+ border: 2px @border_bg solid;
+ border-bottom: 0;
+ font-size: 90%;
+ -moz-border-top-colors: @border_bg @border_fg;
+ -moz-border-left-colors: @border_bg @border_fg;
+ -moz-border-right-colors: @border_bg @bg;
+ -moz-border-radius-topleft: 10px;
+ -moz-border-radius-topright: 10px;
+ border-top-left-radius: 10px; border-top-right-radius: 10px;
+ background: @input_bg2;
+ background: -moz-linear-gradient(top, @input_bg_light, @input_bg);
+ background: -webkit-gradient(linear, left top, left bottom, from(@input_bg_light), to(@input_bg));
+ -webkit-box-shadow: rgba(0, 0, 0, 0.5) 0px 1px 10px 3px;
+ -moz-box-shadow: rgba(0, 0, 0, 0.5) 0px 1px 10px 3px;
+ max-width: 500px;
+}
+.cptHints article h1 { font-size: 110%; text-align: center; }
+.cptHints article h2 { font-size: 95%; }
+.cptHints article h3 { font-size: 90%; }
+.cptHints article table th,
+.cptHints article label { color: @hi_text; }
+.cptHints article h1,
+.cptHints article h2,
+.cptHints article h3 { margin: 2px 0 1px 0; color: @hi_text; }
+.cptHints article p,
+.cptHints article pre,
+
+
+
+
+
+/* Global visual styles */
+.cptBrackets { color: @hi_text; }
+.cptGt { color: @theme_text; font-weight: bold; font-size: 120%; }
+
+/* Use absolute positioning to put all children 100%x100% */
+.cptStack {
+ position: relative;
+}
+.cptStack > * {
+ position: absolute;
+ top: 0; height: 100%; bottom: 0; left: 0; width: 100%; right: 0;
+}
+
+
+
+
+
+
+
+
+/* Layout */
+.cptOutput {
+ background: @bg; color: @text;
+ font-family: @fonts; font-size: 90%;
+ overflow: hidden;
+ position: absolute;
+}
+
+/* The input area */
+.cptInput {
+ border-top: 1px solid @border_bg;
+ background: @input_bg;
+ background: -moz-linear-gradient(top, @input_bg, @input_bg2);
+ background: -webkit-gradient(linear, left top, left bottom, from(@input_bg), to(@input_bg2));
+
+ position: absolute;
+ height: 25px; right: 0; bottom: 0; left: 0;
+}
+
+.cptPrompt {
+ width: 40px; padding-left: 5px;
+ position: absolute;
+ top: 0; width: 40px; bottom: 0; left: 0;
+}
+
+.cptKbd {
+ position: absolute;
+ top: 0; right: 0; bottom: 0; left: 40px;
+}
+
+.cptOutput input.cptCliInput {
+ color: @hi_text; font-size: 120%; font-family: @fonts;
+ background: transparent;
+ outline: none; border: 0;
+ box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none;
+ padding: 4px 0 0 0;
+ width: 100%;
+}
+.cptOutput input.cptCliInput:focus {
+ outline: none; border: 0;
+ box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none;
+}
+
+
+
+
+.cptRowIn {
+ display: box; display: -moz-box; display: -webkit-box;
+ box-orient: horizontal; -moz-box-orient: horizontal; -webkit-box-orient: horizontal;
+ box-align: center; -moz-box-align: center; -webkit-box-align: center;
+ color: @text;
+ background-color: @input_bg;
+ width: 100%;
+}
+.cptRowIn > * { padding-left: 2px; padding-right: 2px; }
+.cptRowIn > img { cursor: pointer; }
+.cptHover { display: none; }
+.cptRowIn:hover > .cptHover { display: block; }
+.cptRowIn:hover > .cptHover.cptHidden { display: none; }
+.cptOutTyped {
+ box-flex: 1; -moz-box-flex: 1; -webkit-box-flex: 1;
+ font-weight: bold; color: @hi_text; font-size: 120%;
+}
+.cptRowOutput { padding-left: 10px; line-height: 1.2em; }
+.cptRowOutput strong,
+.cptRowOutput b,
+.cptRowOutput th,
+.cptRowOutput h1,
+.cptRowOutput h2,
+.cptRowOutput h3 { color: @hi_text; }
+.cptRowOutput a { font-weight: bold; color: @link_text; text-decoration: none; }
+.cptRowOutput a: hover { text-decoration: underline; cursor: pointer; }
+.cptRowOutput input[type=password],
+.cptRowOutput input[type=text],
+.cptRowOutput textarea {
+ color: @hi_text; font-size: 120%;
+ background: transparent; padding: 3px;
+ border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px;
+}
+.cptRowOutput table,
+.cptRowOutput td,
+.cptRowOutput th { border: 0; padding: 0 2px; }
+.cptRowOutput .right { text-align: right; }
diff --git a/plugins/cockpit/lib/ui/plain.js b/plugins/cockpit/lib/ui/plain.js
index 445a7864..f6953bf5 100644
--- a/plugins/cockpit/lib/ui/plain.js
+++ b/plugins/cockpit/lib/ui/plain.js
@@ -42,17 +42,88 @@ var dom = require("pilot/dom").dom;
dom.importCssString(editorCss);
var CliRequisition = require('cockpit/cli').CliRequisition;
+var Hint = require('cockpit/cli').Hint;
var keyutil = require('pilot/keyboard/keyutil');
+var plainRow = require("text!cockpit/ui/plainRow.html");
+var Templater = require("pilot/domtemplate").Templater;
+var canon = require("pilot/canon");
+var Status = require('pilot/types').Status;
+
+/**
+ * On startup we need to:
+ * 1. Add 3 sets of elements to the DOM for:
+ * - command line output
+ * - input hints
+ * - completion
+ * 2. Attach a set of events so the command line works
+ */
exports.startup = function(data, reason) {
+ var doc = document;
+ var win = doc.defaultView;
+
+ var cli = new CliRequisition();
+
// TODO: we should have a better way to specify command lines???
- this.input = document.getElementById('cockpit');
- if (!this.input) {
+ var input = doc.getElementById('cockpit');
+ if (!input) {
console.log('No element with an id of cockpit. Bailing on plain cli');
return;
}
- var cli = new CliRequisition();
+ var templates = doc.createElement('dic');
+ templates.innerHTML = plainRow;
+ var row = templates.firstChild;
+
+ var completer = doc.createElement('div');
+ completer.className = 'cptCompletion VALID';
+ input.parentNode.insertBefore(completer, input);
+
+ var hinter = doc.createElement('div');
+ hinter.className = 'cptHints';
+ input.parentNode.insertBefore(hinter, input);
+
+ var output = doc.createElement('div');
+ output.className = 'cptOutput';
+ input.parentNode.insertBefore(output, input);
+
+ function resizer() {
+ var style = win.getComputedStyle(input, null);
+
+ var top = parseInt(style.getPropertyValue('top'), 10);
+ var height = parseInt(style.getPropertyValue('height'), 10);
+ var left = parseInt(style.getPropertyValue('left'), 10);
+ var width = parseInt(style.getPropertyValue('width'), 10);
+
+ completer.style.top = top + 'px';
+ completer.style.height = height + 'px';
+ completer.style.left = left + 'px';
+ completer.style.width = width + 'px';
+
+ hinter.style.bottom = (win.innerHeight - top) + 'px';
+ hinter.style.left = (left + 30) + 'px';
+
+ output.style.bottom = (win.innerHeight - top) + 'px';
+ output.style.left = left + 'px';
+ output.style.width = width + 'px';
+ }
+
+ win.addEventListener('resize', resizer.bind(this), true);
+ resizer();
+
+ // TODO: be less brutal in how we update this
+ output.innerHTML = '';
+ canon.addEventListener('output', function(ev) {
+ ev.requests.forEach(function(request) {
+ request.outputs.forEach(function(out) {
+ if (typeof out === 'string') {
+ output.appendChild(doc.createTextNode(out));
+ } else {
+ output.appendChild(out);
+ }
+ }, this);
+ }, this);
+ }.bind(this));
/*
// All this does is to kill TABs normal use. I wonder if we can train
@@ -69,7 +140,11 @@ exports.startup = function(data, reason) {
}.bind(this));
*/
- this.input.addEventListener('keyup', function(ev) {
+ var NO_HINT = new Hint(Status.VALID, '', 0, 0);
+ var hints = [];
+ var worst;
+
+ input.addEventListener('keyup', function(ev) {
/*
var handled = keyboardManager.processKeyEvent(ev, this, {
isCommandLine: true, isKeyUp: true
@@ -78,16 +153,88 @@ exports.startup = function(data, reason) {
if (ev.keyCode === keyutil.KeyHelper.KEY.RETURN) {
cli.exec();
- this.input.value = '';
+ input.value = '';
} else {
cli.update({
- typed: this.input.value,
+ typed: input.value,
cursor: {
- start: this.input.selectionStart,
- end: this.input.selectionEnd
+ start: input.selectionStart,
+ end: input.selectionEnd
}
});
- console.log(JSON.stringify(cli.getHints()));
+
+ completer.classList.remove(Status.VALID.toString());
+ completer.classList.remove(Status.INCOMPLETE.toString());
+ completer.classList.remove(Status.INVALID.toString());
+
+ // TODO: borked implementation?
+ // dom.removeCssClass(completer, Status.VALID.toString());
+ // dom.removeCssClass(completer, Status.INCOMPLETE.toString());
+ // dom.removeCssClass(completer, Status.INVALID.toString());
+
+ hints = cli.getHints();
+
+ // Create a marked up version of the input
+ var highlightedInput = '';
+ if (input.value.length > 0) {
+ // 'scores' is an array which tells us what chars are errors
+ // Initialize with everything VALID
+ var scores = input.value.split('').map(function(char) {
+ return Status.VALID;
+ });
+ // For all chars in all hints, check and upgrade the score
+ hints.forEach(function(hint) {
+ for (var i = hint.start; i <= hint.end; i++) {
+ if (hint.status > scores[i]) {
+ scores[i] = hint.status;
+ }
+ }
+ }, this);
+ // Create markup
+ var i = 0;
+ var lastStatus = -1;
+ while (true) {
+ if (lastStatus !== scores[i]) {
+ highlightedInput += '';
+ lastStatus = scores[i];
+ }
+ highlightedInput += input.value[i];
+ i++;
+ if (i === input.value.length) {
+ highlightedInput += '';
+ break;
+ }
+ if (lastStatus !== scores[i]) {
+ highlightedInput += '';
+ }
+ }
+ }
+
+ worst = Hint.worst(hints) || NO_HINT;
+ var message = worst.message;
+ if (worst.predictions && worst.predictions.length > 0) {
+ message += ' [ ';
+ worst.predictions.forEach(function(prediction) {
+ if (prediction.name) {
+ message += prediction.name + ' | ';
+ }
+ else {
+ message += prediction + ' | ';
+ }
+ }, this);
+ message = message.replace(/\| $/, ']');
+
+ var completion = worst.predictions[0];
+ completion = completion.name ? completion.name : completion;
+ completer.innerHTML = highlightedInput + ' -> ' + completion;
+ }
+ else {
+ completer.innerHTML = highlightedInput;
+ }
+ hinter.innerHTML = message;
+
+ completer.classList.add(worst.status.toString());
+ // dom.addCssClass(input, worst.status.toString());
}
// return handled;
diff --git a/plugins/cockpit/lib/ui/plainRow.html b/plugins/cockpit/lib/ui/plainRow.html
new file mode 100644
index 00000000..ca40e36c
--- /dev/null
+++ b/plugins/cockpit/lib/ui/plainRow.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
+
+
+
+
+
![Hide command output]()
+
![Show command output]()
+
![Remove this command from the history]()
+
+
+
+
+
+
+

+
+
diff --git a/plugins/pilot/lib/canon.js b/plugins/pilot/lib/canon.js
index aa308f49..dc303de4 100644
--- a/plugins/pilot/lib/canon.js
+++ b/plugins/pilot/lib/canon.js
@@ -113,7 +113,7 @@ var commands = {};
* decoration to be done.
* TODO: Are we sure that in the future there will be no such decoration?
*/
-exports.addCommand = function(command) {
+function addCommand(command) {
if (!command.name) {
throw new Error('All registered commands must have a name');
}
@@ -138,7 +138,7 @@ exports.addCommand = function(command) {
commands[command.name] = command;
};
-exports.removeCommand = function(command) {
+function removeCommand(command) {
if (typeof command === 'string') {
delete commands[command];
}
@@ -147,11 +147,11 @@ exports.removeCommand = function(command) {
}
};
-exports.getCommand = function(name) {
+function getCommand(name) {
return commands[name];
};
-exports.getCommandNames = function() {
+function getCommandNames() {
return Object.keys(commands);
};
@@ -160,7 +160,7 @@ exports.getCommandNames = function() {
* everything it needs to about the command params
* @param command Either a command, or the name of one
*/
-exports.exec = function(command, args) {
+function exec(command, args) {
if (typeof command === 'string') {
command = commands[command];
}
@@ -176,8 +176,15 @@ exports.exec = function(command, args) {
return true;
};
+exports.removeCommand = removeCommand;
+exports.addCommand = addCommand;
+exports.getCommand = getCommand;
+exports.getCommandNames = getCommandNames;
+exports.exec = exec;
+
+
/**
- * We publish a 'addedRequestOutput' event whenever new command begins output
+ * We publish a 'output' event whenever new command begins output
* TODO: make this more obvious
*/
oop.implement(exports, EventEmitter);
@@ -202,7 +209,7 @@ oop.implement(exports, EventEmitter);
/**
* The array of requests that wish to announce their presence
*/
-exports.requests = [];
+var requests = [];
/**
* How many requests do we store?
@@ -212,16 +219,17 @@ var maxRequestLength = 100;
/**
* Called by Request instances when some output (or a cell to async() happens)
*/
-exports.addRequestOutput = function(request) {
- exports.requests.push(request);
+function addRequestOutput(request) {
+ requests.push(request);
// This could probably be optimized with some maths, but 99.99% of the
// time we will only be off by one, and I'm feeling lazy.
- while (exports.requests.length > maxRequestLength) {
- exports.requests.shiftObject();
+ while (requests.length > maxRequestLength) {
+ requests.shiftObject();
}
- exports._dispatchEvent('addedRequestOutput', { request: request });
+ exports._dispatchEvent('output', { requests: requests });
};
+exports.addRequestOutput = addRequestOutput;
/**
* To create an invocation, you need to do something like this (all the ctor
diff --git a/plugins/pilot/lib/commands/settings.js b/plugins/pilot/lib/commands/settings.js
index f2b6100b..efcbc5ea 100644
--- a/plugins/pilot/lib/commands/settings.js
+++ b/plugins/pilot/lib/commands/settings.js
@@ -61,24 +61,24 @@ var setCommandSpec = {
// 'set' by itself lists all the settings
var settingsList = env.settings._list();
html = '';
- // first sort the settingsList based on the key
+ // first sort the settingsList based on the name
settingsList.sort(function(a, b) {
- if (a.key < b.key) {
+ if (a.name < b.name) {
return -1;
- } else if (a.key == b.key) {
+ } else if (a.name == b.name) {
return 0;
} else {
return 1;
}
});
- var url = 'https://wiki.mozilla.org/Labs/Skywriter/Settings#' +
- setting.key;
settingsList.forEach(function(setting) {
+ var url = 'https://wiki.mozilla.org/Labs/Skywriter/Settings#' +
+ setting.name;
html += '' +
- setting.key +
+ setting.name +
' = ' +
setting.value +
'
';
diff --git a/plugins/pilot/lib/domtemplate.js b/plugins/pilot/lib/domtemplate.js
new file mode 100644
index 00000000..809f974a
--- /dev/null
+++ b/plugins/pilot/lib/domtemplate.js
@@ -0,0 +1,406 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is DomTemplate.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com) (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define(function(require, exports, module) {
+
+
+// WARNING: do not 'use_strict' without reading the notes in envEval;
+
+/**
+ * A templater that allows one to quickly template DOM nodes.
+ */
+function Templater() {
+ this.scope = [];
+};
+
+/**
+ * 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.
+ */
+Templater.prototype.processNode = function(node, data) {
+ if (typeof node === 'string') {
+ node = document.getElementById(node);
+ }
+ if (data === null || data === undefined) {
+ data = {};
+ }
+ this.scope.push(node.nodeName + (node.id ? '#' + node.id : ''));
+ 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')) {
+ this.processForEach(node, data);
+ return;
+ }
+ if (node.hasAttribute('if')) {
+ if (!this.processIf(node, data)) {
+ return;
+ }
+ }
+ // Only make the node available once we know it's not going away
+ data.__element = node;
+ // 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;
+ this.scope.push(name);
+ try {
+ if (name === 'save') {
+ // Save attributes are a setter using the node
+ value = this.stripBraces(value);
+ this.property(value, data, node);
+ node.removeAttribute('save');
+ } else if (name.substring(0, 2) === 'on') {
+ // Event registration relies on property doing a bind
+ value = this.stripBraces(value);
+ var func = this.property(value, data);
+ if (typeof func !== 'function') {
+ this.handleError('Expected ' + value +
+ ' to resolve to a function, but got ' + typeof func);
+ }
+ node.removeAttribute(name);
+ var capture = node.hasAttribute('capture' + name.substring(2));
+ node.addEventListener(name.substring(2), func, capture);
+ if (capture) {
+ node.removeAttribute('capture' + name.substring(2));
+ }
+ } else {
+ // Replace references in all other attributes
+ var self = this;
+ var newValue = value.replace(/\$\{[^}]*\}/g, function(path) {
+ return self.envEval(path.slice(2, -1), data, value);
+ });
+ // Remove '_' prefix of attribute names so the DOM won't try
+ // to use them before we've processed the template
+ if (name.charAt(0) === '_') {
+ node.removeAttribute(name);
+ node.setAttribute(name.substring(1), newValue);
+ } else if (value !== newValue) {
+ attrs[i].value = newValue;
+ }
+ }
+ } finally {
+ this.scope.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++) {
+ this.processNode(childNodes[j], data);
+ }
+
+ if (node.nodeType === Node.TEXT_NODE) {
+ this.processTextNode(node, data);
+ }
+ } finally {
+ this.scope.pop();
+ }
+};
+
+/**
+ * Handle
+ * @param node An element with an 'if' attribute
+ * @param data The data to use with envEval
+ * @returns true if processing should continue, false otherwise
+ */
+Templater.prototype.processIf = function(node, data) {
+ this.scope.push('if');
+ try {
+ var originalValue = node.getAttribute('if');
+ var value = this.stripBraces(originalValue);
+ var recurse = true;
+ try {
+ var reply = this.envEval(value, data, originalValue);
+ recurse = !!reply;
+ } catch (ex) {
+ this.handleError('Error with \'' + value + '\'', ex);
+ recurse = false;
+ }
+ if (!recurse) {
+ node.parentNode.removeChild(node);
+ }
+ node.removeAttribute('if');
+ return recurse;
+ } finally {
+ this.scope.pop();
+ }
+};
+
+/**
+ * Handle and the special case of
+ *
+ * @param node An element with a 'foreach' attribute
+ * @param data The data to use with envEval
+ */
+Templater.prototype.processForEach = function(node, data) {
+ this.scope.push('foreach');
+ try {
+ var originalValue = node.getAttribute('foreach');
+ var value = originalValue;
+
+ var paramName = 'param';
+ if (value.charAt(0) === '$') {
+ // No custom loop variable name. Use the default: 'param'
+ value = this.stripBraces(value);
+ } else {
+ // Extract the loop variable name from 'NAME in ${ARRAY}'
+ var nameArr = value.split(' in ');
+ paramName = nameArr[0].trim();
+ value = this.stripBraces(nameArr[1].trim());
+ }
+ node.removeAttribute('foreach');
+ try {
+ var self = this;
+ // Process a single iteration of a loop
+ var processSingle = function(member, clone, ref) {
+ ref.parentNode.insertBefore(clone, ref);
+ data[paramName] = member;
+ self.processNode(clone, data);
+ delete data[paramName];
+ };
+
+ // processSingle is no good for nodes where we want to work on
+ // the childNodes rather than the node itself
+ var processAll = function(scope, member) {
+ self.scope.push(scope);
+ try {
+ if (node.nodeName === 'LOOP') {
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var clone = node.childNodes[i].cloneNode(true);
+ processSingle(member, clone, node);
+ }
+ } else {
+ var clone = node.cloneNode(true);
+ clone.removeAttribute('foreach');
+ processSingle(member, clone, node);
+ }
+ } finally {
+ self.scope.pop();
+ }
+ };
+
+ var reply = this.envEval(value, data, originalValue);
+ if (Array.isArray(reply)) {
+ reply.forEach(function(data, i) {
+ processAll('' + i, data);
+ }, this);
+ } else {
+ for (var param in reply) {
+ if (reply.hasOwnProperty(param)) {
+ processAll(param, param);
+ }
+ }
+ }
+ node.parentNode.removeChild(node);
+ } catch (ex) {
+ this.handleError('Error with \'' + value + '\'', ex);
+ }
+ } finally {
+ this.scope.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
+ */
+Templater.prototype.processTextNode = function(node, data) {
+ // Replace references in other attributes
+ var 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(/\$\{([^}]*)\}/g, '\uF001$$$1\uF002');
+ var parts = value.split(/\uF001|\uF002/);
+ if (parts.length > 1) {
+ parts.forEach(function(part) {
+ if (part === null || part === undefined || part === '') {
+ return;
+ }
+ if (part.charAt(0) === '$') {
+ part = this.envEval(part.slice(1), data, node.data);
+ }
+ // It looks like this was done a few lines above but see envEval
+ if (part === null) {
+ part = "null";
+ }
+ if (part === undefined) {
+ part = "undefined";
+ }
+ // if (isDOMElement(part)) { ... }
+ if (typeof part.cloneNode !== 'function') {
+ part = node.ownerDocument.createTextNode(part.toString());
+ }
+ node.parentNode.insertBefore(part, node);
+ }, this);
+ node.parentNode.removeChild(node);
+ }
+};
+
+/**
+ * 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
+ */
+Templater.prototype.stripBraces = function(str) {
+ if (!str.match(/\$\{.*\}/g)) {
+ this.handleError('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:
+ *
+ * - property('a.b', { a: { b: 99 }}); // returns 99
+ *
- property('a', { a: { b: 99 }}); // returns { b: 99 }
+ *
- property('a', { a: { b: 99 }}, 42); // returns 99 and alters the
+ * input data to be { a: { b: 42 }}
+ *
+ * @param path An array of strings indicating the path through the data, or
+ * a string to be cut into an array using split('.')
+ * @param data An object to look in for the path argument
+ * @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 path before any
+ * newValue is applied.
+ */
+Templater.prototype.property = function(path, data, newValue) {
+ this.scope.push(path);
+ try {
+ if (typeof path === 'string') {
+ path = path.split('.');
+ }
+ var value = data[path[0]];
+ if (path.length === 1) {
+ if (newValue !== undefined) {
+ data[path[0]] = newValue;
+ }
+ if (typeof value === 'function') {
+ return function() {
+ return value.apply(data, arguments);
+ };
+ }
+ return value;
+ }
+ if (!value) {
+ this.handleError('Can\'t find path=' + path);
+ return null;
+ }
+ return this.property(path.slice(1), value, newValue);
+ } finally {
+ this.scope.pop();
+ }
+};
+
+/**
+ * Like eval, but that creates a context of the variables in env in
+ * which the script is evaluated.
+ * WARNING: This script uses 'with' which is generally regarded to be evil.
+ * The alternative is to create a Function at runtime that takes X parameters
+ * according to the X keys in the env object, and then call that function using
+ * the values in the env object. This is likely to be slow, but workable.
+ * @param script The string to be evaluated.
+ * @param env The environment in which to eval the script.
+ * @param context Optional debugging string in case of failure
+ * @return The return value of the script, or the error message if the script
+ * execution failed.
+ */
+Templater.prototype.envEval = function(script, env, context) {
+ with (env) {
+ try {
+ this.scope.push(context);
+ return eval(script);
+ } catch (ex) {
+ this.handleError('Template error evaluating \'' + script + '\'', ex);
+ return script;
+ } finally {
+ this.scope.pop();
+ }
+ }
+};
+
+/**
+ * A generic way of reporting errors, for easy overloading in different
+ * environments.
+ * @param message the error message to report.
+ * @param ex optional associated exception.
+ */
+Templater.prototype.handleError = function(message, ex) {
+ this.logError(message);
+ this.logError('In: ' + this.scope.join(' > '));
+ if (ex) {
+ this.logError(ex);
+ }
+};
+
+
+/**
+ * A generic way of reporting errors, for easy overloading in different
+ * environments.
+ * @param message the error message to report.
+ */
+Templater.prototype.logError = function(message) {
+ console.log(message);
+};
+
+if (this.exports) {
+ exports.Templater = Templater;
+}
+
+
+});
diff --git a/plugins/pilot/lib/settings.js b/plugins/pilot/lib/settings.js
index fbe8b2ab..2024d62f 100644
--- a/plugins/pilot/lib/settings.js
+++ b/plugins/pilot/lib/settings.js
@@ -204,7 +204,7 @@ Settings.prototype = {
resetAll: function() {
this.getSettingNames().forEach(function(key) {
this.resetValue(key);
- }.bind(this));
+ }, this);
},
/**
@@ -215,9 +215,9 @@ Settings.prototype = {
this.getSettingNames().forEach(function(setting) {
reply.push({
'key': setting,
- 'value': this.get(setting)
+ 'value': this.getSetting(setting).get()
});
- }.bind(this));
+ }, this);
return reply;
},
diff --git a/plugins/pilot/lib/types.js b/plugins/pilot/lib/types.js
index 0452b0a6..abc1c4d7 100644
--- a/plugins/pilot/lib/types.js
+++ b/plugins/pilot/lib/types.js
@@ -48,16 +48,21 @@ var Status = {
* valid. There are a number of failure states, so the best way to check
* for failure is (x !== Status.VALID)
*/
- VALID: 'VALID',
+ VALID: {
+ toString: function() { return 'VALID'; },
+ valueOf: function() { return 0; }
+ },
/**
- * The conversion process did not work like Status.INVALID, however it was
- * noted that the string provided to 'parse()' could be VALID by the
- * addition of more characters, so the typing may not be actually incorrect
- * yet, just unfinished.
+ * A conversion process failed, however it was noted that the string
+ * provided to 'parse()' could be VALID by the addition of more characters,
+ * so the typing may not be actually incorrect yet, just unfinished.
* @see Status.INVALID
*/
- INCOMPLETE: 'INCOMPLETE',
+ INCOMPLETE: {
+ toString: function() { return 'INCOMPLETE'; },
+ valueOf: function() { return 1; }
+ },
/**
* The conversion process did not work, the value should be null and a
@@ -65,7 +70,10 @@ var Status = {
* values may be available.
* @see Status.INCOMPLETE
*/
- INVALID: 'INVALID',
+ INVALID: {
+ toString: function() { return 'INVALID'; },
+ valueOf: function() { return 2; }
+ },
/**
* A combined status is the worser of the provided statuses
diff --git a/plugins/pilot/lib/types/basic.js b/plugins/pilot/lib/types/basic.js
index 8e7e5ebc..f6e2adc5 100644
--- a/plugins/pilot/lib/types/basic.js
+++ b/plugins/pilot/lib/types/basic.js
@@ -119,8 +119,8 @@ SelectionType.prototype.stringify = function(value) {
return value;
};
-SelectionType.prototype.parse = function(value) {
- if (typeof value != 'string') {
+SelectionType.prototype.parse = function(str) {
+ if (typeof str != 'string') {
throw new Error('non-string passed to parse()');
}
if (!this.data) {
@@ -128,32 +128,40 @@ SelectionType.prototype.parse = function(value) {
}
var data = (typeof(this.data) === "function") ? this.data() : this.data;
- var match = false;
+ var match;
var completions = [];
data.forEach(function(option) {
- if (value == option) {
- match = true;
+ if (str == option) {
+ match = this.fromString(option);
}
- else if (option.indexOf(value) === 0) {
- completions.push(option);
+ else if (option.indexOf(str) === 0) {
+ completions.push(this.fromString(option));
}
- });
+ }, this);
if (match) {
- return new Conversion(value);
+ return new Conversion(match);
}
else {
- var status = completions.length > 0 ? Status.INCOMPLETE : Status.INVALID;
-
- // TODO: better error message - include options?
- // TODO: better completions - we're just using the extensions
- return new Conversion(null,
- status,
- 'Can\'t convert \'' + value + '\' to a selection.',
- completions);
+ if (completions.length > 0) {
+ return new Conversion(null,
+ Status.INCOMPLETE,
+ 'Several possibilities for \'' + str + '\'',
+ completions);
+ }
+ else {
+ return new Conversion(null,
+ Status.INVALID,
+ 'Can\'t use \'' + str + '\'.',
+ completions);
+ }
}
};
+SelectionType.prototype.fromString = function(str) {
+ return str;
+};
+
SelectionType.prototype.name = 'selection';
/**
@@ -170,17 +178,8 @@ var bool = new SelectionType({
stringify: function(value) {
return '' + value;
},
- parse: function(value) {
- var conversion = SelectionType.prototype.parse(value);
-
- if (conversion.value === 'true') {
- conversion.value = true;
- }
- if (conversion.value === 'false') {
- conversion.value = false;
- }
-
- return conversion;
+ fromString: function(str) {
+ return str === 'true' ? true : false;
}
});
diff --git a/plugins/pilot/lib/types/command.js b/plugins/pilot/lib/types/command.js
index b53fc19d..f29c8bc2 100644
--- a/plugins/pilot/lib/types/command.js
+++ b/plugins/pilot/lib/types/command.js
@@ -54,15 +54,8 @@ var command = new SelectionType({
stringify: function(command) {
return command.name;
},
- parse: function(value) {
- var conversion = SelectionType.prototype.parse.call(this, value);
- if (conversion.value) {
- conversion.value = canon.getCommand(conversion.value);
- }
- else {
- conversion.message = 'Several possibilities for \'' + value + '\'';
- }
- return conversion;
+ fromString: function(str) {
+ return canon.getCommand(str);
}
});
diff --git a/plugins/pilot/lib/types/settings.js b/plugins/pilot/lib/types/settings.js
index 587a1df7..4b64d069 100644
--- a/plugins/pilot/lib/types/settings.js
+++ b/plugins/pilot/lib/types/settings.js
@@ -63,17 +63,9 @@ var setting = new SelectionType({
lastSetting = setting;
return setting.name;
},
- parse: function(text) {
- lastSetting = text;
-
- var conversion = SelectionType.prototype.parse.call(this, text);
- if (conversion.value) {
- conversion.value = settings.getSetting(conversion.value);
- }
- else {
- conversion.message = 'Several possibilities for \'' + text + '\'';
- }
- return conversion;
+ fromString: function(str) {
+ lastSetting = settings.getSetting(str);
+ return lastSetting;
}
});
@@ -84,7 +76,7 @@ var setting = new SelectionType({
var settingValue = new DeferredType({
name: 'settingValue',
defer: function() {
- return env.settings.getSetting(lastSetting).type;
+ return lastSetting.type;
}
});