make the command line ui process hints at least to a basic extent

This commit is contained in:
Joe Walker 2010-12-09 18:22:44 +00:00
commit 1e65d71bde
12 changed files with 911 additions and 90 deletions

View file

@ -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;
});

View file

@ -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; }

View file

@ -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 += '<span class=' + scores[i].toString() + '>';
lastStatus = scores[i];
}
highlightedInput += input.value[i];
i++;
if (i === input.value.length) {
highlightedInput += '</span>';
break;
}
if (lastStatus !== scores[i]) {
highlightedInput += '</span>';
}
}
}
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 + ' &nbsp;-&gt; ' + completion;
}
else {
completer.innerHTML = highlightedInput;
}
hinter.innerHTML = message;
completer.classList.add(worst.status.toString());
// dom.addCssClass(input, worst.status.toString());
}
// return handled;

View file

@ -0,0 +1,47 @@
<!-- The input area, with fixed height -->
<div id=cptInput class=cptInput>
<!-- The prompt -->
<div class="cptPrompt cptGt"><span class="cptBrackets">{ }</span> &gt;</div>
<!-- Where you type commands -->
<div class="cptKbd cptStack">
<div class=cptCompletion save="${cliInputView._completer}"></div>
<input class=cptCliInput save="${cliInputView._inputer}"/>
</div>
<!-- Help as you type -->
<div id=cptHints class=cptHints>
</div>
</div>
<div class=cptRow>
<!-- The div for the input (i.e. what was typed) -->
<div class="cptRowIn" save="${actions.rowin}"
onclick="${actions.copyToInput}"
ondblclick="${actions.executeRequest}">
<!-- What the user actually typed -->
<div class="cptGt">&gt; </div>
<div class="cptOutTyped" save="${actions.typed}"></div>
<!-- The extra details that appear on hover -->
<div class=cptHover save="${actions.duration}"></div>
<img class=cptHover onclick="${actions.hideOutput}" save="${actions.hide}"
alt="Hide command output" _src="${imagePath}/minus.png"/>
<img class="cptHover cptHidden" onclick="${actions.showOutput}" save="${actions.show}"
alt="Show command output" _src="${imagePath}/plus.png"/>
<img class=cptHover onclick="${actions.remove}"
alt="Remove this command from the history" _src="${imagePath}/closer.png"/>
</div>
<!-- The div for the command output -->
<div class="cptRowOut" save="${actions.rowout}">
<div class="cptRowOutput" save="${actions.output}"></div>
<img src="${imagePath}/throbber.gif" save="${actions.throb}"/>
</div>
</div>

View file

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

View file

@ -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 += '<a class="setting" href="' + url +
'" title="View external documentation on setting: ' +
setting.key +
setting.name +
'" target="_blank">' +
setting.key +
setting.name +
'</a> = ' +
setting.value +
'<br/>';

View file

@ -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 <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
*/
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 <x foreach="param in ${array}"> and the special case of
* <loop foreach="param in ${array}">
* @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 <loop> 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:
* <ul>
* <li>property('a.b', { a: { b: 99 }}); // returns 99
* <li>property('a', { a: { b: 99 }}); // returns { b: 99 }
* <li>property('a', { a: { b: 99 }}, 42); // returns 99 and alters the
* input data to be { a: { b: 42 }}
* </ul>
* @param path An array of strings indicating the path through the data, or
* a string to be cut into an array using <tt>split('.')</tt>
* @param data An object to look in for the <tt>path</tt> 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 <tt>path</tt> before any
* <tt>newValue</tt> 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 <tt>env</tt> 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;
}
});

View file

@ -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;
},

View file

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

View file

@ -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;
}
});

View file

@ -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);
}
});

View file

@ -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;
}
});