with raphael

This commit is contained in:
Peter Cottle 2012-09-16 12:47:49 -07:00
parent d0a4d1956c
commit b21f2e536f
3 changed files with 452 additions and 0 deletions

10
lib/raphael-min.js vendored Normal file

File diff suppressed because one or more lines are too long

255
src/commandModel.js Normal file
View file

@ -0,0 +1,255 @@
var Command = Backbone.Model.extend({
defaults: {
status: 'inqueue',
result: '',
error: null,
generalArgs: [],
supportedMap: {},
options: null,
method: null,
createTime: null,
rawStr: null
},
validateAtInit: function() {
if (!this.get('rawStr')) {
throw new Error('Give me a string!');
}
if (!this.get('createTime')) {
this.set('createTime', new Date().toString());
}
this.on('change:error', this.errorChanged, this);
},
initialize: function() {
this.validateAtInit();
this.parseOrCatch();
},
parseOrCatch: function() {
try {
this.parse();
} catch (err) {
if (err instanceof CommandProcessError ||
err instanceof GitError) {
this.set('status', 'error');
this.set('error', err);
} else if (err instanceof CommandResult) {
this.set('status', 'finished');
this.set('error', err);
} else {
throw err;
}
}
},
errorChanged: function(model, err) {
this.set('err', err);
this.set('status', 'error');
this.formatError();
},
formatError: function() {
this.set('result', this.get('err').toResult());
},
getShortcutMap: function() {
return {
'git commit': /^gc($|\s)/,
'git add': /^ga($|\s)/,
'git checkout': /^gchk($|\s)/,
'git rebase': /^gr($|\s)/,
'git branch': /^gb($|\s)/
};
},
getRegexMap: function() {
return {
// ($|\s) means that we either have to end the string
// after the command or there needs to be a space for options
commit: /^commit($|\s)/,
add: /^add($|\s)/,
checkout: /^checkout($|\s)/,
rebase: /^rebase($|\s)/,
reset: /^reset($|\s)/,
branch: /^branch($|\s)/,
revert: /^revert($|\s)/,
merge: /^merge($|\s)/
};
},
getSandboxCommands: function() {
return [
[/^ls/, function() {
throw new CommandResult({
msg: "DontWorryAboutFilesInThisDemo.txt"
});
}],
[/^cd/, function() {
throw new CommandResult({
msg: "Directory Changed to '/directories/dont/matter/in/this/demo'"
});
}],
[/^git$/, function() {
// TODO better git description. also help, hint, etc
throw new CommandResult({
msg: _.escape("\
Git Version \n \
PCOTTLE.1.0 \
Usage: \n \
git <command> [<args>] \
")
});
}]
];
},
parse: function() {
var str = this.get('rawStr');
// first if the string is empty, they just want a blank line
if (!str.length) {
throw new CommandResult({msg: ""});
}
// then check if it's one of our sandbox commands
_.each(this.getSandboxCommands(), function(tuple) {
var regex = tuple[0];
if (regex.exec(str)) {
tuple[1]();
}
});
// then check if shortcut exists, and replace, but
// preserve options if so
_.each(this.getShortcutMap(), function(regex, method) {
var results = regex.exec(str);
if (results) {
str = method + ' ' + str.slice(results[0].length);
}
});
// see if begins with git
if (str.slice(0,3) !== 'git') {
throw new CommandProcessError({
msg: 'Git commands only, sorry!'
});
}
// ok, we have a (probably) valid command. actually parse it
this.gitParse(str);
},
gitParse: function(str) {
// now slice off command part
var fullCommand = str.slice('git '.length);
// see if we support this particular command
_.each(this.getRegexMap(), function(regex, method) {
if (regex.exec(fullCommand)) {
this.set('options', fullCommand.slice(method.length + 1));
this.set('method', method);
// we should stop iterating, but the regex will only match
// one command in practice. we could stop iterating if we used
// jqeurys for each but im using underscore (for no real reason other
// than style)
}
}, this);
if (!this.get('method')) {
throw new CommandProcessError({
msg: "Sorry, this demo does not support that git command: " + fullCommand
});
}
// parse off the options and assemble the map / general args
var optionParser = new OptionParser(this.get('method'), this.get('options'));
// steal these away so we can be completely JSON
this.set('generalArgs', optionParser.generalArgs);
this.set('supportedMap', optionParser.supportedMap);
},
});
/**
* OptionParser
*/
function OptionParser(method, options) {
this.method = method;
this.rawOptions = options;
this.supportedMap = this.getMasterOptionMap()[method];
if (this.supportedMap === undefined) {
throw new Error('No option map for ' + method);
}
this.generalArgs = [];
this.explodeAndSet();
}
OptionParser.prototype.getMasterOptionMap = function() {
// here a value of false means that we support it, even if its just a
// pass-through option. If the value is not here (aka will be undefined
// when accessed), we do not support it.
return {
commit: {
'--amend': false,
'-a': false, // warning
'-am': false
},
add: {},
branch: {
'-d': false,
'-D': false
},
checkout: {
'-b': false
},
reset: {
'--hard': false,
'--soft': false, // this will raise an error but we catch it in gitEngine
},
merge: {},
rebase: {},
revert: {}
};
};
OptionParser.prototype.explodeAndSet = function() {
// split on spaces, except when inside quotes, and strip quotes after.
// for some reason the regex includes the quotes even if i move the parantheses
// inside
var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/g) || [];
_.each(exploded, function(part, i) {
exploded[i] = part.replace(/['"]/g, '');
});
for (var i = 0; i < exploded.length; i++) {
var part = exploded[i];
if (part.slice(0,1) == '-') {
// it's an option, check supportedMap
if (this.supportedMap[part] === undefined) {
throw new CommandProcessError({
msg: 'The option "' + part + '" is not supported'
});
}
// go through and include all the next args until we hit another option or the end
var optionArgs = [];
var next = i + 1;
while (next < exploded.length && exploded[next].slice(0,1) != '-') {
optionArgs.push(exploded[next]);
next += 1;
}
i = next - 1;
// **phew** we are done grabbing those. theseArgs is truthy even with an empty array
this.supportedMap[part] = optionArgs;
} else {
// must be a general arg
this.generalArgs.push(part);
}
}
// done!
};

187
src/commandViews.js Normal file
View file

@ -0,0 +1,187 @@
var CommandPromptView = Backbone.View.extend({
initialize: function(options) {
this.collection = options.collection;
this.commands = [];
this.index = -1;
},
events: {
'keyup #commandTextField': 'keyUp'
},
keyUp: function(e) {
// we need to capture some of these events.
// WARNING: this key map is not internationalized :(
var keyMap = {
// enter
13: _.bind(function() {
this.submit();
}, this),
// up
38: _.bind(function() {
this.commandSelectChange(1);
}, this),
// down
40: _.bind(function() {
this.commandSelectChange(-1);
}, this)
};
if (keyMap[e.which] !== undefined) {
e.preventDefault();
keyMap[e.which]();
}
},
commandSelectChange: function(delta) {
this.index += delta;
// if we are over / under, display blank line. yes this eliminates your
// partially written command, but i doubt that is much in this demo
if (this.index >= this.commands.length || this.index < 0) {
this.clear();
this.index = -1;
return;
}
// yay! we actually can display something
this.setTextField(this.commands[this.index]);
},
setTextField: function(value) {
this.$('#commandTextField').val(value);
},
clear: function() {
this.setTextField('');
},
submit: function() {
var value = this.$('#commandTextField').val().replace('\n', '');
this.clear();
// if we are entering a real command, add it to our history
if (value.length) {
this.commands.unshift(value);
}
this.index = -1;
// split commands on semicolon
_.each(value.split(';'), _.bind(function(command) {
command = command.replace(/^(\s+)/, '');
command = command.replace(/(\s+)$/, '');
if (command.length) {
this.addToCollection(command);
}
}, this));
},
addToCollection: function(value) {
var command = new Command({
rawStr: value
});
this.collection.add(command);
}
});
// This is the view for all commands -- it will represent
// their status (inqueue, processing, finished, error),
// their value ("git commit --amend"),
// and the result (either errors or warnings or whatever)
var CommandView = Backbone.View.extend({
tagName: 'div',
model: Command,
template: _.template($('#command-template').html()),
events: {
'click': 'clicked'
},
clicked: function(e) {
console.log('was clicked');
},
initialize: function() {
this.model.bind('change', this.wasChanged, this);
this.model.bind('destroy', this.remove, this);
},
wasChanged: function(model, changeEvent) {
console.log('command changed', model, changeEvent);
// for changes that are just comestic, we actually only want to toggle classes
// with jquery rather than brutally delete a html of HTML
var changes = changeEvent.changes;
var changeKeys = _.keys(changes);
if (_.difference(changeKeys, ['status']) == 0) {
this.updateStatus();
} else if (_.difference(changeKeys, ['error']) == 0) {
// the above will
this.render();
} else {
this.render();
}
},
updateStatus: function() {
var statuses = ['inqueue', 'processing', 'finished'];
var toggleMap = {};
_.each(statuses, function(status) {
toggleMap[status] = false;
});
toggleMap[this.model.get('status')] = true;
var query = this.$('p.commandLine');
_.each(toggleMap, function(value, key) {
query.toggleClass(key, value);
});
},
render: function() {
var json = _.extend(
{
resultType: '',
result: ''
},
this.model.toJSON()
);
this.$el.html(this.template(json));
return this;
},
remove: function() {
$(this.el).hide();
}
});
var CommandLineHistoryView = Backbone.View.extend({
initialize: function(options) {
this.collection = options.collection;
this.collection.on('add', this.addOne, this);
this.collection.on('reset', this.addAll, this);
this.collection.on('all', this.render, this);
this.collection.on('change', this.scrollDown, this);
},
scrollDown: function() {
var el = $('#commandLineHistory')[0];
el.scrollTop = el.scrollHeight;
},
addOne: function(command) {
var view = new CommandView({
model: command
});
this.$('#commandDisplay').append(view.render().el);
this.scrollDown();
},
addAll: function() {
this.collection.each(this.addOne);
}
});