mirror of
https://github.com/pcottle/learnGitBranching.git
synced 2025-06-25 23:48:34 +02:00
21584 lines
615 KiB
JavaScript
21584 lines
615 KiB
JavaScript
(function(){
|
||
var require = function (file, cwd) {
|
||
var resolved = require.resolve(file, cwd || '/');
|
||
var mod = require.modules[resolved];
|
||
if (!mod) throw new Error(
|
||
'Failed to resolve module ' + file + ', tried ' + resolved
|
||
);
|
||
var cached = require.cache[resolved];
|
||
var res = cached? cached.exports : mod();
|
||
return res;
|
||
};
|
||
|
||
require.paths = [];
|
||
require.modules = {};
|
||
require.cache = {};
|
||
require.extensions = [".js",".coffee",".json"];
|
||
|
||
require._core = {
|
||
'assert': true,
|
||
'events': true,
|
||
'fs': true,
|
||
'path': true,
|
||
'vm': true
|
||
};
|
||
|
||
require.resolve = (function () {
|
||
return function (x, cwd) {
|
||
if (!cwd) cwd = '/';
|
||
|
||
if (require._core[x]) return x;
|
||
var path = require.modules.path();
|
||
cwd = path.resolve('/', cwd);
|
||
var y = cwd || '/';
|
||
|
||
if (x.match(/^(?:\.\.?\/|\/)/)) {
|
||
var m = loadAsFileSync(path.resolve(y, x))
|
||
|| loadAsDirectorySync(path.resolve(y, x));
|
||
if (m) return m;
|
||
}
|
||
|
||
var n = loadNodeModulesSync(x, y);
|
||
if (n) return n;
|
||
|
||
throw new Error("Cannot find module '" + x + "'");
|
||
|
||
function loadAsFileSync (x) {
|
||
x = path.normalize(x);
|
||
if (require.modules[x]) {
|
||
return x;
|
||
}
|
||
|
||
for (var i = 0; i < require.extensions.length; i++) {
|
||
var ext = require.extensions[i];
|
||
if (require.modules[x + ext]) return x + ext;
|
||
}
|
||
}
|
||
|
||
function loadAsDirectorySync (x) {
|
||
x = x.replace(/\/+$/, '');
|
||
var pkgfile = path.normalize(x + '/package.json');
|
||
if (require.modules[pkgfile]) {
|
||
var pkg = require.modules[pkgfile]();
|
||
var b = pkg.browserify;
|
||
if (typeof b === 'object' && b.main) {
|
||
var m = loadAsFileSync(path.resolve(x, b.main));
|
||
if (m) return m;
|
||
}
|
||
else if (typeof b === 'string') {
|
||
var m = loadAsFileSync(path.resolve(x, b));
|
||
if (m) return m;
|
||
}
|
||
else if (pkg.main) {
|
||
var m = loadAsFileSync(path.resolve(x, pkg.main));
|
||
if (m) return m;
|
||
}
|
||
}
|
||
|
||
return loadAsFileSync(x + '/index');
|
||
}
|
||
|
||
function loadNodeModulesSync (x, start) {
|
||
var dirs = nodeModulesPathsSync(start);
|
||
for (var i = 0; i < dirs.length; i++) {
|
||
var dir = dirs[i];
|
||
var m = loadAsFileSync(dir + '/' + x);
|
||
if (m) return m;
|
||
var n = loadAsDirectorySync(dir + '/' + x);
|
||
if (n) return n;
|
||
}
|
||
|
||
var m = loadAsFileSync(x);
|
||
if (m) return m;
|
||
}
|
||
|
||
function nodeModulesPathsSync (start) {
|
||
var parts;
|
||
if (start === '/') parts = [ '' ];
|
||
else parts = path.normalize(start).split('/');
|
||
|
||
var dirs = [];
|
||
for (var i = parts.length - 1; i >= 0; i--) {
|
||
if (parts[i] === 'node_modules') continue;
|
||
var dir = parts.slice(0, i + 1).join('/') + '/node_modules';
|
||
dirs.push(dir);
|
||
}
|
||
|
||
return dirs;
|
||
}
|
||
};
|
||
})();
|
||
|
||
require.alias = function (from, to) {
|
||
var path = require.modules.path();
|
||
var res = null;
|
||
try {
|
||
res = require.resolve(from + '/package.json', '/');
|
||
}
|
||
catch (err) {
|
||
res = require.resolve(from, '/');
|
||
}
|
||
var basedir = path.dirname(res);
|
||
|
||
var keys = (Object.keys || function (obj) {
|
||
var res = [];
|
||
for (var key in obj) res.push(key);
|
||
return res;
|
||
})(require.modules);
|
||
|
||
for (var i = 0; i < keys.length; i++) {
|
||
var key = keys[i];
|
||
if (key.slice(0, basedir.length + 1) === basedir + '/') {
|
||
var f = key.slice(basedir.length);
|
||
require.modules[to + f] = require.modules[basedir + f];
|
||
}
|
||
else if (key === basedir) {
|
||
require.modules[to] = require.modules[basedir];
|
||
}
|
||
}
|
||
};
|
||
|
||
(function () {
|
||
var process = {};
|
||
var global = typeof window !== 'undefined' ? window : {};
|
||
var definedProcess = false;
|
||
|
||
require.define = function (filename, fn) {
|
||
if (!definedProcess && require.modules.__browserify_process) {
|
||
process = require.modules.__browserify_process();
|
||
definedProcess = true;
|
||
}
|
||
|
||
var dirname = require._core[filename]
|
||
? ''
|
||
: require.modules.path().dirname(filename)
|
||
;
|
||
|
||
var require_ = function (file) {
|
||
var requiredModule = require(file, dirname);
|
||
var cached = require.cache[require.resolve(file, dirname)];
|
||
|
||
if (cached && cached.parent === null) {
|
||
cached.parent = module_;
|
||
}
|
||
|
||
return requiredModule;
|
||
};
|
||
require_.resolve = function (name) {
|
||
return require.resolve(name, dirname);
|
||
};
|
||
require_.modules = require.modules;
|
||
require_.define = require.define;
|
||
require_.cache = require.cache;
|
||
var module_ = {
|
||
id : filename,
|
||
filename: filename,
|
||
exports : {},
|
||
loaded : false,
|
||
parent: null
|
||
};
|
||
|
||
require.modules[filename] = function () {
|
||
require.cache[filename] = module_;
|
||
fn.call(
|
||
module_.exports,
|
||
require_,
|
||
module_,
|
||
module_.exports,
|
||
dirname,
|
||
filename,
|
||
process,
|
||
global
|
||
);
|
||
module_.loaded = true;
|
||
return module_.exports;
|
||
};
|
||
};
|
||
})();
|
||
|
||
|
||
require.define("path",function(require,module,exports,__dirname,__filename,process,global){function filter (xs, fn) {
|
||
var res = [];
|
||
for (var i = 0; i < xs.length; i++) {
|
||
if (fn(xs[i], i, xs)) res.push(xs[i]);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
// resolves . and .. elements in a path array with directory names there
|
||
// must be no slashes, empty elements, or device names (c:\) in the array
|
||
// (so also no leading and trailing slashes - it does not distinguish
|
||
// relative and absolute paths)
|
||
function normalizeArray(parts, allowAboveRoot) {
|
||
// if the path tries to go above the root, `up` ends up > 0
|
||
var up = 0;
|
||
for (var i = parts.length; i >= 0; i--) {
|
||
var last = parts[i];
|
||
if (last == '.') {
|
||
parts.splice(i, 1);
|
||
} else if (last === '..') {
|
||
parts.splice(i, 1);
|
||
up++;
|
||
} else if (up) {
|
||
parts.splice(i, 1);
|
||
up--;
|
||
}
|
||
}
|
||
|
||
// if the path is allowed to go above the root, restore leading ..s
|
||
if (allowAboveRoot) {
|
||
for (; up--; up) {
|
||
parts.unshift('..');
|
||
}
|
||
}
|
||
|
||
return parts;
|
||
}
|
||
|
||
// Regex to split a filename into [*, dir, basename, ext]
|
||
// posix version
|
||
var splitPathRe = /^(.+\/(?!$)|\/)?((?:.+?)?(\.[^.]*)?)$/;
|
||
|
||
// path.resolve([from ...], to)
|
||
// posix version
|
||
exports.resolve = function() {
|
||
var resolvedPath = '',
|
||
resolvedAbsolute = false;
|
||
|
||
for (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) {
|
||
var path = (i >= 0)
|
||
? arguments[i]
|
||
: process.cwd();
|
||
|
||
// Skip empty and invalid entries
|
||
if (typeof path !== 'string' || !path) {
|
||
continue;
|
||
}
|
||
|
||
resolvedPath = path + '/' + resolvedPath;
|
||
resolvedAbsolute = path.charAt(0) === '/';
|
||
}
|
||
|
||
// At this point the path should be resolved to a full absolute path, but
|
||
// handle relative paths to be safe (might happen when process.cwd() fails)
|
||
|
||
// Normalize the path
|
||
resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
|
||
return !!p;
|
||
}), !resolvedAbsolute).join('/');
|
||
|
||
return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
|
||
};
|
||
|
||
// path.normalize(path)
|
||
// posix version
|
||
exports.normalize = function(path) {
|
||
var isAbsolute = path.charAt(0) === '/',
|
||
trailingSlash = path.slice(-1) === '/';
|
||
|
||
// Normalize the path
|
||
path = normalizeArray(filter(path.split('/'), function(p) {
|
||
return !!p;
|
||
}), !isAbsolute).join('/');
|
||
|
||
if (!path && !isAbsolute) {
|
||
path = '.';
|
||
}
|
||
if (path && trailingSlash) {
|
||
path += '/';
|
||
}
|
||
|
||
return (isAbsolute ? '/' : '') + path;
|
||
};
|
||
|
||
|
||
// posix version
|
||
exports.join = function() {
|
||
var paths = Array.prototype.slice.call(arguments, 0);
|
||
return exports.normalize(filter(paths, function(p, index) {
|
||
return p && typeof p === 'string';
|
||
}).join('/'));
|
||
};
|
||
|
||
|
||
exports.dirname = function(path) {
|
||
var dir = splitPathRe.exec(path)[1] || '';
|
||
var isWindows = false;
|
||
if (!dir) {
|
||
// No dirname
|
||
return '.';
|
||
} else if (dir.length === 1 ||
|
||
(isWindows && dir.length <= 3 && dir.charAt(1) === ':')) {
|
||
// It is just a slash or a drive letter with a slash
|
||
return dir;
|
||
} else {
|
||
// It is a full dirname, strip trailing slash
|
||
return dir.substring(0, dir.length - 1);
|
||
}
|
||
};
|
||
|
||
|
||
exports.basename = function(path, ext) {
|
||
var f = splitPathRe.exec(path)[2] || '';
|
||
// TODO: make this comparison case-insensitive on windows?
|
||
if (ext && f.substr(-1 * ext.length) === ext) {
|
||
f = f.substr(0, f.length - ext.length);
|
||
}
|
||
return f;
|
||
};
|
||
|
||
|
||
exports.extname = function(path) {
|
||
return splitPathRe.exec(path)[3] || '';
|
||
};
|
||
|
||
});
|
||
|
||
require.define("__browserify_process",function(require,module,exports,__dirname,__filename,process,global){var process = module.exports = {};
|
||
|
||
process.nextTick = (function () {
|
||
var canSetImmediate = typeof window !== 'undefined'
|
||
&& window.setImmediate;
|
||
var canPost = typeof window !== 'undefined'
|
||
&& window.postMessage && window.addEventListener
|
||
;
|
||
|
||
if (canSetImmediate) {
|
||
return function (f) { return window.setImmediate(f) };
|
||
}
|
||
|
||
if (canPost) {
|
||
var queue = [];
|
||
window.addEventListener('message', function (ev) {
|
||
if (ev.source === window && ev.data === 'browserify-tick') {
|
||
ev.stopPropagation();
|
||
if (queue.length > 0) {
|
||
var fn = queue.shift();
|
||
fn();
|
||
}
|
||
}
|
||
}, true);
|
||
|
||
return function nextTick(fn) {
|
||
queue.push(fn);
|
||
window.postMessage('browserify-tick', '*');
|
||
};
|
||
}
|
||
|
||
return function nextTick(fn) {
|
||
setTimeout(fn, 0);
|
||
};
|
||
})();
|
||
|
||
process.title = 'browser';
|
||
process.browser = true;
|
||
process.env = {};
|
||
process.argv = [];
|
||
|
||
process.binding = function (name) {
|
||
if (name === 'evals') return (require)('vm')
|
||
else throw new Error('No such module. (Possibly not yet loaded)')
|
||
};
|
||
|
||
(function () {
|
||
var cwd = '/';
|
||
var path;
|
||
process.cwd = function () { return cwd };
|
||
process.chdir = function (dir) {
|
||
if (!path) path = require('path');
|
||
cwd = path.resolve(dir, cwd);
|
||
};
|
||
})();
|
||
|
||
});
|
||
|
||
require.define("/node_modules/underscore/package.json",function(require,module,exports,__dirname,__filename,process,global){module.exports = {"main":"underscore.js"}
|
||
});
|
||
|
||
require.define("/node_modules/underscore/underscore.js",function(require,module,exports,__dirname,__filename,process,global){// Underscore.js 1.4.3
|
||
// http://underscorejs.org
|
||
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||
// Underscore may be freely distributed under the MIT license.
|
||
|
||
(function() {
|
||
|
||
// Baseline setup
|
||
// --------------
|
||
|
||
// Establish the root object, `window` in the browser, or `global` on the server.
|
||
var root = this;
|
||
|
||
// Save the previous value of the `_` variable.
|
||
var previousUnderscore = root._;
|
||
|
||
// Establish the object that gets returned to break out of a loop iteration.
|
||
var breaker = {};
|
||
|
||
// Save bytes in the minified (but not gzipped) version:
|
||
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
|
||
|
||
// Create quick reference variables for speed access to core prototypes.
|
||
var push = ArrayProto.push,
|
||
slice = ArrayProto.slice,
|
||
concat = ArrayProto.concat,
|
||
toString = ObjProto.toString,
|
||
hasOwnProperty = ObjProto.hasOwnProperty;
|
||
|
||
// All **ECMAScript 5** native function implementations that we hope to use
|
||
// are declared here.
|
||
var
|
||
nativeForEach = ArrayProto.forEach,
|
||
nativeMap = ArrayProto.map,
|
||
nativeReduce = ArrayProto.reduce,
|
||
nativeReduceRight = ArrayProto.reduceRight,
|
||
nativeFilter = ArrayProto.filter,
|
||
nativeEvery = ArrayProto.every,
|
||
nativeSome = ArrayProto.some,
|
||
nativeIndexOf = ArrayProto.indexOf,
|
||
nativeLastIndexOf = ArrayProto.lastIndexOf,
|
||
nativeIsArray = Array.isArray,
|
||
nativeKeys = Object.keys,
|
||
nativeBind = FuncProto.bind;
|
||
|
||
// Create a safe reference to the Underscore object for use below.
|
||
var _ = function(obj) {
|
||
if (obj instanceof _) return obj;
|
||
if (!(this instanceof _)) return new _(obj);
|
||
this._wrapped = obj;
|
||
};
|
||
|
||
// Export the Underscore object for **Node.js**, with
|
||
// backwards-compatibility for the old `require()` API. If we're in
|
||
// the browser, add `_` as a global object via a string identifier,
|
||
// for Closure Compiler "advanced" mode.
|
||
if (typeof exports !== 'undefined') {
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
exports = module.exports = _;
|
||
}
|
||
exports._ = _;
|
||
} else {
|
||
root._ = _;
|
||
}
|
||
|
||
// Current version.
|
||
_.VERSION = '1.4.3';
|
||
|
||
// Collection Functions
|
||
// --------------------
|
||
|
||
// The cornerstone, an `each` implementation, aka `forEach`.
|
||
// Handles objects with the built-in `forEach`, arrays, and raw objects.
|
||
// Delegates to **ECMAScript 5**'s native `forEach` if available.
|
||
var each = _.each = _.forEach = function(obj, iterator, context) {
|
||
if (obj == null) return;
|
||
if (nativeForEach && obj.forEach === nativeForEach) {
|
||
obj.forEach(iterator, context);
|
||
} else if (obj.length === +obj.length) {
|
||
for (var i = 0, l = obj.length; i < l; i++) {
|
||
if (iterator.call(context, obj[i], i, obj) === breaker) return;
|
||
}
|
||
} else {
|
||
for (var key in obj) {
|
||
if (_.has(obj, key)) {
|
||
if (iterator.call(context, obj[key], key, obj) === breaker) return;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Return the results of applying the iterator to each element.
|
||
// Delegates to **ECMAScript 5**'s native `map` if available.
|
||
_.map = _.collect = function(obj, iterator, context) {
|
||
var results = [];
|
||
if (obj == null) return results;
|
||
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
|
||
each(obj, function(value, index, list) {
|
||
results[results.length] = iterator.call(context, value, index, list);
|
||
});
|
||
return results;
|
||
};
|
||
|
||
var reduceError = 'Reduce of empty array with no initial value';
|
||
|
||
// **Reduce** builds up a single result from a list of values, aka `inject`,
|
||
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
|
||
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
|
||
var initial = arguments.length > 2;
|
||
if (obj == null) obj = [];
|
||
if (nativeReduce && obj.reduce === nativeReduce) {
|
||
if (context) iterator = _.bind(iterator, context);
|
||
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
|
||
}
|
||
each(obj, function(value, index, list) {
|
||
if (!initial) {
|
||
memo = value;
|
||
initial = true;
|
||
} else {
|
||
memo = iterator.call(context, memo, value, index, list);
|
||
}
|
||
});
|
||
if (!initial) throw new TypeError(reduceError);
|
||
return memo;
|
||
};
|
||
|
||
// The right-associative version of reduce, also known as `foldr`.
|
||
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
|
||
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
|
||
var initial = arguments.length > 2;
|
||
if (obj == null) obj = [];
|
||
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
|
||
if (context) iterator = _.bind(iterator, context);
|
||
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
|
||
}
|
||
var length = obj.length;
|
||
if (length !== +length) {
|
||
var keys = _.keys(obj);
|
||
length = keys.length;
|
||
}
|
||
each(obj, function(value, index, list) {
|
||
index = keys ? keys[--length] : --length;
|
||
if (!initial) {
|
||
memo = obj[index];
|
||
initial = true;
|
||
} else {
|
||
memo = iterator.call(context, memo, obj[index], index, list);
|
||
}
|
||
});
|
||
if (!initial) throw new TypeError(reduceError);
|
||
return memo;
|
||
};
|
||
|
||
// Return the first value which passes a truth test. Aliased as `detect`.
|
||
_.find = _.detect = function(obj, iterator, context) {
|
||
var result;
|
||
any(obj, function(value, index, list) {
|
||
if (iterator.call(context, value, index, list)) {
|
||
result = value;
|
||
return true;
|
||
}
|
||
});
|
||
return result;
|
||
};
|
||
|
||
// Return all the elements that pass a truth test.
|
||
// Delegates to **ECMAScript 5**'s native `filter` if available.
|
||
// Aliased as `select`.
|
||
_.filter = _.select = function(obj, iterator, context) {
|
||
var results = [];
|
||
if (obj == null) return results;
|
||
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
|
||
each(obj, function(value, index, list) {
|
||
if (iterator.call(context, value, index, list)) results[results.length] = value;
|
||
});
|
||
return results;
|
||
};
|
||
|
||
// Return all the elements for which a truth test fails.
|
||
_.reject = function(obj, iterator, context) {
|
||
return _.filter(obj, function(value, index, list) {
|
||
return !iterator.call(context, value, index, list);
|
||
}, context);
|
||
};
|
||
|
||
// Determine whether all of the elements match a truth test.
|
||
// Delegates to **ECMAScript 5**'s native `every` if available.
|
||
// Aliased as `all`.
|
||
_.every = _.all = function(obj, iterator, context) {
|
||
iterator || (iterator = _.identity);
|
||
var result = true;
|
||
if (obj == null) return result;
|
||
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
|
||
each(obj, function(value, index, list) {
|
||
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
|
||
});
|
||
return !!result;
|
||
};
|
||
|
||
// Determine if at least one element in the object matches a truth test.
|
||
// Delegates to **ECMAScript 5**'s native `some` if available.
|
||
// Aliased as `any`.
|
||
var any = _.some = _.any = function(obj, iterator, context) {
|
||
iterator || (iterator = _.identity);
|
||
var result = false;
|
||
if (obj == null) return result;
|
||
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
|
||
each(obj, function(value, index, list) {
|
||
if (result || (result = iterator.call(context, value, index, list))) return breaker;
|
||
});
|
||
return !!result;
|
||
};
|
||
|
||
// Determine if the array or object contains a given value (using `===`).
|
||
// Aliased as `include`.
|
||
_.contains = _.include = function(obj, target) {
|
||
if (obj == null) return false;
|
||
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
|
||
return any(obj, function(value) {
|
||
return value === target;
|
||
});
|
||
};
|
||
|
||
// Invoke a method (with arguments) on every item in a collection.
|
||
_.invoke = function(obj, method) {
|
||
var args = slice.call(arguments, 2);
|
||
return _.map(obj, function(value) {
|
||
return (_.isFunction(method) ? method : value[method]).apply(value, args);
|
||
});
|
||
};
|
||
|
||
// Convenience version of a common use case of `map`: fetching a property.
|
||
_.pluck = function(obj, key) {
|
||
return _.map(obj, function(value){ return value[key]; });
|
||
};
|
||
|
||
// Convenience version of a common use case of `filter`: selecting only objects
|
||
// with specific `key:value` pairs.
|
||
_.where = function(obj, attrs) {
|
||
if (_.isEmpty(attrs)) return [];
|
||
return _.filter(obj, function(value) {
|
||
for (var key in attrs) {
|
||
if (attrs[key] !== value[key]) return false;
|
||
}
|
||
return true;
|
||
});
|
||
};
|
||
|
||
// Return the maximum element or (element-based computation).
|
||
// Can't optimize arrays of integers longer than 65,535 elements.
|
||
// See: https://bugs.webkit.org/show_bug.cgi?id=80797
|
||
_.max = function(obj, iterator, context) {
|
||
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
|
||
return Math.max.apply(Math, obj);
|
||
}
|
||
if (!iterator && _.isEmpty(obj)) return -Infinity;
|
||
var result = {computed : -Infinity, value: -Infinity};
|
||
each(obj, function(value, index, list) {
|
||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||
computed >= result.computed && (result = {value : value, computed : computed});
|
||
});
|
||
return result.value;
|
||
};
|
||
|
||
// Return the minimum element (or element-based computation).
|
||
_.min = function(obj, iterator, context) {
|
||
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
|
||
return Math.min.apply(Math, obj);
|
||
}
|
||
if (!iterator && _.isEmpty(obj)) return Infinity;
|
||
var result = {computed : Infinity, value: Infinity};
|
||
each(obj, function(value, index, list) {
|
||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||
computed < result.computed && (result = {value : value, computed : computed});
|
||
});
|
||
return result.value;
|
||
};
|
||
|
||
// Shuffle an array.
|
||
_.shuffle = function(obj) {
|
||
var rand;
|
||
var index = 0;
|
||
var shuffled = [];
|
||
each(obj, function(value) {
|
||
rand = _.random(index++);
|
||
shuffled[index - 1] = shuffled[rand];
|
||
shuffled[rand] = value;
|
||
});
|
||
return shuffled;
|
||
};
|
||
|
||
// An internal function to generate lookup iterators.
|
||
var lookupIterator = function(value) {
|
||
return _.isFunction(value) ? value : function(obj){ return obj[value]; };
|
||
};
|
||
|
||
// Sort the object's values by a criterion produced by an iterator.
|
||
_.sortBy = function(obj, value, context) {
|
||
var iterator = lookupIterator(value);
|
||
return _.pluck(_.map(obj, function(value, index, list) {
|
||
return {
|
||
value : value,
|
||
index : index,
|
||
criteria : iterator.call(context, value, index, list)
|
||
};
|
||
}).sort(function(left, right) {
|
||
var a = left.criteria;
|
||
var b = right.criteria;
|
||
if (a !== b) {
|
||
if (a > b || a === void 0) return 1;
|
||
if (a < b || b === void 0) return -1;
|
||
}
|
||
return left.index < right.index ? -1 : 1;
|
||
}), 'value');
|
||
};
|
||
|
||
// An internal function used for aggregate "group by" operations.
|
||
var group = function(obj, value, context, behavior) {
|
||
var result = {};
|
||
var iterator = lookupIterator(value || _.identity);
|
||
each(obj, function(value, index) {
|
||
var key = iterator.call(context, value, index, obj);
|
||
behavior(result, key, value);
|
||
});
|
||
return result;
|
||
};
|
||
|
||
// Groups the object's values by a criterion. Pass either a string attribute
|
||
// to group by, or a function that returns the criterion.
|
||
_.groupBy = function(obj, value, context) {
|
||
return group(obj, value, context, function(result, key, value) {
|
||
(_.has(result, key) ? result[key] : (result[key] = [])).push(value);
|
||
});
|
||
};
|
||
|
||
// Counts instances of an object that group by a certain criterion. Pass
|
||
// either a string attribute to count by, or a function that returns the
|
||
// criterion.
|
||
_.countBy = function(obj, value, context) {
|
||
return group(obj, value, context, function(result, key) {
|
||
if (!_.has(result, key)) result[key] = 0;
|
||
result[key]++;
|
||
});
|
||
};
|
||
|
||
// Use a comparator function to figure out the smallest index at which
|
||
// an object should be inserted so as to maintain order. Uses binary search.
|
||
_.sortedIndex = function(array, obj, iterator, context) {
|
||
iterator = iterator == null ? _.identity : lookupIterator(iterator);
|
||
var value = iterator.call(context, obj);
|
||
var low = 0, high = array.length;
|
||
while (low < high) {
|
||
var mid = (low + high) >>> 1;
|
||
iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
|
||
}
|
||
return low;
|
||
};
|
||
|
||
// Safely convert anything iterable into a real, live array.
|
||
_.toArray = function(obj) {
|
||
if (!obj) return [];
|
||
if (_.isArray(obj)) return slice.call(obj);
|
||
if (obj.length === +obj.length) return _.map(obj, _.identity);
|
||
return _.values(obj);
|
||
};
|
||
|
||
// Return the number of elements in an object.
|
||
_.size = function(obj) {
|
||
if (obj == null) return 0;
|
||
return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
|
||
};
|
||
|
||
// Array Functions
|
||
// ---------------
|
||
|
||
// Get the first element of an array. Passing **n** will return the first N
|
||
// values in the array. Aliased as `head` and `take`. The **guard** check
|
||
// allows it to work with `_.map`.
|
||
_.first = _.head = _.take = function(array, n, guard) {
|
||
if (array == null) return void 0;
|
||
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
|
||
};
|
||
|
||
// Returns everything but the last entry of the array. Especially useful on
|
||
// the arguments object. Passing **n** will return all the values in
|
||
// the array, excluding the last N. The **guard** check allows it to work with
|
||
// `_.map`.
|
||
_.initial = function(array, n, guard) {
|
||
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
|
||
};
|
||
|
||
// Get the last element of an array. Passing **n** will return the last N
|
||
// values in the array. The **guard** check allows it to work with `_.map`.
|
||
_.last = function(array, n, guard) {
|
||
if (array == null) return void 0;
|
||
if ((n != null) && !guard) {
|
||
return slice.call(array, Math.max(array.length - n, 0));
|
||
} else {
|
||
return array[array.length - 1];
|
||
}
|
||
};
|
||
|
||
// Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
|
||
// Especially useful on the arguments object. Passing an **n** will return
|
||
// the rest N values in the array. The **guard**
|
||
// check allows it to work with `_.map`.
|
||
_.rest = _.tail = _.drop = function(array, n, guard) {
|
||
return slice.call(array, (n == null) || guard ? 1 : n);
|
||
};
|
||
|
||
// Trim out all falsy values from an array.
|
||
_.compact = function(array) {
|
||
return _.filter(array, _.identity);
|
||
};
|
||
|
||
// Internal implementation of a recursive `flatten` function.
|
||
var flatten = function(input, shallow, output) {
|
||
each(input, function(value) {
|
||
if (_.isArray(value)) {
|
||
shallow ? push.apply(output, value) : flatten(value, shallow, output);
|
||
} else {
|
||
output.push(value);
|
||
}
|
||
});
|
||
return output;
|
||
};
|
||
|
||
// Return a completely flattened version of an array.
|
||
_.flatten = function(array, shallow) {
|
||
return flatten(array, shallow, []);
|
||
};
|
||
|
||
// Return a version of the array that does not contain the specified value(s).
|
||
_.without = function(array) {
|
||
return _.difference(array, slice.call(arguments, 1));
|
||
};
|
||
|
||
// Produce a duplicate-free version of the array. If the array has already
|
||
// been sorted, you have the option of using a faster algorithm.
|
||
// Aliased as `unique`.
|
||
_.uniq = _.unique = function(array, isSorted, iterator, context) {
|
||
if (_.isFunction(isSorted)) {
|
||
context = iterator;
|
||
iterator = isSorted;
|
||
isSorted = false;
|
||
}
|
||
var initial = iterator ? _.map(array, iterator, context) : array;
|
||
var results = [];
|
||
var seen = [];
|
||
each(initial, function(value, index) {
|
||
if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
|
||
seen.push(value);
|
||
results.push(array[index]);
|
||
}
|
||
});
|
||
return results;
|
||
};
|
||
|
||
// Produce an array that contains the union: each distinct element from all of
|
||
// the passed-in arrays.
|
||
_.union = function() {
|
||
return _.uniq(concat.apply(ArrayProto, arguments));
|
||
};
|
||
|
||
// Produce an array that contains every item shared between all the
|
||
// passed-in arrays.
|
||
_.intersection = function(array) {
|
||
var rest = slice.call(arguments, 1);
|
||
return _.filter(_.uniq(array), function(item) {
|
||
return _.every(rest, function(other) {
|
||
return _.indexOf(other, item) >= 0;
|
||
});
|
||
});
|
||
};
|
||
|
||
// Take the difference between one array and a number of other arrays.
|
||
// Only the elements present in just the first array will remain.
|
||
_.difference = function(array) {
|
||
var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
|
||
return _.filter(array, function(value){ return !_.contains(rest, value); });
|
||
};
|
||
|
||
// Zip together multiple lists into a single array -- elements that share
|
||
// an index go together.
|
||
_.zip = function() {
|
||
var args = slice.call(arguments);
|
||
var length = _.max(_.pluck(args, 'length'));
|
||
var results = new Array(length);
|
||
for (var i = 0; i < length; i++) {
|
||
results[i] = _.pluck(args, "" + i);
|
||
}
|
||
return results;
|
||
};
|
||
|
||
// Converts lists into objects. Pass either a single array of `[key, value]`
|
||
// pairs, or two parallel arrays of the same length -- one of keys, and one of
|
||
// the corresponding values.
|
||
_.object = function(list, values) {
|
||
if (list == null) return {};
|
||
var result = {};
|
||
for (var i = 0, l = list.length; i < l; i++) {
|
||
if (values) {
|
||
result[list[i]] = values[i];
|
||
} else {
|
||
result[list[i][0]] = list[i][1];
|
||
}
|
||
}
|
||
return result;
|
||
};
|
||
|
||
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
|
||
// we need this function. Return the position of the first occurrence of an
|
||
// item in an array, or -1 if the item is not included in the array.
|
||
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
|
||
// If the array is large and already in sort order, pass `true`
|
||
// for **isSorted** to use binary search.
|
||
_.indexOf = function(array, item, isSorted) {
|
||
if (array == null) return -1;
|
||
var i = 0, l = array.length;
|
||
if (isSorted) {
|
||
if (typeof isSorted == 'number') {
|
||
i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
|
||
} else {
|
||
i = _.sortedIndex(array, item);
|
||
return array[i] === item ? i : -1;
|
||
}
|
||
}
|
||
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
|
||
for (; i < l; i++) if (array[i] === item) return i;
|
||
return -1;
|
||
};
|
||
|
||
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
|
||
_.lastIndexOf = function(array, item, from) {
|
||
if (array == null) return -1;
|
||
var hasIndex = from != null;
|
||
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
|
||
return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
|
||
}
|
||
var i = (hasIndex ? from : array.length);
|
||
while (i--) if (array[i] === item) return i;
|
||
return -1;
|
||
};
|
||
|
||
// Generate an integer Array containing an arithmetic progression. A port of
|
||
// the native Python `range()` function. See
|
||
// [the Python documentation](http://docs.python.org/library/functions.html#range).
|
||
_.range = function(start, stop, step) {
|
||
if (arguments.length <= 1) {
|
||
stop = start || 0;
|
||
start = 0;
|
||
}
|
||
step = arguments[2] || 1;
|
||
|
||
var len = Math.max(Math.ceil((stop - start) / step), 0);
|
||
var idx = 0;
|
||
var range = new Array(len);
|
||
|
||
while(idx < len) {
|
||
range[idx++] = start;
|
||
start += step;
|
||
}
|
||
|
||
return range;
|
||
};
|
||
|
||
// Function (ahem) Functions
|
||
// ------------------
|
||
|
||
// Reusable constructor function for prototype setting.
|
||
var ctor = function(){};
|
||
|
||
// Create a function bound to a given object (assigning `this`, and arguments,
|
||
// optionally). Binding with arguments is also known as `curry`.
|
||
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
|
||
// We check for `func.bind` first, to fail fast when `func` is undefined.
|
||
_.bind = function(func, context) {
|
||
var args, bound;
|
||
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
|
||
if (!_.isFunction(func)) throw new TypeError;
|
||
args = slice.call(arguments, 2);
|
||
return bound = function() {
|
||
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
|
||
ctor.prototype = func.prototype;
|
||
var self = new ctor;
|
||
ctor.prototype = null;
|
||
var result = func.apply(self, args.concat(slice.call(arguments)));
|
||
if (Object(result) === result) return result;
|
||
return self;
|
||
};
|
||
};
|
||
|
||
// Bind all of an object's methods to that object. Useful for ensuring that
|
||
// all callbacks defined on an object belong to it.
|
||
_.bindAll = function(obj) {
|
||
var funcs = slice.call(arguments, 1);
|
||
if (funcs.length == 0) funcs = _.functions(obj);
|
||
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
|
||
return obj;
|
||
};
|
||
|
||
// Memoize an expensive function by storing its results.
|
||
_.memoize = function(func, hasher) {
|
||
var memo = {};
|
||
hasher || (hasher = _.identity);
|
||
return function() {
|
||
var key = hasher.apply(this, arguments);
|
||
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
|
||
};
|
||
};
|
||
|
||
// Delays a function for the given number of milliseconds, and then calls
|
||
// it with the arguments supplied.
|
||
_.delay = function(func, wait) {
|
||
var args = slice.call(arguments, 2);
|
||
return setTimeout(function(){ return func.apply(null, args); }, wait);
|
||
};
|
||
|
||
// Defers a function, scheduling it to run after the current call stack has
|
||
// cleared.
|
||
_.defer = function(func) {
|
||
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
|
||
};
|
||
|
||
// Returns a function, that, when invoked, will only be triggered at most once
|
||
// during a given window of time.
|
||
_.throttle = function(func, wait) {
|
||
var context, args, timeout, result;
|
||
var previous = 0;
|
||
var later = function() {
|
||
previous = new Date;
|
||
timeout = null;
|
||
result = func.apply(context, args);
|
||
};
|
||
return function() {
|
||
var now = new Date;
|
||
var remaining = wait - (now - previous);
|
||
context = this;
|
||
args = arguments;
|
||
if (remaining <= 0) {
|
||
clearTimeout(timeout);
|
||
timeout = null;
|
||
previous = now;
|
||
result = func.apply(context, args);
|
||
} else if (!timeout) {
|
||
timeout = setTimeout(later, remaining);
|
||
}
|
||
return result;
|
||
};
|
||
};
|
||
|
||
// Returns a function, that, as long as it continues to be invoked, will not
|
||
// be triggered. The function will be called after it stops being called for
|
||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||
// leading edge, instead of the trailing.
|
||
_.debounce = function(func, wait, immediate) {
|
||
var timeout, result;
|
||
return function() {
|
||
var context = this, args = arguments;
|
||
var later = function() {
|
||
timeout = null;
|
||
if (!immediate) result = func.apply(context, args);
|
||
};
|
||
var callNow = immediate && !timeout;
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
if (callNow) result = func.apply(context, args);
|
||
return result;
|
||
};
|
||
};
|
||
|
||
// Returns a function that will be executed at most one time, no matter how
|
||
// often you call it. Useful for lazy initialization.
|
||
_.once = function(func) {
|
||
var ran = false, memo;
|
||
return function() {
|
||
if (ran) return memo;
|
||
ran = true;
|
||
memo = func.apply(this, arguments);
|
||
func = null;
|
||
return memo;
|
||
};
|
||
};
|
||
|
||
// Returns the first function passed as an argument to the second,
|
||
// allowing you to adjust arguments, run code before and after, and
|
||
// conditionally execute the original function.
|
||
_.wrap = function(func, wrapper) {
|
||
return function() {
|
||
var args = [func];
|
||
push.apply(args, arguments);
|
||
return wrapper.apply(this, args);
|
||
};
|
||
};
|
||
|
||
// Returns a function that is the composition of a list of functions, each
|
||
// consuming the return value of the function that follows.
|
||
_.compose = function() {
|
||
var funcs = arguments;
|
||
return function() {
|
||
var args = arguments;
|
||
for (var i = funcs.length - 1; i >= 0; i--) {
|
||
args = [funcs[i].apply(this, args)];
|
||
}
|
||
return args[0];
|
||
};
|
||
};
|
||
|
||
// Returns a function that will only be executed after being called N times.
|
||
_.after = function(times, func) {
|
||
if (times <= 0) return func();
|
||
return function() {
|
||
if (--times < 1) {
|
||
return func.apply(this, arguments);
|
||
}
|
||
};
|
||
};
|
||
|
||
// Object Functions
|
||
// ----------------
|
||
|
||
// Retrieve the names of an object's properties.
|
||
// Delegates to **ECMAScript 5**'s native `Object.keys`
|
||
_.keys = nativeKeys || function(obj) {
|
||
if (obj !== Object(obj)) throw new TypeError('Invalid object');
|
||
var keys = [];
|
||
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
|
||
return keys;
|
||
};
|
||
|
||
// Retrieve the values of an object's properties.
|
||
_.values = function(obj) {
|
||
var values = [];
|
||
for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
|
||
return values;
|
||
};
|
||
|
||
// Convert an object into a list of `[key, value]` pairs.
|
||
_.pairs = function(obj) {
|
||
var pairs = [];
|
||
for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
|
||
return pairs;
|
||
};
|
||
|
||
// Invert the keys and values of an object. The values must be serializable.
|
||
_.invert = function(obj) {
|
||
var result = {};
|
||
for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
|
||
return result;
|
||
};
|
||
|
||
// Return a sorted list of the function names available on the object.
|
||
// Aliased as `methods`
|
||
_.functions = _.methods = function(obj) {
|
||
var names = [];
|
||
for (var key in obj) {
|
||
if (_.isFunction(obj[key])) names.push(key);
|
||
}
|
||
return names.sort();
|
||
};
|
||
|
||
// Extend a given object with all the properties in passed-in object(s).
|
||
_.extend = function(obj) {
|
||
each(slice.call(arguments, 1), function(source) {
|
||
if (source) {
|
||
for (var prop in source) {
|
||
obj[prop] = source[prop];
|
||
}
|
||
}
|
||
});
|
||
return obj;
|
||
};
|
||
|
||
// Return a copy of the object only containing the whitelisted properties.
|
||
_.pick = function(obj) {
|
||
var copy = {};
|
||
var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
|
||
each(keys, function(key) {
|
||
if (key in obj) copy[key] = obj[key];
|
||
});
|
||
return copy;
|
||
};
|
||
|
||
// Return a copy of the object without the blacklisted properties.
|
||
_.omit = function(obj) {
|
||
var copy = {};
|
||
var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
|
||
for (var key in obj) {
|
||
if (!_.contains(keys, key)) copy[key] = obj[key];
|
||
}
|
||
return copy;
|
||
};
|
||
|
||
// Fill in a given object with default properties.
|
||
_.defaults = function(obj) {
|
||
each(slice.call(arguments, 1), function(source) {
|
||
if (source) {
|
||
for (var prop in source) {
|
||
if (obj[prop] == null) obj[prop] = source[prop];
|
||
}
|
||
}
|
||
});
|
||
return obj;
|
||
};
|
||
|
||
// Create a (shallow-cloned) duplicate of an object.
|
||
_.clone = function(obj) {
|
||
if (!_.isObject(obj)) return obj;
|
||
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
|
||
};
|
||
|
||
// Invokes interceptor with the obj, and then returns obj.
|
||
// The primary purpose of this method is to "tap into" a method chain, in
|
||
// order to perform operations on intermediate results within the chain.
|
||
_.tap = function(obj, interceptor) {
|
||
interceptor(obj);
|
||
return obj;
|
||
};
|
||
|
||
// Internal recursive comparison function for `isEqual`.
|
||
var eq = function(a, b, aStack, bStack) {
|
||
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
||
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
|
||
if (a === b) return a !== 0 || 1 / a == 1 / b;
|
||
// A strict comparison is necessary because `null == undefined`.
|
||
if (a == null || b == null) return a === b;
|
||
// Unwrap any wrapped objects.
|
||
if (a instanceof _) a = a._wrapped;
|
||
if (b instanceof _) b = b._wrapped;
|
||
// Compare `[[Class]]` names.
|
||
var className = toString.call(a);
|
||
if (className != toString.call(b)) return false;
|
||
switch (className) {
|
||
// Strings, numbers, dates, and booleans are compared by value.
|
||
case '[object String]':
|
||
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
|
||
// equivalent to `new String("5")`.
|
||
return a == String(b);
|
||
case '[object Number]':
|
||
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
|
||
// other numeric values.
|
||
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
|
||
case '[object Date]':
|
||
case '[object Boolean]':
|
||
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
|
||
// millisecond representations. Note that invalid dates with millisecond representations
|
||
// of `NaN` are not equivalent.
|
||
return +a == +b;
|
||
// RegExps are compared by their source patterns and flags.
|
||
case '[object RegExp]':
|
||
return a.source == b.source &&
|
||
a.global == b.global &&
|
||
a.multiline == b.multiline &&
|
||
a.ignoreCase == b.ignoreCase;
|
||
}
|
||
if (typeof a != 'object' || typeof b != 'object') return false;
|
||
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
||
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
||
var length = aStack.length;
|
||
while (length--) {
|
||
// Linear search. Performance is inversely proportional to the number of
|
||
// unique nested structures.
|
||
if (aStack[length] == a) return bStack[length] == b;
|
||
}
|
||
// Add the first object to the stack of traversed objects.
|
||
aStack.push(a);
|
||
bStack.push(b);
|
||
var size = 0, result = true;
|
||
// Recursively compare objects and arrays.
|
||
if (className == '[object Array]') {
|
||
// Compare array lengths to determine if a deep comparison is necessary.
|
||
size = a.length;
|
||
result = size == b.length;
|
||
if (result) {
|
||
// Deep compare the contents, ignoring non-numeric properties.
|
||
while (size--) {
|
||
if (!(result = eq(a[size], b[size], aStack, bStack))) break;
|
||
}
|
||
}
|
||
} else {
|
||
// Objects with different constructors are not equivalent, but `Object`s
|
||
// from different frames are.
|
||
var aCtor = a.constructor, bCtor = b.constructor;
|
||
if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
|
||
_.isFunction(bCtor) && (bCtor instanceof bCtor))) {
|
||
return false;
|
||
}
|
||
// Deep compare objects.
|
||
for (var key in a) {
|
||
if (_.has(a, key)) {
|
||
// Count the expected number of properties.
|
||
size++;
|
||
// Deep compare each member.
|
||
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
|
||
}
|
||
}
|
||
// Ensure that both objects contain the same number of properties.
|
||
if (result) {
|
||
for (key in b) {
|
||
if (_.has(b, key) && !(size--)) break;
|
||
}
|
||
result = !size;
|
||
}
|
||
}
|
||
// Remove the first object from the stack of traversed objects.
|
||
aStack.pop();
|
||
bStack.pop();
|
||
return result;
|
||
};
|
||
|
||
// Perform a deep comparison to check if two objects are equal.
|
||
_.isEqual = function(a, b) {
|
||
return eq(a, b, [], []);
|
||
};
|
||
|
||
// Is a given array, string, or object empty?
|
||
// An "empty" object has no enumerable own-properties.
|
||
_.isEmpty = function(obj) {
|
||
if (obj == null) return true;
|
||
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
|
||
for (var key in obj) if (_.has(obj, key)) return false;
|
||
return true;
|
||
};
|
||
|
||
// Is a given value a DOM element?
|
||
_.isElement = function(obj) {
|
||
return !!(obj && obj.nodeType === 1);
|
||
};
|
||
|
||
// Is a given value an array?
|
||
// Delegates to ECMA5's native Array.isArray
|
||
_.isArray = nativeIsArray || function(obj) {
|
||
return toString.call(obj) == '[object Array]';
|
||
};
|
||
|
||
// Is a given variable an object?
|
||
_.isObject = function(obj) {
|
||
return obj === Object(obj);
|
||
};
|
||
|
||
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
|
||
each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
|
||
_['is' + name] = function(obj) {
|
||
return toString.call(obj) == '[object ' + name + ']';
|
||
};
|
||
});
|
||
|
||
// Define a fallback version of the method in browsers (ahem, IE), where
|
||
// there isn't any inspectable "Arguments" type.
|
||
if (!_.isArguments(arguments)) {
|
||
_.isArguments = function(obj) {
|
||
return !!(obj && _.has(obj, 'callee'));
|
||
};
|
||
}
|
||
|
||
// Optimize `isFunction` if appropriate.
|
||
if (typeof (/./) !== 'function') {
|
||
_.isFunction = function(obj) {
|
||
return typeof obj === 'function';
|
||
};
|
||
}
|
||
|
||
// Is a given object a finite number?
|
||
_.isFinite = function(obj) {
|
||
return isFinite(obj) && !isNaN(parseFloat(obj));
|
||
};
|
||
|
||
// Is the given value `NaN`? (NaN is the only number which does not equal itself).
|
||
_.isNaN = function(obj) {
|
||
return _.isNumber(obj) && obj != +obj;
|
||
};
|
||
|
||
// Is a given value a boolean?
|
||
_.isBoolean = function(obj) {
|
||
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
|
||
};
|
||
|
||
// Is a given value equal to null?
|
||
_.isNull = function(obj) {
|
||
return obj === null;
|
||
};
|
||
|
||
// Is a given variable undefined?
|
||
_.isUndefined = function(obj) {
|
||
return obj === void 0;
|
||
};
|
||
|
||
// Shortcut function for checking if an object has a given property directly
|
||
// on itself (in other words, not on a prototype).
|
||
_.has = function(obj, key) {
|
||
return hasOwnProperty.call(obj, key);
|
||
};
|
||
|
||
// Utility Functions
|
||
// -----------------
|
||
|
||
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
|
||
// previous owner. Returns a reference to the Underscore object.
|
||
_.noConflict = function() {
|
||
root._ = previousUnderscore;
|
||
return this;
|
||
};
|
||
|
||
// Keep the identity function around for default iterators.
|
||
_.identity = function(value) {
|
||
return value;
|
||
};
|
||
|
||
// Run a function **n** times.
|
||
_.times = function(n, iterator, context) {
|
||
var accum = Array(n);
|
||
for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
|
||
return accum;
|
||
};
|
||
|
||
// Return a random integer between min and max (inclusive).
|
||
_.random = function(min, max) {
|
||
if (max == null) {
|
||
max = min;
|
||
min = 0;
|
||
}
|
||
return min + (0 | Math.random() * (max - min + 1));
|
||
};
|
||
|
||
// List of HTML entities for escaping.
|
||
var entityMap = {
|
||
escape: {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": ''',
|
||
'/': '/'
|
||
}
|
||
};
|
||
entityMap.unescape = _.invert(entityMap.escape);
|
||
|
||
// Regexes containing the keys and values listed immediately above.
|
||
var entityRegexes = {
|
||
escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
|
||
unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
|
||
};
|
||
|
||
// Functions for escaping and unescaping strings to/from HTML interpolation.
|
||
_.each(['escape', 'unescape'], function(method) {
|
||
_[method] = function(string) {
|
||
if (string == null) return '';
|
||
return ('' + string).replace(entityRegexes[method], function(match) {
|
||
return entityMap[method][match];
|
||
});
|
||
};
|
||
});
|
||
|
||
// If the value of the named property is a function then invoke it;
|
||
// otherwise, return it.
|
||
_.result = function(object, property) {
|
||
if (object == null) return null;
|
||
var value = object[property];
|
||
return _.isFunction(value) ? value.call(object) : value;
|
||
};
|
||
|
||
// Add your own custom functions to the Underscore object.
|
||
_.mixin = function(obj) {
|
||
each(_.functions(obj), function(name){
|
||
var func = _[name] = obj[name];
|
||
_.prototype[name] = function() {
|
||
var args = [this._wrapped];
|
||
push.apply(args, arguments);
|
||
return result.call(this, func.apply(_, args));
|
||
};
|
||
});
|
||
};
|
||
|
||
// Generate a unique integer id (unique within the entire client session).
|
||
// Useful for temporary DOM ids.
|
||
var idCounter = 0;
|
||
_.uniqueId = function(prefix) {
|
||
var id = '' + ++idCounter;
|
||
return prefix ? prefix + id : id;
|
||
};
|
||
|
||
// By default, Underscore uses ERB-style template delimiters, change the
|
||
// following template settings to use alternative delimiters.
|
||
_.templateSettings = {
|
||
evaluate : /<%([\s\S]+?)%>/g,
|
||
interpolate : /<%=([\s\S]+?)%>/g,
|
||
escape : /<%-([\s\S]+?)%>/g
|
||
};
|
||
|
||
// When customizing `templateSettings`, if you don't want to define an
|
||
// interpolation, evaluation or escaping regex, we need one that is
|
||
// guaranteed not to match.
|
||
var noMatch = /(.)^/;
|
||
|
||
// Certain characters need to be escaped so that they can be put into a
|
||
// string literal.
|
||
var escapes = {
|
||
"'": "'",
|
||
'\\': '\\',
|
||
'\r': 'r',
|
||
'\n': 'n',
|
||
'\t': 't',
|
||
'\u2028': 'u2028',
|
||
'\u2029': 'u2029'
|
||
};
|
||
|
||
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
|
||
|
||
// JavaScript micro-templating, similar to John Resig's implementation.
|
||
// Underscore templating handles arbitrary delimiters, preserves whitespace,
|
||
// and correctly escapes quotes within interpolated code.
|
||
_.template = function(text, data, settings) {
|
||
settings = _.defaults({}, settings, _.templateSettings);
|
||
|
||
// Combine delimiters into one regular expression via alternation.
|
||
var matcher = new RegExp([
|
||
(settings.escape || noMatch).source,
|
||
(settings.interpolate || noMatch).source,
|
||
(settings.evaluate || noMatch).source
|
||
].join('|') + '|$', 'g');
|
||
|
||
// Compile the template source, escaping string literals appropriately.
|
||
var index = 0;
|
||
var source = "__p+='";
|
||
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
|
||
source += text.slice(index, offset)
|
||
.replace(escaper, function(match) { return '\\' + escapes[match]; });
|
||
|
||
if (escape) {
|
||
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
|
||
}
|
||
if (interpolate) {
|
||
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
|
||
}
|
||
if (evaluate) {
|
||
source += "';\n" + evaluate + "\n__p+='";
|
||
}
|
||
index = offset + match.length;
|
||
return match;
|
||
});
|
||
source += "';\n";
|
||
|
||
// If a variable is not specified, place data values in local scope.
|
||
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
|
||
|
||
source = "var __t,__p='',__j=Array.prototype.join," +
|
||
"print=function(){__p+=__j.call(arguments,'');};\n" +
|
||
source + "return __p;\n";
|
||
|
||
try {
|
||
var render = new Function(settings.variable || 'obj', '_', source);
|
||
} catch (e) {
|
||
e.source = source;
|
||
throw e;
|
||
}
|
||
|
||
if (data) return render(data, _);
|
||
var template = function(data) {
|
||
return render.call(this, data, _);
|
||
};
|
||
|
||
// Provide the compiled function source as a convenience for precompilation.
|
||
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
|
||
|
||
return template;
|
||
};
|
||
|
||
// Add a "chain" function, which will delegate to the wrapper.
|
||
_.chain = function(obj) {
|
||
return _(obj).chain();
|
||
};
|
||
|
||
// OOP
|
||
// ---------------
|
||
// If Underscore is called as a function, it returns a wrapped object that
|
||
// can be used OO-style. This wrapper holds altered versions of all the
|
||
// underscore functions. Wrapped objects may be chained.
|
||
|
||
// Helper function to continue chaining intermediate results.
|
||
var result = function(obj) {
|
||
return this._chain ? _(obj).chain() : obj;
|
||
};
|
||
|
||
// Add all of the Underscore functions to the wrapper object.
|
||
_.mixin(_);
|
||
|
||
// Add all mutator Array functions to the wrapper.
|
||
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
|
||
var method = ArrayProto[name];
|
||
_.prototype[name] = function() {
|
||
var obj = this._wrapped;
|
||
method.apply(obj, arguments);
|
||
if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
|
||
return result.call(this, obj);
|
||
};
|
||
});
|
||
|
||
// Add all accessor Array functions to the wrapper.
|
||
each(['concat', 'join', 'slice'], function(name) {
|
||
var method = ArrayProto[name];
|
||
_.prototype[name] = function() {
|
||
return result.call(this, method.apply(this._wrapped, arguments));
|
||
};
|
||
});
|
||
|
||
_.extend(_.prototype, {
|
||
|
||
// Start chaining a wrapped Underscore object.
|
||
chain: function() {
|
||
this._chain = true;
|
||
return this;
|
||
},
|
||
|
||
// Extracts the result from a wrapped and chained object.
|
||
value: function() {
|
||
return this._wrapped;
|
||
}
|
||
|
||
});
|
||
|
||
}).call(this);
|
||
|
||
});
|
||
|
||
require.define("/node_modules/backbone/package.json",function(require,module,exports,__dirname,__filename,process,global){module.exports = {"main":"backbone.js"}
|
||
});
|
||
|
||
require.define("/node_modules/backbone/backbone.js",function(require,module,exports,__dirname,__filename,process,global){// Backbone.js 0.9.9
|
||
|
||
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||
// Backbone may be freely distributed under the MIT license.
|
||
// For all details and documentation:
|
||
// http://backbonejs.org
|
||
|
||
(function(){
|
||
|
||
// Initial Setup
|
||
// -------------
|
||
|
||
// Save a reference to the global object (`window` in the browser, `exports`
|
||
// on the server).
|
||
var root = this;
|
||
|
||
// Save the previous value of the `Backbone` variable, so that it can be
|
||
// restored later on, if `noConflict` is used.
|
||
var previousBackbone = root.Backbone;
|
||
|
||
// Create a local reference to array methods.
|
||
var array = [];
|
||
var push = array.push;
|
||
var slice = array.slice;
|
||
var splice = array.splice;
|
||
|
||
// The top-level namespace. All public Backbone classes and modules will
|
||
// be attached to this. Exported for both CommonJS and the browser.
|
||
var Backbone;
|
||
if (typeof exports !== 'undefined') {
|
||
Backbone = exports;
|
||
} else {
|
||
Backbone = root.Backbone = {};
|
||
}
|
||
|
||
// Current version of the library. Keep in sync with `package.json`.
|
||
Backbone.VERSION = '0.9.9';
|
||
|
||
// Require Underscore, if we're on the server, and it's not already present.
|
||
var _ = root._;
|
||
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
|
||
|
||
// For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
|
||
Backbone.$ = root.jQuery || root.Zepto || root.ender;
|
||
|
||
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
|
||
// to its previous owner. Returns a reference to this Backbone object.
|
||
Backbone.noConflict = function() {
|
||
root.Backbone = previousBackbone;
|
||
return this;
|
||
};
|
||
|
||
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
|
||
// will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
|
||
// set a `X-Http-Method-Override` header.
|
||
Backbone.emulateHTTP = false;
|
||
|
||
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
|
||
// `application/json` requests ... will encode the body as
|
||
// `application/x-www-form-urlencoded` instead and will send the model in a
|
||
// form param named `model`.
|
||
Backbone.emulateJSON = false;
|
||
|
||
// Backbone.Events
|
||
// ---------------
|
||
|
||
// Regular expression used to split event strings.
|
||
var eventSplitter = /\s+/;
|
||
|
||
// Implement fancy features of the Events API such as multiple event
|
||
// names `"change blur"` and jQuery-style event maps `{change: action}`
|
||
// in terms of the existing API.
|
||
var eventsApi = function(obj, action, name, rest) {
|
||
if (!name) return true;
|
||
if (typeof name === 'object') {
|
||
for (var key in name) {
|
||
obj[action].apply(obj, [key, name[key]].concat(rest));
|
||
}
|
||
} else if (eventSplitter.test(name)) {
|
||
var names = name.split(eventSplitter);
|
||
for (var i = 0, l = names.length; i < l; i++) {
|
||
obj[action].apply(obj, [names[i]].concat(rest));
|
||
}
|
||
} else {
|
||
return true;
|
||
}
|
||
};
|
||
|
||
// Optimized internal dispatch function for triggering events. Tries to
|
||
// keep the usual cases speedy (most Backbone events have 3 arguments).
|
||
var triggerEvents = function(obj, events, args) {
|
||
var ev, i = -1, l = events.length;
|
||
switch (args.length) {
|
||
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
|
||
return;
|
||
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
|
||
return;
|
||
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
|
||
return;
|
||
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
|
||
return;
|
||
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
|
||
}
|
||
};
|
||
|
||
// A module that can be mixed in to *any object* in order to provide it with
|
||
// custom events. You may bind with `on` or remove with `off` callback
|
||
// functions to an event; `trigger`-ing an event fires all callbacks in
|
||
// succession.
|
||
//
|
||
// var object = {};
|
||
// _.extend(object, Backbone.Events);
|
||
// object.on('expand', function(){ alert('expanded'); });
|
||
// object.trigger('expand');
|
||
//
|
||
var Events = Backbone.Events = {
|
||
|
||
// Bind one or more space separated events, or an events map,
|
||
// to a `callback` function. Passing `"all"` will bind the callback to
|
||
// all events fired.
|
||
on: function(name, callback, context) {
|
||
if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
|
||
this._events || (this._events = {});
|
||
var list = this._events[name] || (this._events[name] = []);
|
||
list.push({callback: callback, context: context, ctx: context || this});
|
||
return this;
|
||
},
|
||
|
||
// Bind events to only be triggered a single time. After the first time
|
||
// the callback is invoked, it will be removed.
|
||
once: function(name, callback, context) {
|
||
if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this;
|
||
var self = this;
|
||
var once = _.once(function() {
|
||
self.off(name, once);
|
||
callback.apply(this, arguments);
|
||
});
|
||
once._callback = callback;
|
||
this.on(name, once, context);
|
||
return this;
|
||
},
|
||
|
||
// Remove one or many callbacks. If `context` is null, removes all
|
||
// callbacks with that function. If `callback` is null, removes all
|
||
// callbacks for the event. If `events` is null, removes all bound
|
||
// callbacks for all events.
|
||
off: function(name, callback, context) {
|
||
var list, ev, events, names, i, l, j, k;
|
||
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
|
||
if (!name && !callback && !context) {
|
||
this._events = {};
|
||
return this;
|
||
}
|
||
|
||
names = name ? [name] : _.keys(this._events);
|
||
for (i = 0, l = names.length; i < l; i++) {
|
||
name = names[i];
|
||
if (list = this._events[name]) {
|
||
events = [];
|
||
if (callback || context) {
|
||
for (j = 0, k = list.length; j < k; j++) {
|
||
ev = list[j];
|
||
if ((callback && callback !== (ev.callback._callback || ev.callback)) ||
|
||
(context && context !== ev.context)) {
|
||
events.push(ev);
|
||
}
|
||
}
|
||
}
|
||
this._events[name] = events;
|
||
}
|
||
}
|
||
|
||
return this;
|
||
},
|
||
|
||
// Trigger one or many events, firing all bound callbacks. Callbacks are
|
||
// passed the same arguments as `trigger` is, apart from the event name
|
||
// (unless you're listening on `"all"`, which will cause your callback to
|
||
// receive the true name of the event as the first argument).
|
||
trigger: function(name) {
|
||
if (!this._events) return this;
|
||
var args = slice.call(arguments, 1);
|
||
if (!eventsApi(this, 'trigger', name, args)) return this;
|
||
var events = this._events[name];
|
||
var allEvents = this._events.all;
|
||
if (events) triggerEvents(this, events, args);
|
||
if (allEvents) triggerEvents(this, allEvents, arguments);
|
||
return this;
|
||
},
|
||
|
||
// An inversion-of-control version of `on`. Tell *this* object to listen to
|
||
// an event in another object ... keeping track of what it's listening to.
|
||
listenTo: function(object, events, callback) {
|
||
var listeners = this._listeners || (this._listeners = {});
|
||
var id = object._listenerId || (object._listenerId = _.uniqueId('l'));
|
||
listeners[id] = object;
|
||
object.on(events, callback || this, this);
|
||
return this;
|
||
},
|
||
|
||
// Tell this object to stop listening to either specific events ... or
|
||
// to every object it's currently listening to.
|
||
stopListening: function(object, events, callback) {
|
||
var listeners = this._listeners;
|
||
if (!listeners) return;
|
||
if (object) {
|
||
object.off(events, callback, this);
|
||
if (!events && !callback) delete listeners[object._listenerId];
|
||
} else {
|
||
for (var id in listeners) {
|
||
listeners[id].off(null, null, this);
|
||
}
|
||
this._listeners = {};
|
||
}
|
||
return this;
|
||
}
|
||
};
|
||
|
||
// Aliases for backwards compatibility.
|
||
Events.bind = Events.on;
|
||
Events.unbind = Events.off;
|
||
|
||
// Allow the `Backbone` object to serve as a global event bus, for folks who
|
||
// want global "pubsub" in a convenient place.
|
||
_.extend(Backbone, Events);
|
||
|
||
// Backbone.Model
|
||
// --------------
|
||
|
||
// Create a new model, with defined attributes. A client id (`cid`)
|
||
// is automatically generated and assigned for you.
|
||
var Model = Backbone.Model = function(attributes, options) {
|
||
var defaults;
|
||
var attrs = attributes || {};
|
||
this.cid = _.uniqueId('c');
|
||
this.changed = {};
|
||
this.attributes = {};
|
||
this._changes = [];
|
||
if (options && options.collection) this.collection = options.collection;
|
||
if (options && options.parse) attrs = this.parse(attrs);
|
||
if (defaults = _.result(this, 'defaults')) _.defaults(attrs, defaults);
|
||
this.set(attrs, {silent: true});
|
||
this._currentAttributes = _.clone(this.attributes);
|
||
this._previousAttributes = _.clone(this.attributes);
|
||
this.initialize.apply(this, arguments);
|
||
};
|
||
|
||
// Attach all inheritable methods to the Model prototype.
|
||
_.extend(Model.prototype, Events, {
|
||
|
||
// A hash of attributes whose current and previous value differ.
|
||
changed: null,
|
||
|
||
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
||
// CouchDB users may want to set this to `"_id"`.
|
||
idAttribute: 'id',
|
||
|
||
// Initialize is an empty function by default. Override it with your own
|
||
// initialization logic.
|
||
initialize: function(){},
|
||
|
||
// Return a copy of the model's `attributes` object.
|
||
toJSON: function(options) {
|
||
return _.clone(this.attributes);
|
||
},
|
||
|
||
// Proxy `Backbone.sync` by default.
|
||
sync: function() {
|
||
return Backbone.sync.apply(this, arguments);
|
||
},
|
||
|
||
// Get the value of an attribute.
|
||
get: function(attr) {
|
||
return this.attributes[attr];
|
||
},
|
||
|
||
// Get the HTML-escaped value of an attribute.
|
||
escape: function(attr) {
|
||
return _.escape(this.get(attr));
|
||
},
|
||
|
||
// Returns `true` if the attribute contains a value that is not null
|
||
// or undefined.
|
||
has: function(attr) {
|
||
return this.get(attr) != null;
|
||
},
|
||
|
||
// Set a hash of model attributes on the object, firing `"change"` unless
|
||
// you choose to silence it.
|
||
set: function(key, val, options) {
|
||
var attr, attrs;
|
||
if (key == null) return this;
|
||
|
||
// Handle both `"key", value` and `{key: value}` -style arguments.
|
||
if (_.isObject(key)) {
|
||
attrs = key;
|
||
options = val;
|
||
} else {
|
||
(attrs = {})[key] = val;
|
||
}
|
||
|
||
// Extract attributes and options.
|
||
var silent = options && options.silent;
|
||
var unset = options && options.unset;
|
||
|
||
// Run validation.
|
||
if (!this._validate(attrs, options)) return false;
|
||
|
||
// Check for changes of `id`.
|
||
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
|
||
|
||
var now = this.attributes;
|
||
|
||
// For each `set` attribute...
|
||
for (attr in attrs) {
|
||
val = attrs[attr];
|
||
|
||
// Update or delete the current value, and track the change.
|
||
unset ? delete now[attr] : now[attr] = val;
|
||
this._changes.push(attr, val);
|
||
}
|
||
|
||
// Signal that the model's state has potentially changed, and we need
|
||
// to recompute the actual changes.
|
||
this._hasComputed = false;
|
||
|
||
// Fire the `"change"` events.
|
||
if (!silent) this.change(options);
|
||
return this;
|
||
},
|
||
|
||
// Remove an attribute from the model, firing `"change"` unless you choose
|
||
// to silence it. `unset` is a noop if the attribute doesn't exist.
|
||
unset: function(attr, options) {
|
||
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
|
||
},
|
||
|
||
// Clear all attributes on the model, firing `"change"` unless you choose
|
||
// to silence it.
|
||
clear: function(options) {
|
||
var attrs = {};
|
||
for (var key in this.attributes) attrs[key] = void 0;
|
||
return this.set(attrs, _.extend({}, options, {unset: true}));
|
||
},
|
||
|
||
// Fetch the model from the server. If the server's representation of the
|
||
// model differs from its current attributes, they will be overriden,
|
||
// triggering a `"change"` event.
|
||
fetch: function(options) {
|
||
options = options ? _.clone(options) : {};
|
||
if (options.parse === void 0) options.parse = true;
|
||
var model = this;
|
||
var success = options.success;
|
||
options.success = function(resp, status, xhr) {
|
||
if (!model.set(model.parse(resp), options)) return false;
|
||
if (success) success(model, resp, options);
|
||
};
|
||
return this.sync('read', this, options);
|
||
},
|
||
|
||
// Set a hash of model attributes, and sync the model to the server.
|
||
// If the server returns an attributes hash that differs, the model's
|
||
// state will be `set` again.
|
||
save: function(key, val, options) {
|
||
var attrs, current, done;
|
||
|
||
// Handle both `"key", value` and `{key: value}` -style arguments.
|
||
if (key == null || _.isObject(key)) {
|
||
attrs = key;
|
||
options = val;
|
||
} else if (key != null) {
|
||
(attrs = {})[key] = val;
|
||
}
|
||
options = options ? _.clone(options) : {};
|
||
|
||
// If we're "wait"-ing to set changed attributes, validate early.
|
||
if (options.wait) {
|
||
if (attrs && !this._validate(attrs, options)) return false;
|
||
current = _.clone(this.attributes);
|
||
}
|
||
|
||
// Regular saves `set` attributes before persisting to the server.
|
||
var silentOptions = _.extend({}, options, {silent: true});
|
||
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
|
||
return false;
|
||
}
|
||
|
||
// Do not persist invalid models.
|
||
if (!attrs && !this._validate(null, options)) return false;
|
||
|
||
// After a successful server-side save, the client is (optionally)
|
||
// updated with the server-side state.
|
||
var model = this;
|
||
var success = options.success;
|
||
options.success = function(resp, status, xhr) {
|
||
done = true;
|
||
var serverAttrs = model.parse(resp);
|
||
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
|
||
if (!model.set(serverAttrs, options)) return false;
|
||
if (success) success(model, resp, options);
|
||
};
|
||
|
||
// Finish configuring and sending the Ajax request.
|
||
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
|
||
if (method == 'patch') options.attrs = attrs;
|
||
var xhr = this.sync(method, this, options);
|
||
|
||
// When using `wait`, reset attributes to original values unless
|
||
// `success` has been called already.
|
||
if (!done && options.wait) {
|
||
this.clear(silentOptions);
|
||
this.set(current, silentOptions);
|
||
}
|
||
|
||
return xhr;
|
||
},
|
||
|
||
// Destroy this model on the server if it was already persisted.
|
||
// Optimistically removes the model from its collection, if it has one.
|
||
// If `wait: true` is passed, waits for the server to respond before removal.
|
||
destroy: function(options) {
|
||
options = options ? _.clone(options) : {};
|
||
var model = this;
|
||
var success = options.success;
|
||
|
||
var destroy = function() {
|
||
model.trigger('destroy', model, model.collection, options);
|
||
};
|
||
|
||
options.success = function(resp) {
|
||
if (options.wait || model.isNew()) destroy();
|
||
if (success) success(model, resp, options);
|
||
};
|
||
|
||
if (this.isNew()) {
|
||
options.success();
|
||
return false;
|
||
}
|
||
|
||
var xhr = this.sync('delete', this, options);
|
||
if (!options.wait) destroy();
|
||
return xhr;
|
||
},
|
||
|
||
// Default URL for the model's representation on the server -- if you're
|
||
// using Backbone's restful methods, override this to change the endpoint
|
||
// that will be called.
|
||
url: function() {
|
||
var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
|
||
if (this.isNew()) return base;
|
||
return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
|
||
},
|
||
|
||
// **parse** converts a response into the hash of attributes to be `set` on
|
||
// the model. The default implementation is just to pass the response along.
|
||
parse: function(resp) {
|
||
return resp;
|
||
},
|
||
|
||
// Create a new model with identical attributes to this one.
|
||
clone: function() {
|
||
return new this.constructor(this.attributes);
|
||
},
|
||
|
||
// A model is new if it has never been saved to the server, and lacks an id.
|
||
isNew: function() {
|
||
return this.id == null;
|
||
},
|
||
|
||
// Call this method to manually fire a `"change"` event for this model and
|
||
// a `"change:attribute"` event for each changed attribute.
|
||
// Calling this will cause all objects observing the model to update.
|
||
change: function(options) {
|
||
var changing = this._changing;
|
||
this._changing = true;
|
||
|
||
// Generate the changes to be triggered on the model.
|
||
var triggers = this._computeChanges(true);
|
||
|
||
this._pending = !!triggers.length;
|
||
|
||
for (var i = triggers.length - 2; i >= 0; i -= 2) {
|
||
this.trigger('change:' + triggers[i], this, triggers[i + 1], options);
|
||
}
|
||
|
||
if (changing) return this;
|
||
|
||
// Trigger a `change` while there have been changes.
|
||
while (this._pending) {
|
||
this._pending = false;
|
||
this.trigger('change', this, options);
|
||
this._previousAttributes = _.clone(this.attributes);
|
||
}
|
||
|
||
this._changing = false;
|
||
return this;
|
||
},
|
||
|
||
// Determine if the model has changed since the last `"change"` event.
|
||
// If you specify an attribute name, determine if that attribute has changed.
|
||
hasChanged: function(attr) {
|
||
if (!this._hasComputed) this._computeChanges();
|
||
if (attr == null) return !_.isEmpty(this.changed);
|
||
return _.has(this.changed, attr);
|
||
},
|
||
|
||
// Return an object containing all the attributes that have changed, or
|
||
// false if there are no changed attributes. Useful for determining what
|
||
// parts of a view need to be updated and/or what attributes need to be
|
||
// persisted to the server. Unset attributes will be set to undefined.
|
||
// You can also pass an attributes object to diff against the model,
|
||
// determining if there *would be* a change.
|
||
changedAttributes: function(diff) {
|
||
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
||
var val, changed = false, old = this._previousAttributes;
|
||
for (var attr in diff) {
|
||
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
|
||
(changed || (changed = {}))[attr] = val;
|
||
}
|
||
return changed;
|
||
},
|
||
|
||
// Looking at the built up list of `set` attribute changes, compute how
|
||
// many of the attributes have actually changed. If `loud`, return a
|
||
// boiled-down list of only the real changes.
|
||
_computeChanges: function(loud) {
|
||
this.changed = {};
|
||
var already = {};
|
||
var triggers = [];
|
||
var current = this._currentAttributes;
|
||
var changes = this._changes;
|
||
|
||
// Loop through the current queue of potential model changes.
|
||
for (var i = changes.length - 2; i >= 0; i -= 2) {
|
||
var key = changes[i], val = changes[i + 1];
|
||
if (already[key]) continue;
|
||
already[key] = true;
|
||
|
||
// Check if the attribute has been modified since the last change,
|
||
// and update `this.changed` accordingly. If we're inside of a `change`
|
||
// call, also add a trigger to the list.
|
||
if (current[key] !== val) {
|
||
this.changed[key] = val;
|
||
if (!loud) continue;
|
||
triggers.push(key, val);
|
||
current[key] = val;
|
||
}
|
||
}
|
||
if (loud) this._changes = [];
|
||
|
||
// Signals `this.changed` is current to prevent duplicate calls from `this.hasChanged`.
|
||
this._hasComputed = true;
|
||
return triggers;
|
||
},
|
||
|
||
// Get the previous value of an attribute, recorded at the time the last
|
||
// `"change"` event was fired.
|
||
previous: function(attr) {
|
||
if (attr == null || !this._previousAttributes) return null;
|
||
return this._previousAttributes[attr];
|
||
},
|
||
|
||
// Get all of the attributes of the model at the time of the previous
|
||
// `"change"` event.
|
||
previousAttributes: function() {
|
||
return _.clone(this._previousAttributes);
|
||
},
|
||
|
||
// Run validation against the next complete set of model attributes,
|
||
// returning `true` if all is well. If a specific `error` callback has
|
||
// been passed, call that instead of firing the general `"error"` event.
|
||
_validate: function(attrs, options) {
|
||
if (!this.validate) return true;
|
||
attrs = _.extend({}, this.attributes, attrs);
|
||
var error = this.validate(attrs, options);
|
||
if (!error) return true;
|
||
if (options && options.error) options.error(this, error, options);
|
||
this.trigger('error', this, error, options);
|
||
return false;
|
||
}
|
||
|
||
});
|
||
|
||
// Backbone.Collection
|
||
// -------------------
|
||
|
||
// Provides a standard collection class for our sets of models, ordered
|
||
// or unordered. If a `comparator` is specified, the Collection will maintain
|
||
// its models in sort order, as they're added and removed.
|
||
var Collection = Backbone.Collection = function(models, options) {
|
||
options || (options = {});
|
||
if (options.model) this.model = options.model;
|
||
if (options.comparator !== void 0) this.comparator = options.comparator;
|
||
this._reset();
|
||
this.initialize.apply(this, arguments);
|
||
if (models) this.reset(models, _.extend({silent: true}, options));
|
||
};
|
||
|
||
// Define the Collection's inheritable methods.
|
||
_.extend(Collection.prototype, Events, {
|
||
|
||
// The default model for a collection is just a **Backbone.Model**.
|
||
// This should be overridden in most cases.
|
||
model: Model,
|
||
|
||
// Initialize is an empty function by default. Override it with your own
|
||
// initialization logic.
|
||
initialize: function(){},
|
||
|
||
// The JSON representation of a Collection is an array of the
|
||
// models' attributes.
|
||
toJSON: function(options) {
|
||
return this.map(function(model){ return model.toJSON(options); });
|
||
},
|
||
|
||
// Proxy `Backbone.sync` by default.
|
||
sync: function() {
|
||
return Backbone.sync.apply(this, arguments);
|
||
},
|
||
|
||
// Add a model, or list of models to the set. Pass **silent** to avoid
|
||
// firing the `add` event for every new model.
|
||
add: function(models, options) {
|
||
var i, args, length, model, existing, needsSort;
|
||
var at = options && options.at;
|
||
var sort = ((options && options.sort) == null ? true : options.sort);
|
||
models = _.isArray(models) ? models.slice() : [models];
|
||
|
||
// Turn bare objects into model references, and prevent invalid models
|
||
// from being added.
|
||
for (i = models.length - 1; i >= 0; i--) {
|
||
if(!(model = this._prepareModel(models[i], options))) {
|
||
this.trigger("error", this, models[i], options);
|
||
models.splice(i, 1);
|
||
continue;
|
||
}
|
||
models[i] = model;
|
||
|
||
existing = model.id != null && this._byId[model.id];
|
||
// If a duplicate is found, prevent it from being added and
|
||
// optionally merge it into the existing model.
|
||
if (existing || this._byCid[model.cid]) {
|
||
if (options && options.merge && existing) {
|
||
existing.set(model.attributes, options);
|
||
needsSort = sort;
|
||
}
|
||
models.splice(i, 1);
|
||
continue;
|
||
}
|
||
|
||
// Listen to added models' events, and index models for lookup by
|
||
// `id` and by `cid`.
|
||
model.on('all', this._onModelEvent, this);
|
||
this._byCid[model.cid] = model;
|
||
if (model.id != null) this._byId[model.id] = model;
|
||
}
|
||
|
||
// See if sorting is needed, update `length` and splice in new models.
|
||
if (models.length) needsSort = sort;
|
||
this.length += models.length;
|
||
args = [at != null ? at : this.models.length, 0];
|
||
push.apply(args, models);
|
||
splice.apply(this.models, args);
|
||
|
||
// Sort the collection if appropriate.
|
||
if (needsSort && this.comparator && at == null) this.sort({silent: true});
|
||
|
||
if (options && options.silent) return this;
|
||
|
||
// Trigger `add` events.
|
||
while (model = models.shift()) {
|
||
model.trigger('add', model, this, options);
|
||
}
|
||
|
||
return this;
|
||
},
|
||
|
||
// Remove a model, or a list of models from the set. Pass silent to avoid
|
||
// firing the `remove` event for every model removed.
|
||
remove: function(models, options) {
|
||
var i, l, index, model;
|
||
options || (options = {});
|
||
models = _.isArray(models) ? models.slice() : [models];
|
||
for (i = 0, l = models.length; i < l; i++) {
|
||
model = this.get(models[i]);
|
||
if (!model) continue;
|
||
delete this._byId[model.id];
|
||
delete this._byCid[model.cid];
|
||
index = this.indexOf(model);
|
||
this.models.splice(index, 1);
|
||
this.length--;
|
||
if (!options.silent) {
|
||
options.index = index;
|
||
model.trigger('remove', model, this, options);
|
||
}
|
||
this._removeReference(model);
|
||
}
|
||
return this;
|
||
},
|
||
|
||
// Add a model to the end of the collection.
|
||
push: function(model, options) {
|
||
model = this._prepareModel(model, options);
|
||
this.add(model, _.extend({at: this.length}, options));
|
||
return model;
|
||
},
|
||
|
||
// Remove a model from the end of the collection.
|
||
pop: function(options) {
|
||
var model = this.at(this.length - 1);
|
||
this.remove(model, options);
|
||
return model;
|
||
},
|
||
|
||
// Add a model to the beginning of the collection.
|
||
unshift: function(model, options) {
|
||
model = this._prepareModel(model, options);
|
||
this.add(model, _.extend({at: 0}, options));
|
||
return model;
|
||
},
|
||
|
||
// Remove a model from the beginning of the collection.
|
||
shift: function(options) {
|
||
var model = this.at(0);
|
||
this.remove(model, options);
|
||
return model;
|
||
},
|
||
|
||
// Slice out a sub-array of models from the collection.
|
||
slice: function(begin, end) {
|
||
return this.models.slice(begin, end);
|
||
},
|
||
|
||
// Get a model from the set by id.
|
||
get: function(obj) {
|
||
if (obj == null) return void 0;
|
||
return this._byId[obj.id != null ? obj.id : obj] || this._byCid[obj.cid || obj];
|
||
},
|
||
|
||
// Get the model at the given index.
|
||
at: function(index) {
|
||
return this.models[index];
|
||
},
|
||
|
||
// Return models with matching attributes. Useful for simple cases of `filter`.
|
||
where: function(attrs) {
|
||
if (_.isEmpty(attrs)) return [];
|
||
return this.filter(function(model) {
|
||
for (var key in attrs) {
|
||
if (attrs[key] !== model.get(key)) return false;
|
||
}
|
||
return true;
|
||
});
|
||
},
|
||
|
||
// Force the collection to re-sort itself. You don't need to call this under
|
||
// normal circumstances, as the set will maintain sort order as each item
|
||
// is added.
|
||
sort: function(options) {
|
||
if (!this.comparator) {
|
||
throw new Error('Cannot sort a set without a comparator');
|
||
}
|
||
|
||
if (_.isString(this.comparator) || this.comparator.length === 1) {
|
||
this.models = this.sortBy(this.comparator, this);
|
||
} else {
|
||
this.models.sort(_.bind(this.comparator, this));
|
||
}
|
||
|
||
if (!options || !options.silent) this.trigger('sort', this, options);
|
||
return this;
|
||
},
|
||
|
||
// Pluck an attribute from each model in the collection.
|
||
pluck: function(attr) {
|
||
return _.invoke(this.models, 'get', attr);
|
||
},
|
||
|
||
// Smartly update a collection with a change set of models, adding,
|
||
// removing, and merging as necessary.
|
||
update: function(models, options) {
|
||
var model, i, l, existing;
|
||
var add = [], remove = [], modelMap = {};
|
||
var idAttr = this.model.prototype.idAttribute;
|
||
options = _.extend({add: true, merge: true, remove: true}, options);
|
||
if (options.parse) models = this.parse(models);
|
||
|
||
// Allow a single model (or no argument) to be passed.
|
||
if (!_.isArray(models)) models = models ? [models] : [];
|
||
|
||
// Proxy to `add` for this case, no need to iterate...
|
||
if (options.add && !options.remove) return this.add(models, options);
|
||
|
||
// Determine which models to add and merge, and which to remove.
|
||
for (i = 0, l = models.length; i < l; i++) {
|
||
model = models[i];
|
||
existing = this.get(model.id || model.cid || model[idAttr]);
|
||
if (options.remove && existing) modelMap[existing.cid] = true;
|
||
if ((options.add && !existing) || (options.merge && existing)) {
|
||
add.push(model);
|
||
}
|
||
}
|
||
if (options.remove) {
|
||
for (i = 0, l = this.models.length; i < l; i++) {
|
||
model = this.models[i];
|
||
if (!modelMap[model.cid]) remove.push(model);
|
||
}
|
||
}
|
||
|
||
// Remove models (if applicable) before we add and merge the rest.
|
||
if (remove.length) this.remove(remove, options);
|
||
if (add.length) this.add(add, options);
|
||
return this;
|
||
},
|
||
|
||
// When you have more items than you want to add or remove individually,
|
||
// you can reset the entire set with a new list of models, without firing
|
||
// any `add` or `remove` events. Fires `reset` when finished.
|
||
reset: function(models, options) {
|
||
options || (options = {});
|
||
if (options.parse) models = this.parse(models);
|
||
for (var i = 0, l = this.models.length; i < l; i++) {
|
||
this._removeReference(this.models[i]);
|
||
}
|
||
options.previousModels = this.models;
|
||
this._reset();
|
||
if (models) this.add(models, _.extend({silent: true}, options));
|
||
if (!options.silent) this.trigger('reset', this, options);
|
||
return this;
|
||
},
|
||
|
||
// Fetch the default set of models for this collection, resetting the
|
||
// collection when they arrive. If `add: true` is passed, appends the
|
||
// models to the collection instead of resetting.
|
||
fetch: function(options) {
|
||
options = options ? _.clone(options) : {};
|
||
if (options.parse === void 0) options.parse = true;
|
||
var collection = this;
|
||
var success = options.success;
|
||
options.success = function(resp, status, xhr) {
|
||
var method = options.update ? 'update' : 'reset';
|
||
collection[method](resp, options);
|
||
if (success) success(collection, resp, options);
|
||
};
|
||
return this.sync('read', this, options);
|
||
},
|
||
|
||
// Create a new instance of a model in this collection. Add the model to the
|
||
// collection immediately, unless `wait: true` is passed, in which case we
|
||
// wait for the server to agree.
|
||
create: function(model, options) {
|
||
var collection = this;
|
||
options = options ? _.clone(options) : {};
|
||
model = this._prepareModel(model, options);
|
||
if (!model) return false;
|
||
if (!options.wait) collection.add(model, options);
|
||
var success = options.success;
|
||
options.success = function(model, resp, options) {
|
||
if (options.wait) collection.add(model, options);
|
||
if (success) success(model, resp, options);
|
||
};
|
||
model.save(null, options);
|
||
return model;
|
||
},
|
||
|
||
// **parse** converts a response into a list of models to be added to the
|
||
// collection. The default implementation is just to pass it through.
|
||
parse: function(resp) {
|
||
return resp;
|
||
},
|
||
|
||
// Create a new collection with an identical list of models as this one.
|
||
clone: function() {
|
||
return new this.constructor(this.models);
|
||
},
|
||
|
||
// Proxy to _'s chain. Can't be proxied the same way the rest of the
|
||
// underscore methods are proxied because it relies on the underscore
|
||
// constructor.
|
||
chain: function() {
|
||
return _(this.models).chain();
|
||
},
|
||
|
||
// Reset all internal state. Called when the collection is reset.
|
||
_reset: function() {
|
||
this.length = 0;
|
||
this.models = [];
|
||
this._byId = {};
|
||
this._byCid = {};
|
||
},
|
||
|
||
// Prepare a model or hash of attributes to be added to this collection.
|
||
_prepareModel: function(attrs, options) {
|
||
if (attrs instanceof Model) {
|
||
if (!attrs.collection) attrs.collection = this;
|
||
return attrs;
|
||
}
|
||
options || (options = {});
|
||
options.collection = this;
|
||
var model = new this.model(attrs, options);
|
||
if (!model._validate(attrs, options)) return false;
|
||
return model;
|
||
},
|
||
|
||
// Internal method to remove a model's ties to a collection.
|
||
_removeReference: function(model) {
|
||
if (this === model.collection) delete model.collection;
|
||
model.off('all', this._onModelEvent, this);
|
||
},
|
||
|
||
// Internal method called every time a model in the set fires an event.
|
||
// Sets need to update their indexes when models change ids. All other
|
||
// events simply proxy through. "add" and "remove" events that originate
|
||
// in other collections are ignored.
|
||
_onModelEvent: function(event, model, collection, options) {
|
||
if ((event === 'add' || event === 'remove') && collection !== this) return;
|
||
if (event === 'destroy') this.remove(model, options);
|
||
if (model && event === 'change:' + model.idAttribute) {
|
||
delete this._byId[model.previous(model.idAttribute)];
|
||
if (model.id != null) this._byId[model.id] = model;
|
||
}
|
||
this.trigger.apply(this, arguments);
|
||
}
|
||
|
||
});
|
||
|
||
// Underscore methods that we want to implement on the Collection.
|
||
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
|
||
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
|
||
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
|
||
'max', 'min', 'sortedIndex', 'toArray', 'size', 'first', 'head', 'take',
|
||
'initial', 'rest', 'tail', 'last', 'without', 'indexOf', 'shuffle',
|
||
'lastIndexOf', 'isEmpty'];
|
||
|
||
// Mix in each Underscore method as a proxy to `Collection#models`.
|
||
_.each(methods, function(method) {
|
||
Collection.prototype[method] = function() {
|
||
var args = slice.call(arguments);
|
||
args.unshift(this.models);
|
||
return _[method].apply(_, args);
|
||
};
|
||
});
|
||
|
||
// Underscore methods that take a property name as an argument.
|
||
var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
|
||
|
||
// Use attributes instead of properties.
|
||
_.each(attributeMethods, function(method) {
|
||
Collection.prototype[method] = function(value, context) {
|
||
var iterator = _.isFunction(value) ? value : function(model) {
|
||
return model.get(value);
|
||
};
|
||
return _[method](this.models, iterator, context);
|
||
};
|
||
});
|
||
|
||
// Backbone.Router
|
||
// ---------------
|
||
|
||
// Routers map faux-URLs to actions, and fire events when routes are
|
||
// matched. Creating a new one sets its `routes` hash, if not set statically.
|
||
var Router = Backbone.Router = function(options) {
|
||
options || (options = {});
|
||
if (options.routes) this.routes = options.routes;
|
||
this._bindRoutes();
|
||
this.initialize.apply(this, arguments);
|
||
};
|
||
|
||
// Cached regular expressions for matching named param parts and splatted
|
||
// parts of route strings.
|
||
var optionalParam = /\((.*?)\)/g;
|
||
var namedParam = /:\w+/g;
|
||
var splatParam = /\*\w+/g;
|
||
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
|
||
|
||
// Set up all inheritable **Backbone.Router** properties and methods.
|
||
_.extend(Router.prototype, Events, {
|
||
|
||
// Initialize is an empty function by default. Override it with your own
|
||
// initialization logic.
|
||
initialize: function(){},
|
||
|
||
// Manually bind a single named route to a callback. For example:
|
||
//
|
||
// this.route('search/:query/p:num', 'search', function(query, num) {
|
||
// ...
|
||
// });
|
||
//
|
||
route: function(route, name, callback) {
|
||
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
||
if (!callback) callback = this[name];
|
||
Backbone.history.route(route, _.bind(function(fragment) {
|
||
var args = this._extractParameters(route, fragment);
|
||
callback && callback.apply(this, args);
|
||
this.trigger.apply(this, ['route:' + name].concat(args));
|
||
Backbone.history.trigger('route', this, name, args);
|
||
}, this));
|
||
return this;
|
||
},
|
||
|
||
// Simple proxy to `Backbone.history` to save a fragment into the history.
|
||
navigate: function(fragment, options) {
|
||
Backbone.history.navigate(fragment, options);
|
||
return this;
|
||
},
|
||
|
||
// Bind all defined routes to `Backbone.history`. We have to reverse the
|
||
// order of the routes here to support behavior where the most general
|
||
// routes can be defined at the bottom of the route map.
|
||
_bindRoutes: function() {
|
||
if (!this.routes) return;
|
||
var route, routes = _.keys(this.routes);
|
||
while ((route = routes.pop()) != null) {
|
||
this.route(route, this.routes[route]);
|
||
}
|
||
},
|
||
|
||
// Convert a route string into a regular expression, suitable for matching
|
||
// against the current location hash.
|
||
_routeToRegExp: function(route) {
|
||
route = route.replace(escapeRegExp, '\\$&')
|
||
.replace(optionalParam, '(?:$1)?')
|
||
.replace(namedParam, '([^\/]+)')
|
||
.replace(splatParam, '(.*?)');
|
||
return new RegExp('^' + route + '$');
|
||
},
|
||
|
||
// Given a route, and a URL fragment that it matches, return the array of
|
||
// extracted parameters.
|
||
_extractParameters: function(route, fragment) {
|
||
return route.exec(fragment).slice(1);
|
||
}
|
||
|
||
});
|
||
|
||
// Backbone.History
|
||
// ----------------
|
||
|
||
// Handles cross-browser history management, based on URL fragments. If the
|
||
// browser does not support `onhashchange`, falls back to polling.
|
||
var History = Backbone.History = function() {
|
||
this.handlers = [];
|
||
_.bindAll(this, 'checkUrl');
|
||
|
||
// #1653 - Ensure that `History` can be used outside of the browser.
|
||
if (typeof window !== 'undefined') {
|
||
this.location = window.location;
|
||
this.history = window.history;
|
||
}
|
||
};
|
||
|
||
// Cached regex for stripping a leading hash/slash and trailing space.
|
||
var routeStripper = /^[#\/]|\s+$/g;
|
||
|
||
// Cached regex for stripping leading and trailing slashes.
|
||
var rootStripper = /^\/+|\/+$/g;
|
||
|
||
// Cached regex for detecting MSIE.
|
||
var isExplorer = /msie [\w.]+/;
|
||
|
||
// Cached regex for removing a trailing slash.
|
||
var trailingSlash = /\/$/;
|
||
|
||
// Has the history handling already been started?
|
||
History.started = false;
|
||
|
||
// Set up all inheritable **Backbone.History** properties and methods.
|
||
_.extend(History.prototype, Events, {
|
||
|
||
// The default interval to poll for hash changes, if necessary, is
|
||
// twenty times a second.
|
||
interval: 50,
|
||
|
||
// Gets the true hash value. Cannot use location.hash directly due to bug
|
||
// in Firefox where location.hash will always be decoded.
|
||
getHash: function(window) {
|
||
var match = (window || this).location.href.match(/#(.*)$/);
|
||
return match ? match[1] : '';
|
||
},
|
||
|
||
// Get the cross-browser normalized URL fragment, either from the URL,
|
||
// the hash, or the override.
|
||
getFragment: function(fragment, forcePushState) {
|
||
if (fragment == null) {
|
||
if (this._hasPushState || !this._wantsHashChange || forcePushState) {
|
||
fragment = this.location.pathname;
|
||
var root = this.root.replace(trailingSlash, '');
|
||
if (!fragment.indexOf(root)) fragment = fragment.substr(root.length);
|
||
} else {
|
||
fragment = this.getHash();
|
||
}
|
||
}
|
||
return fragment.replace(routeStripper, '');
|
||
},
|
||
|
||
// Start the hash change handling, returning `true` if the current URL matches
|
||
// an existing route, and `false` otherwise.
|
||
start: function(options) {
|
||
if (History.started) throw new Error("Backbone.history has already been started");
|
||
History.started = true;
|
||
|
||
// Figure out the initial configuration. Do we need an iframe?
|
||
// Is pushState desired ... is it available?
|
||
this.options = _.extend({}, {root: '/'}, this.options, options);
|
||
this.root = this.options.root;
|
||
this._wantsHashChange = this.options.hashChange !== false;
|
||
this._wantsPushState = !!this.options.pushState;
|
||
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
|
||
var fragment = this.getFragment();
|
||
var docMode = document.documentMode;
|
||
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
|
||
|
||
// Normalize root to always include a leading and trailing slash.
|
||
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
|
||
|
||
if (oldIE && this._wantsHashChange) {
|
||
this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
|
||
this.navigate(fragment);
|
||
}
|
||
|
||
// Depending on whether we're using pushState or hashes, and whether
|
||
// 'onhashchange' is supported, determine how we check the URL state.
|
||
if (this._hasPushState) {
|
||
Backbone.$(window).bind('popstate', this.checkUrl);
|
||
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
|
||
Backbone.$(window).bind('hashchange', this.checkUrl);
|
||
} else if (this._wantsHashChange) {
|
||
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
|
||
}
|
||
|
||
// Determine if we need to change the base url, for a pushState link
|
||
// opened by a non-pushState browser.
|
||
this.fragment = fragment;
|
||
var loc = this.location;
|
||
var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
|
||
|
||
// If we've started off with a route from a `pushState`-enabled browser,
|
||
// but we're currently in a browser that doesn't support it...
|
||
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
|
||
this.fragment = this.getFragment(null, true);
|
||
this.location.replace(this.root + this.location.search + '#' + this.fragment);
|
||
// Return immediately as browser will do redirect to new url
|
||
return true;
|
||
|
||
// Or if we've started out with a hash-based route, but we're currently
|
||
// in a browser where it could be `pushState`-based instead...
|
||
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
|
||
this.fragment = this.getHash().replace(routeStripper, '');
|
||
this.history.replaceState({}, document.title, this.root + this.fragment + loc.search);
|
||
}
|
||
|
||
if (!this.options.silent) return this.loadUrl();
|
||
},
|
||
|
||
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
|
||
// but possibly useful for unit testing Routers.
|
||
stop: function() {
|
||
Backbone.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
|
||
clearInterval(this._checkUrlInterval);
|
||
History.started = false;
|
||
},
|
||
|
||
// Add a route to be tested when the fragment changes. Routes added later
|
||
// may override previous routes.
|
||
route: function(route, callback) {
|
||
this.handlers.unshift({route: route, callback: callback});
|
||
},
|
||
|
||
// Checks the current URL to see if it has changed, and if it has,
|
||
// calls `loadUrl`, normalizing across the hidden iframe.
|
||
checkUrl: function(e) {
|
||
var current = this.getFragment();
|
||
if (current === this.fragment && this.iframe) {
|
||
current = this.getFragment(this.getHash(this.iframe));
|
||
}
|
||
if (current === this.fragment) return false;
|
||
if (this.iframe) this.navigate(current);
|
||
this.loadUrl() || this.loadUrl(this.getHash());
|
||
},
|
||
|
||
// Attempt to load the current URL fragment. If a route succeeds with a
|
||
// match, returns `true`. If no defined routes matches the fragment,
|
||
// returns `false`.
|
||
loadUrl: function(fragmentOverride) {
|
||
var fragment = this.fragment = this.getFragment(fragmentOverride);
|
||
var matched = _.any(this.handlers, function(handler) {
|
||
if (handler.route.test(fragment)) {
|
||
handler.callback(fragment);
|
||
return true;
|
||
}
|
||
});
|
||
return matched;
|
||
},
|
||
|
||
// Save a fragment into the hash history, or replace the URL state if the
|
||
// 'replace' option is passed. You are responsible for properly URL-encoding
|
||
// the fragment in advance.
|
||
//
|
||
// The options object can contain `trigger: true` if you wish to have the
|
||
// route callback be fired (not usually desirable), or `replace: true`, if
|
||
// you wish to modify the current URL without adding an entry to the history.
|
||
navigate: function(fragment, options) {
|
||
if (!History.started) return false;
|
||
if (!options || options === true) options = {trigger: options};
|
||
fragment = this.getFragment(fragment || '');
|
||
if (this.fragment === fragment) return;
|
||
this.fragment = fragment;
|
||
var url = this.root + fragment;
|
||
|
||
// If pushState is available, we use it to set the fragment as a real URL.
|
||
if (this._hasPushState) {
|
||
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
|
||
|
||
// If hash changes haven't been explicitly disabled, update the hash
|
||
// fragment to store history.
|
||
} else if (this._wantsHashChange) {
|
||
this._updateHash(this.location, fragment, options.replace);
|
||
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
|
||
// Opening and closing the iframe tricks IE7 and earlier to push a
|
||
// history entry on hash-tag change. When replace is true, we don't
|
||
// want this.
|
||
if(!options.replace) this.iframe.document.open().close();
|
||
this._updateHash(this.iframe.location, fragment, options.replace);
|
||
}
|
||
|
||
// If you've told us that you explicitly don't want fallback hashchange-
|
||
// based history, then `navigate` becomes a page refresh.
|
||
} else {
|
||
return this.location.assign(url);
|
||
}
|
||
if (options.trigger) this.loadUrl(fragment);
|
||
},
|
||
|
||
// Update the hash location, either replacing the current entry, or adding
|
||
// a new one to the browser history.
|
||
_updateHash: function(location, fragment, replace) {
|
||
if (replace) {
|
||
var href = location.href.replace(/(javascript:|#).*$/, '');
|
||
location.replace(href + '#' + fragment);
|
||
} else {
|
||
// #1649 - Some browsers require that `hash` contains a leading #.
|
||
location.hash = '#' + fragment;
|
||
}
|
||
}
|
||
|
||
});
|
||
|
||
// Create the default Backbone.history.
|
||
Backbone.history = new History;
|
||
|
||
// Backbone.View
|
||
// -------------
|
||
|
||
// Creating a Backbone.View creates its initial element outside of the DOM,
|
||
// if an existing element is not provided...
|
||
var View = Backbone.View = function(options) {
|
||
this.cid = _.uniqueId('view');
|
||
this._configure(options || {});
|
||
this._ensureElement();
|
||
this.initialize.apply(this, arguments);
|
||
this.delegateEvents();
|
||
};
|
||
|
||
// Cached regex to split keys for `delegate`.
|
||
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
||
|
||
// List of view options to be merged as properties.
|
||
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
|
||
|
||
// Set up all inheritable **Backbone.View** properties and methods.
|
||
_.extend(View.prototype, Events, {
|
||
|
||
// The default `tagName` of a View's element is `"div"`.
|
||
tagName: 'div',
|
||
|
||
// jQuery delegate for element lookup, scoped to DOM elements within the
|
||
// current view. This should be prefered to global lookups where possible.
|
||
$: function(selector) {
|
||
return this.$el.find(selector);
|
||
},
|
||
|
||
// Initialize is an empty function by default. Override it with your own
|
||
// initialization logic.
|
||
initialize: function(){},
|
||
|
||
// **render** is the core function that your view should override, in order
|
||
// to populate its element (`this.el`), with the appropriate HTML. The
|
||
// convention is for **render** to always return `this`.
|
||
render: function() {
|
||
return this;
|
||
},
|
||
|
||
// Remove this view by taking the element out of the DOM, and removing any
|
||
// applicable Backbone.Events listeners.
|
||
remove: function() {
|
||
this.$el.remove();
|
||
this.stopListening();
|
||
return this;
|
||
},
|
||
|
||
// For small amounts of DOM Elements, where a full-blown template isn't
|
||
// needed, use **make** to manufacture elements, one at a time.
|
||
//
|
||
// var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
|
||
//
|
||
make: function(tagName, attributes, content) {
|
||
var el = document.createElement(tagName);
|
||
if (attributes) Backbone.$(el).attr(attributes);
|
||
if (content != null) Backbone.$(el).html(content);
|
||
return el;
|
||
},
|
||
|
||
// Change the view's element (`this.el` property), including event
|
||
// re-delegation.
|
||
setElement: function(element, delegate) {
|
||
if (this.$el) this.undelegateEvents();
|
||
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
|
||
this.el = this.$el[0];
|
||
if (delegate !== false) this.delegateEvents();
|
||
return this;
|
||
},
|
||
|
||
// Set callbacks, where `this.events` is a hash of
|
||
//
|
||
// *{"event selector": "callback"}*
|
||
//
|
||
// {
|
||
// 'mousedown .title': 'edit',
|
||
// 'click .button': 'save'
|
||
// 'click .open': function(e) { ... }
|
||
// }
|
||
//
|
||
// pairs. Callbacks will be bound to the view, with `this` set properly.
|
||
// Uses event delegation for efficiency.
|
||
// Omitting the selector binds the event to `this.el`.
|
||
// This only works for delegate-able events: not `focus`, `blur`, and
|
||
// not `change`, `submit`, and `reset` in Internet Explorer.
|
||
delegateEvents: function(events) {
|
||
if (!(events || (events = _.result(this, 'events')))) return;
|
||
this.undelegateEvents();
|
||
for (var key in events) {
|
||
var method = events[key];
|
||
if (!_.isFunction(method)) method = this[events[key]];
|
||
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
|
||
var match = key.match(delegateEventSplitter);
|
||
var eventName = match[1], selector = match[2];
|
||
method = _.bind(method, this);
|
||
eventName += '.delegateEvents' + this.cid;
|
||
if (selector === '') {
|
||
this.$el.bind(eventName, method);
|
||
} else {
|
||
this.$el.delegate(selector, eventName, method);
|
||
}
|
||
}
|
||
},
|
||
|
||
// Clears all callbacks previously bound to the view with `delegateEvents`.
|
||
// You usually don't need to use this, but may wish to if you have multiple
|
||
// Backbone views attached to the same DOM element.
|
||
undelegateEvents: function() {
|
||
this.$el.unbind('.delegateEvents' + this.cid);
|
||
},
|
||
|
||
// Performs the initial configuration of a View with a set of options.
|
||
// Keys with special meaning *(model, collection, id, className)*, are
|
||
// attached directly to the view.
|
||
_configure: function(options) {
|
||
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
|
||
_.extend(this, _.pick(options, viewOptions));
|
||
this.options = options;
|
||
},
|
||
|
||
// Ensure that the View has a DOM element to render into.
|
||
// If `this.el` is a string, pass it through `$()`, take the first
|
||
// matching element, and re-assign it to `el`. Otherwise, create
|
||
// an element from the `id`, `className` and `tagName` properties.
|
||
_ensureElement: function() {
|
||
if (!this.el) {
|
||
var attrs = _.extend({}, _.result(this, 'attributes'));
|
||
if (this.id) attrs.id = _.result(this, 'id');
|
||
if (this.className) attrs['class'] = _.result(this, 'className');
|
||
this.setElement(this.make(_.result(this, 'tagName'), attrs), false);
|
||
} else {
|
||
this.setElement(_.result(this, 'el'), false);
|
||
}
|
||
}
|
||
|
||
});
|
||
|
||
// Backbone.sync
|
||
// -------------
|
||
|
||
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
|
||
var methodMap = {
|
||
'create': 'POST',
|
||
'update': 'PUT',
|
||
'patch': 'PATCH',
|
||
'delete': 'DELETE',
|
||
'read': 'GET'
|
||
};
|
||
|
||
// Override this function to change the manner in which Backbone persists
|
||
// models to the server. You will be passed the type of request, and the
|
||
// model in question. By default, makes a RESTful Ajax request
|
||
// to the model's `url()`. Some possible customizations could be:
|
||
//
|
||
// * Use `setTimeout` to batch rapid-fire updates into a single request.
|
||
// * Send up the models as XML instead of JSON.
|
||
// * Persist models via WebSockets instead of Ajax.
|
||
//
|
||
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
|
||
// as `POST`, with a `_method` parameter containing the true HTTP method,
|
||
// as well as all requests with the body as `application/x-www-form-urlencoded`
|
||
// instead of `application/json` with the model in a param named `model`.
|
||
// Useful when interfacing with server-side languages like **PHP** that make
|
||
// it difficult to read the body of `PUT` requests.
|
||
Backbone.sync = function(method, model, options) {
|
||
var type = methodMap[method];
|
||
|
||
// Default options, unless specified.
|
||
_.defaults(options || (options = {}), {
|
||
emulateHTTP: Backbone.emulateHTTP,
|
||
emulateJSON: Backbone.emulateJSON
|
||
});
|
||
|
||
// Default JSON-request options.
|
||
var params = {type: type, dataType: 'json'};
|
||
|
||
// Ensure that we have a URL.
|
||
if (!options.url) {
|
||
params.url = _.result(model, 'url') || urlError();
|
||
}
|
||
|
||
// Ensure that we have the appropriate request data.
|
||
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
||
params.contentType = 'application/json';
|
||
params.data = JSON.stringify(options.attrs || model.toJSON(options));
|
||
}
|
||
|
||
// For older servers, emulate JSON by encoding the request into an HTML-form.
|
||
if (options.emulateJSON) {
|
||
params.contentType = 'application/x-www-form-urlencoded';
|
||
params.data = params.data ? {model: params.data} : {};
|
||
}
|
||
|
||
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
|
||
// And an `X-HTTP-Method-Override` header.
|
||
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
|
||
params.type = 'POST';
|
||
if (options.emulateJSON) params.data._method = type;
|
||
var beforeSend = options.beforeSend;
|
||
options.beforeSend = function(xhr) {
|
||
xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
||
if (beforeSend) return beforeSend.apply(this, arguments);
|
||
};
|
||
}
|
||
|
||
// Don't process data on a non-GET request.
|
||
if (params.type !== 'GET' && !options.emulateJSON) {
|
||
params.processData = false;
|
||
}
|
||
|
||
var success = options.success;
|
||
options.success = function(resp, status, xhr) {
|
||
if (success) success(resp, status, xhr);
|
||
model.trigger('sync', model, resp, options);
|
||
};
|
||
|
||
var error = options.error;
|
||
options.error = function(xhr, status, thrown) {
|
||
if (error) error(model, xhr, options);
|
||
model.trigger('error', model, xhr, options);
|
||
};
|
||
|
||
// Make the request, allowing the user to override any Ajax options.
|
||
var xhr = Backbone.ajax(_.extend(params, options));
|
||
model.trigger('request', model, xhr, options);
|
||
return xhr;
|
||
};
|
||
|
||
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
|
||
Backbone.ajax = function() {
|
||
return Backbone.$.ajax.apply(Backbone.$, arguments);
|
||
};
|
||
|
||
// Helpers
|
||
// -------
|
||
|
||
// Helper function to correctly set up the prototype chain, for subclasses.
|
||
// Similar to `goog.inherits`, but uses a hash of prototype properties and
|
||
// class properties to be extended.
|
||
var extend = function(protoProps, staticProps) {
|
||
var parent = this;
|
||
var child;
|
||
|
||
// The constructor function for the new subclass is either defined by you
|
||
// (the "constructor" property in your `extend` definition), or defaulted
|
||
// by us to simply call the parent's constructor.
|
||
if (protoProps && _.has(protoProps, 'constructor')) {
|
||
child = protoProps.constructor;
|
||
} else {
|
||
child = function(){ parent.apply(this, arguments); };
|
||
}
|
||
|
||
// Add static properties to the constructor function, if supplied.
|
||
_.extend(child, parent, staticProps);
|
||
|
||
// Set the prototype chain to inherit from `parent`, without calling
|
||
// `parent`'s constructor function.
|
||
var Surrogate = function(){ this.constructor = child; };
|
||
Surrogate.prototype = parent.prototype;
|
||
child.prototype = new Surrogate;
|
||
|
||
// Add prototype properties (instance properties) to the subclass,
|
||
// if supplied.
|
||
if (protoProps) _.extend(child.prototype, protoProps);
|
||
|
||
// Set a convenience property in case the parent's prototype is needed
|
||
// later.
|
||
child.__super__ = parent.prototype;
|
||
|
||
return child;
|
||
};
|
||
|
||
// Set up inheritance for the model, collection, router, view and history.
|
||
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
|
||
|
||
// Throw an error when a URL is needed, and none is supplied.
|
||
var urlError = function() {
|
||
throw new Error('A "url" property or function must be specified');
|
||
};
|
||
|
||
}).call(this);
|
||
|
||
});
|
||
|
||
require.define("/node_modules/backbone/node_modules/underscore/package.json",function(require,module,exports,__dirname,__filename,process,global){module.exports = {"main":"underscore.js"}
|
||
});
|
||
|
||
require.define("/node_modules/backbone/node_modules/underscore/underscore.js",function(require,module,exports,__dirname,__filename,process,global){// Underscore.js 1.4.3
|
||
// http://underscorejs.org
|
||
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||
// Underscore may be freely distributed under the MIT license.
|
||
|
||
(function() {
|
||
|
||
// Baseline setup
|
||
// --------------
|
||
|
||
// Establish the root object, `window` in the browser, or `global` on the server.
|
||
var root = this;
|
||
|
||
// Save the previous value of the `_` variable.
|
||
var previousUnderscore = root._;
|
||
|
||
// Establish the object that gets returned to break out of a loop iteration.
|
||
var breaker = {};
|
||
|
||
// Save bytes in the minified (but not gzipped) version:
|
||
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
|
||
|
||
// Create quick reference variables for speed access to core prototypes.
|
||
var push = ArrayProto.push,
|
||
slice = ArrayProto.slice,
|
||
concat = ArrayProto.concat,
|
||
toString = ObjProto.toString,
|
||
hasOwnProperty = ObjProto.hasOwnProperty;
|
||
|
||
// All **ECMAScript 5** native function implementations that we hope to use
|
||
// are declared here.
|
||
var
|
||
nativeForEach = ArrayProto.forEach,
|
||
nativeMap = ArrayProto.map,
|
||
nativeReduce = ArrayProto.reduce,
|
||
nativeReduceRight = ArrayProto.reduceRight,
|
||
nativeFilter = ArrayProto.filter,
|
||
nativeEvery = ArrayProto.every,
|
||
nativeSome = ArrayProto.some,
|
||
nativeIndexOf = ArrayProto.indexOf,
|
||
nativeLastIndexOf = ArrayProto.lastIndexOf,
|
||
nativeIsArray = Array.isArray,
|
||
nativeKeys = Object.keys,
|
||
nativeBind = FuncProto.bind;
|
||
|
||
// Create a safe reference to the Underscore object for use below.
|
||
var _ = function(obj) {
|
||
if (obj instanceof _) return obj;
|
||
if (!(this instanceof _)) return new _(obj);
|
||
this._wrapped = obj;
|
||
};
|
||
|
||
// Export the Underscore object for **Node.js**, with
|
||
// backwards-compatibility for the old `require()` API. If we're in
|
||
// the browser, add `_` as a global object via a string identifier,
|
||
// for Closure Compiler "advanced" mode.
|
||
if (typeof exports !== 'undefined') {
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
exports = module.exports = _;
|
||
}
|
||
exports._ = _;
|
||
} else {
|
||
root._ = _;
|
||
}
|
||
|
||
// Current version.
|
||
_.VERSION = '1.4.3';
|
||
|
||
// Collection Functions
|
||
// --------------------
|
||
|
||
// The cornerstone, an `each` implementation, aka `forEach`.
|
||
// Handles objects with the built-in `forEach`, arrays, and raw objects.
|
||
// Delegates to **ECMAScript 5**'s native `forEach` if available.
|
||
var each = _.each = _.forEach = function(obj, iterator, context) {
|
||
if (obj == null) return;
|
||
if (nativeForEach && obj.forEach === nativeForEach) {
|
||
obj.forEach(iterator, context);
|
||
} else if (obj.length === +obj.length) {
|
||
for (var i = 0, l = obj.length; i < l; i++) {
|
||
if (iterator.call(context, obj[i], i, obj) === breaker) return;
|
||
}
|
||
} else {
|
||
for (var key in obj) {
|
||
if (_.has(obj, key)) {
|
||
if (iterator.call(context, obj[key], key, obj) === breaker) return;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Return the results of applying the iterator to each element.
|
||
// Delegates to **ECMAScript 5**'s native `map` if available.
|
||
_.map = _.collect = function(obj, iterator, context) {
|
||
var results = [];
|
||
if (obj == null) return results;
|
||
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
|
||
each(obj, function(value, index, list) {
|
||
results[results.length] = iterator.call(context, value, index, list);
|
||
});
|
||
return results;
|
||
};
|
||
|
||
var reduceError = 'Reduce of empty array with no initial value';
|
||
|
||
// **Reduce** builds up a single result from a list of values, aka `inject`,
|
||
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
|
||
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
|
||
var initial = arguments.length > 2;
|
||
if (obj == null) obj = [];
|
||
if (nativeReduce && obj.reduce === nativeReduce) {
|
||
if (context) iterator = _.bind(iterator, context);
|
||
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
|
||
}
|
||
each(obj, function(value, index, list) {
|
||
if (!initial) {
|
||
memo = value;
|
||
initial = true;
|
||
} else {
|
||
memo = iterator.call(context, memo, value, index, list);
|
||
}
|
||
});
|
||
if (!initial) throw new TypeError(reduceError);
|
||
return memo;
|
||
};
|
||
|
||
// The right-associative version of reduce, also known as `foldr`.
|
||
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
|
||
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
|
||
var initial = arguments.length > 2;
|
||
if (obj == null) obj = [];
|
||
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
|
||
if (context) iterator = _.bind(iterator, context);
|
||
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
|
||
}
|
||
var length = obj.length;
|
||
if (length !== +length) {
|
||
var keys = _.keys(obj);
|
||
length = keys.length;
|
||
}
|
||
each(obj, function(value, index, list) {
|
||
index = keys ? keys[--length] : --length;
|
||
if (!initial) {
|
||
memo = obj[index];
|
||
initial = true;
|
||
} else {
|
||
memo = iterator.call(context, memo, obj[index], index, list);
|
||
}
|
||
});
|
||
if (!initial) throw new TypeError(reduceError);
|
||
return memo;
|
||
};
|
||
|
||
// Return the first value which passes a truth test. Aliased as `detect`.
|
||
_.find = _.detect = function(obj, iterator, context) {
|
||
var result;
|
||
any(obj, function(value, index, list) {
|
||
if (iterator.call(context, value, index, list)) {
|
||
result = value;
|
||
return true;
|
||
}
|
||
});
|
||
return result;
|
||
};
|
||
|
||
// Return all the elements that pass a truth test.
|
||
// Delegates to **ECMAScript 5**'s native `filter` if available.
|
||
// Aliased as `select`.
|
||
_.filter = _.select = function(obj, iterator, context) {
|
||
var results = [];
|
||
if (obj == null) return results;
|
||
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
|
||
each(obj, function(value, index, list) {
|
||
if (iterator.call(context, value, index, list)) results[results.length] = value;
|
||
});
|
||
return results;
|
||
};
|
||
|
||
// Return all the elements for which a truth test fails.
|
||
_.reject = function(obj, iterator, context) {
|
||
return _.filter(obj, function(value, index, list) {
|
||
return !iterator.call(context, value, index, list);
|
||
}, context);
|
||
};
|
||
|
||
// Determine whether all of the elements match a truth test.
|
||
// Delegates to **ECMAScript 5**'s native `every` if available.
|
||
// Aliased as `all`.
|
||
_.every = _.all = function(obj, iterator, context) {
|
||
iterator || (iterator = _.identity);
|
||
var result = true;
|
||
if (obj == null) return result;
|
||
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
|
||
each(obj, function(value, index, list) {
|
||
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
|
||
});
|
||
return !!result;
|
||
};
|
||
|
||
// Determine if at least one element in the object matches a truth test.
|
||
// Delegates to **ECMAScript 5**'s native `some` if available.
|
||
// Aliased as `any`.
|
||
var any = _.some = _.any = function(obj, iterator, context) {
|
||
iterator || (iterator = _.identity);
|
||
var result = false;
|
||
if (obj == null) return result;
|
||
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
|
||
each(obj, function(value, index, list) {
|
||
if (result || (result = iterator.call(context, value, index, list))) return breaker;
|
||
});
|
||
return !!result;
|
||
};
|
||
|
||
// Determine if the array or object contains a given value (using `===`).
|
||
// Aliased as `include`.
|
||
_.contains = _.include = function(obj, target) {
|
||
if (obj == null) return false;
|
||
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
|
||
return any(obj, function(value) {
|
||
return value === target;
|
||
});
|
||
};
|
||
|
||
// Invoke a method (with arguments) on every item in a collection.
|
||
_.invoke = function(obj, method) {
|
||
var args = slice.call(arguments, 2);
|
||
return _.map(obj, function(value) {
|
||
return (_.isFunction(method) ? method : value[method]).apply(value, args);
|
||
});
|
||
};
|
||
|
||
// Convenience version of a common use case of `map`: fetching a property.
|
||
_.pluck = function(obj, key) {
|
||
return _.map(obj, function(value){ return value[key]; });
|
||
};
|
||
|
||
// Convenience version of a common use case of `filter`: selecting only objects
|
||
// with specific `key:value` pairs.
|
||
_.where = function(obj, attrs) {
|
||
if (_.isEmpty(attrs)) return [];
|
||
return _.filter(obj, function(value) {
|
||
for (var key in attrs) {
|
||
if (attrs[key] !== value[key]) return false;
|
||
}
|
||
return true;
|
||
});
|
||
};
|
||
|
||
// Return the maximum element or (element-based computation).
|
||
// Can't optimize arrays of integers longer than 65,535 elements.
|
||
// See: https://bugs.webkit.org/show_bug.cgi?id=80797
|
||
_.max = function(obj, iterator, context) {
|
||
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
|
||
return Math.max.apply(Math, obj);
|
||
}
|
||
if (!iterator && _.isEmpty(obj)) return -Infinity;
|
||
var result = {computed : -Infinity, value: -Infinity};
|
||
each(obj, function(value, index, list) {
|
||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||
computed >= result.computed && (result = {value : value, computed : computed});
|
||
});
|
||
return result.value;
|
||
};
|
||
|
||
// Return the minimum element (or element-based computation).
|
||
_.min = function(obj, iterator, context) {
|
||
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
|
||
return Math.min.apply(Math, obj);
|
||
}
|
||
if (!iterator && _.isEmpty(obj)) return Infinity;
|
||
var result = {computed : Infinity, value: Infinity};
|
||
each(obj, function(value, index, list) {
|
||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||
computed < result.computed && (result = {value : value, computed : computed});
|
||
});
|
||
return result.value;
|
||
};
|
||
|
||
// Shuffle an array.
|
||
_.shuffle = function(obj) {
|
||
var rand;
|
||
var index = 0;
|
||
var shuffled = [];
|
||
each(obj, function(value) {
|
||
rand = _.random(index++);
|
||
shuffled[index - 1] = shuffled[rand];
|
||
shuffled[rand] = value;
|
||
});
|
||
return shuffled;
|
||
};
|
||
|
||
// An internal function to generate lookup iterators.
|
||
var lookupIterator = function(value) {
|
||
return _.isFunction(value) ? value : function(obj){ return obj[value]; };
|
||
};
|
||
|
||
// Sort the object's values by a criterion produced by an iterator.
|
||
_.sortBy = function(obj, value, context) {
|
||
var iterator = lookupIterator(value);
|
||
return _.pluck(_.map(obj, function(value, index, list) {
|
||
return {
|
||
value : value,
|
||
index : index,
|
||
criteria : iterator.call(context, value, index, list)
|
||
};
|
||
}).sort(function(left, right) {
|
||
var a = left.criteria;
|
||
var b = right.criteria;
|
||
if (a !== b) {
|
||
if (a > b || a === void 0) return 1;
|
||
if (a < b || b === void 0) return -1;
|
||
}
|
||
return left.index < right.index ? -1 : 1;
|
||
}), 'value');
|
||
};
|
||
|
||
// An internal function used for aggregate "group by" operations.
|
||
var group = function(obj, value, context, behavior) {
|
||
var result = {};
|
||
var iterator = lookupIterator(value || _.identity);
|
||
each(obj, function(value, index) {
|
||
var key = iterator.call(context, value, index, obj);
|
||
behavior(result, key, value);
|
||
});
|
||
return result;
|
||
};
|
||
|
||
// Groups the object's values by a criterion. Pass either a string attribute
|
||
// to group by, or a function that returns the criterion.
|
||
_.groupBy = function(obj, value, context) {
|
||
return group(obj, value, context, function(result, key, value) {
|
||
(_.has(result, key) ? result[key] : (result[key] = [])).push(value);
|
||
});
|
||
};
|
||
|
||
// Counts instances of an object that group by a certain criterion. Pass
|
||
// either a string attribute to count by, or a function that returns the
|
||
// criterion.
|
||
_.countBy = function(obj, value, context) {
|
||
return group(obj, value, context, function(result, key) {
|
||
if (!_.has(result, key)) result[key] = 0;
|
||
result[key]++;
|
||
});
|
||
};
|
||
|
||
// Use a comparator function to figure out the smallest index at which
|
||
// an object should be inserted so as to maintain order. Uses binary search.
|
||
_.sortedIndex = function(array, obj, iterator, context) {
|
||
iterator = iterator == null ? _.identity : lookupIterator(iterator);
|
||
var value = iterator.call(context, obj);
|
||
var low = 0, high = array.length;
|
||
while (low < high) {
|
||
var mid = (low + high) >>> 1;
|
||
iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
|
||
}
|
||
return low;
|
||
};
|
||
|
||
// Safely convert anything iterable into a real, live array.
|
||
_.toArray = function(obj) {
|
||
if (!obj) return [];
|
||
if (_.isArray(obj)) return slice.call(obj);
|
||
if (obj.length === +obj.length) return _.map(obj, _.identity);
|
||
return _.values(obj);
|
||
};
|
||
|
||
// Return the number of elements in an object.
|
||
_.size = function(obj) {
|
||
if (obj == null) return 0;
|
||
return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
|
||
};
|
||
|
||
// Array Functions
|
||
// ---------------
|
||
|
||
// Get the first element of an array. Passing **n** will return the first N
|
||
// values in the array. Aliased as `head` and `take`. The **guard** check
|
||
// allows it to work with `_.map`.
|
||
_.first = _.head = _.take = function(array, n, guard) {
|
||
if (array == null) return void 0;
|
||
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
|
||
};
|
||
|
||
// Returns everything but the last entry of the array. Especially useful on
|
||
// the arguments object. Passing **n** will return all the values in
|
||
// the array, excluding the last N. The **guard** check allows it to work with
|
||
// `_.map`.
|
||
_.initial = function(array, n, guard) {
|
||
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
|
||
};
|
||
|
||
// Get the last element of an array. Passing **n** will return the last N
|
||
// values in the array. The **guard** check allows it to work with `_.map`.
|
||
_.last = function(array, n, guard) {
|
||
if (array == null) return void 0;
|
||
if ((n != null) && !guard) {
|
||
return slice.call(array, Math.max(array.length - n, 0));
|
||
} else {
|
||
return array[array.length - 1];
|
||
}
|
||
};
|
||
|
||
// Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
|
||
// Especially useful on the arguments object. Passing an **n** will return
|
||
// the rest N values in the array. The **guard**
|
||
// check allows it to work with `_.map`.
|
||
_.rest = _.tail = _.drop = function(array, n, guard) {
|
||
return slice.call(array, (n == null) || guard ? 1 : n);
|
||
};
|
||
|
||
// Trim out all falsy values from an array.
|
||
_.compact = function(array) {
|
||
return _.filter(array, _.identity);
|
||
};
|
||
|
||
// Internal implementation of a recursive `flatten` function.
|
||
var flatten = function(input, shallow, output) {
|
||
each(input, function(value) {
|
||
if (_.isArray(value)) {
|
||
shallow ? push.apply(output, value) : flatten(value, shallow, output);
|
||
} else {
|
||
output.push(value);
|
||
}
|
||
});
|
||
return output;
|
||
};
|
||
|
||
// Return a completely flattened version of an array.
|
||
_.flatten = function(array, shallow) {
|
||
return flatten(array, shallow, []);
|
||
};
|
||
|
||
// Return a version of the array that does not contain the specified value(s).
|
||
_.without = function(array) {
|
||
return _.difference(array, slice.call(arguments, 1));
|
||
};
|
||
|
||
// Produce a duplicate-free version of the array. If the array has already
|
||
// been sorted, you have the option of using a faster algorithm.
|
||
// Aliased as `unique`.
|
||
_.uniq = _.unique = function(array, isSorted, iterator, context) {
|
||
if (_.isFunction(isSorted)) {
|
||
context = iterator;
|
||
iterator = isSorted;
|
||
isSorted = false;
|
||
}
|
||
var initial = iterator ? _.map(array, iterator, context) : array;
|
||
var results = [];
|
||
var seen = [];
|
||
each(initial, function(value, index) {
|
||
if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
|
||
seen.push(value);
|
||
results.push(array[index]);
|
||
}
|
||
});
|
||
return results;
|
||
};
|
||
|
||
// Produce an array that contains the union: each distinct element from all of
|
||
// the passed-in arrays.
|
||
_.union = function() {
|
||
return _.uniq(concat.apply(ArrayProto, arguments));
|
||
};
|
||
|
||
// Produce an array that contains every item shared between all the
|
||
// passed-in arrays.
|
||
_.intersection = function(array) {
|
||
var rest = slice.call(arguments, 1);
|
||
return _.filter(_.uniq(array), function(item) {
|
||
return _.every(rest, function(other) {
|
||
return _.indexOf(other, item) >= 0;
|
||
});
|
||
});
|
||
};
|
||
|
||
// Take the difference between one array and a number of other arrays.
|
||
// Only the elements present in just the first array will remain.
|
||
_.difference = function(array) {
|
||
var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
|
||
return _.filter(array, function(value){ return !_.contains(rest, value); });
|
||
};
|
||
|
||
// Zip together multiple lists into a single array -- elements that share
|
||
// an index go together.
|
||
_.zip = function() {
|
||
var args = slice.call(arguments);
|
||
var length = _.max(_.pluck(args, 'length'));
|
||
var results = new Array(length);
|
||
for (var i = 0; i < length; i++) {
|
||
results[i] = _.pluck(args, "" + i);
|
||
}
|
||
return results;
|
||
};
|
||
|
||
// Converts lists into objects. Pass either a single array of `[key, value]`
|
||
// pairs, or two parallel arrays of the same length -- one of keys, and one of
|
||
// the corresponding values.
|
||
_.object = function(list, values) {
|
||
if (list == null) return {};
|
||
var result = {};
|
||
for (var i = 0, l = list.length; i < l; i++) {
|
||
if (values) {
|
||
result[list[i]] = values[i];
|
||
} else {
|
||
result[list[i][0]] = list[i][1];
|
||
}
|
||
}
|
||
return result;
|
||
};
|
||
|
||
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
|
||
// we need this function. Return the position of the first occurrence of an
|
||
// item in an array, or -1 if the item is not included in the array.
|
||
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
|
||
// If the array is large and already in sort order, pass `true`
|
||
// for **isSorted** to use binary search.
|
||
_.indexOf = function(array, item, isSorted) {
|
||
if (array == null) return -1;
|
||
var i = 0, l = array.length;
|
||
if (isSorted) {
|
||
if (typeof isSorted == 'number') {
|
||
i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
|
||
} else {
|
||
i = _.sortedIndex(array, item);
|
||
return array[i] === item ? i : -1;
|
||
}
|
||
}
|
||
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
|
||
for (; i < l; i++) if (array[i] === item) return i;
|
||
return -1;
|
||
};
|
||
|
||
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
|
||
_.lastIndexOf = function(array, item, from) {
|
||
if (array == null) return -1;
|
||
var hasIndex = from != null;
|
||
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
|
||
return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
|
||
}
|
||
var i = (hasIndex ? from : array.length);
|
||
while (i--) if (array[i] === item) return i;
|
||
return -1;
|
||
};
|
||
|
||
// Generate an integer Array containing an arithmetic progression. A port of
|
||
// the native Python `range()` function. See
|
||
// [the Python documentation](http://docs.python.org/library/functions.html#range).
|
||
_.range = function(start, stop, step) {
|
||
if (arguments.length <= 1) {
|
||
stop = start || 0;
|
||
start = 0;
|
||
}
|
||
step = arguments[2] || 1;
|
||
|
||
var len = Math.max(Math.ceil((stop - start) / step), 0);
|
||
var idx = 0;
|
||
var range = new Array(len);
|
||
|
||
while(idx < len) {
|
||
range[idx++] = start;
|
||
start += step;
|
||
}
|
||
|
||
return range;
|
||
};
|
||
|
||
// Function (ahem) Functions
|
||
// ------------------
|
||
|
||
// Reusable constructor function for prototype setting.
|
||
var ctor = function(){};
|
||
|
||
// Create a function bound to a given object (assigning `this`, and arguments,
|
||
// optionally). Binding with arguments is also known as `curry`.
|
||
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
|
||
// We check for `func.bind` first, to fail fast when `func` is undefined.
|
||
_.bind = function(func, context) {
|
||
var args, bound;
|
||
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
|
||
if (!_.isFunction(func)) throw new TypeError;
|
||
args = slice.call(arguments, 2);
|
||
return bound = function() {
|
||
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
|
||
ctor.prototype = func.prototype;
|
||
var self = new ctor;
|
||
ctor.prototype = null;
|
||
var result = func.apply(self, args.concat(slice.call(arguments)));
|
||
if (Object(result) === result) return result;
|
||
return self;
|
||
};
|
||
};
|
||
|
||
// Bind all of an object's methods to that object. Useful for ensuring that
|
||
// all callbacks defined on an object belong to it.
|
||
_.bindAll = function(obj) {
|
||
var funcs = slice.call(arguments, 1);
|
||
if (funcs.length == 0) funcs = _.functions(obj);
|
||
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
|
||
return obj;
|
||
};
|
||
|
||
// Memoize an expensive function by storing its results.
|
||
_.memoize = function(func, hasher) {
|
||
var memo = {};
|
||
hasher || (hasher = _.identity);
|
||
return function() {
|
||
var key = hasher.apply(this, arguments);
|
||
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
|
||
};
|
||
};
|
||
|
||
// Delays a function for the given number of milliseconds, and then calls
|
||
// it with the arguments supplied.
|
||
_.delay = function(func, wait) {
|
||
var args = slice.call(arguments, 2);
|
||
return setTimeout(function(){ return func.apply(null, args); }, wait);
|
||
};
|
||
|
||
// Defers a function, scheduling it to run after the current call stack has
|
||
// cleared.
|
||
_.defer = function(func) {
|
||
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
|
||
};
|
||
|
||
// Returns a function, that, when invoked, will only be triggered at most once
|
||
// during a given window of time.
|
||
_.throttle = function(func, wait) {
|
||
var context, args, timeout, result;
|
||
var previous = 0;
|
||
var later = function() {
|
||
previous = new Date;
|
||
timeout = null;
|
||
result = func.apply(context, args);
|
||
};
|
||
return function() {
|
||
var now = new Date;
|
||
var remaining = wait - (now - previous);
|
||
context = this;
|
||
args = arguments;
|
||
if (remaining <= 0) {
|
||
clearTimeout(timeout);
|
||
timeout = null;
|
||
previous = now;
|
||
result = func.apply(context, args);
|
||
} else if (!timeout) {
|
||
timeout = setTimeout(later, remaining);
|
||
}
|
||
return result;
|
||
};
|
||
};
|
||
|
||
// Returns a function, that, as long as it continues to be invoked, will not
|
||
// be triggered. The function will be called after it stops being called for
|
||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||
// leading edge, instead of the trailing.
|
||
_.debounce = function(func, wait, immediate) {
|
||
var timeout, result;
|
||
return function() {
|
||
var context = this, args = arguments;
|
||
var later = function() {
|
||
timeout = null;
|
||
if (!immediate) result = func.apply(context, args);
|
||
};
|
||
var callNow = immediate && !timeout;
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
if (callNow) result = func.apply(context, args);
|
||
return result;
|
||
};
|
||
};
|
||
|
||
// Returns a function that will be executed at most one time, no matter how
|
||
// often you call it. Useful for lazy initialization.
|
||
_.once = function(func) {
|
||
var ran = false, memo;
|
||
return function() {
|
||
if (ran) return memo;
|
||
ran = true;
|
||
memo = func.apply(this, arguments);
|
||
func = null;
|
||
return memo;
|
||
};
|
||
};
|
||
|
||
// Returns the first function passed as an argument to the second,
|
||
// allowing you to adjust arguments, run code before and after, and
|
||
// conditionally execute the original function.
|
||
_.wrap = function(func, wrapper) {
|
||
return function() {
|
||
var args = [func];
|
||
push.apply(args, arguments);
|
||
return wrapper.apply(this, args);
|
||
};
|
||
};
|
||
|
||
// Returns a function that is the composition of a list of functions, each
|
||
// consuming the return value of the function that follows.
|
||
_.compose = function() {
|
||
var funcs = arguments;
|
||
return function() {
|
||
var args = arguments;
|
||
for (var i = funcs.length - 1; i >= 0; i--) {
|
||
args = [funcs[i].apply(this, args)];
|
||
}
|
||
return args[0];
|
||
};
|
||
};
|
||
|
||
// Returns a function that will only be executed after being called N times.
|
||
_.after = function(times, func) {
|
||
if (times <= 0) return func();
|
||
return function() {
|
||
if (--times < 1) {
|
||
return func.apply(this, arguments);
|
||
}
|
||
};
|
||
};
|
||
|
||
// Object Functions
|
||
// ----------------
|
||
|
||
// Retrieve the names of an object's properties.
|
||
// Delegates to **ECMAScript 5**'s native `Object.keys`
|
||
_.keys = nativeKeys || function(obj) {
|
||
if (obj !== Object(obj)) throw new TypeError('Invalid object');
|
||
var keys = [];
|
||
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
|
||
return keys;
|
||
};
|
||
|
||
// Retrieve the values of an object's properties.
|
||
_.values = function(obj) {
|
||
var values = [];
|
||
for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
|
||
return values;
|
||
};
|
||
|
||
// Convert an object into a list of `[key, value]` pairs.
|
||
_.pairs = function(obj) {
|
||
var pairs = [];
|
||
for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
|
||
return pairs;
|
||
};
|
||
|
||
// Invert the keys and values of an object. The values must be serializable.
|
||
_.invert = function(obj) {
|
||
var result = {};
|
||
for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
|
||
return result;
|
||
};
|
||
|
||
// Return a sorted list of the function names available on the object.
|
||
// Aliased as `methods`
|
||
_.functions = _.methods = function(obj) {
|
||
var names = [];
|
||
for (var key in obj) {
|
||
if (_.isFunction(obj[key])) names.push(key);
|
||
}
|
||
return names.sort();
|
||
};
|
||
|
||
// Extend a given object with all the properties in passed-in object(s).
|
||
_.extend = function(obj) {
|
||
each(slice.call(arguments, 1), function(source) {
|
||
if (source) {
|
||
for (var prop in source) {
|
||
obj[prop] = source[prop];
|
||
}
|
||
}
|
||
});
|
||
return obj;
|
||
};
|
||
|
||
// Return a copy of the object only containing the whitelisted properties.
|
||
_.pick = function(obj) {
|
||
var copy = {};
|
||
var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
|
||
each(keys, function(key) {
|
||
if (key in obj) copy[key] = obj[key];
|
||
});
|
||
return copy;
|
||
};
|
||
|
||
// Return a copy of the object without the blacklisted properties.
|
||
_.omit = function(obj) {
|
||
var copy = {};
|
||
var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
|
||
for (var key in obj) {
|
||
if (!_.contains(keys, key)) copy[key] = obj[key];
|
||
}
|
||
return copy;
|
||
};
|
||
|
||
// Fill in a given object with default properties.
|
||
_.defaults = function(obj) {
|
||
each(slice.call(arguments, 1), function(source) {
|
||
if (source) {
|
||
for (var prop in source) {
|
||
if (obj[prop] == null) obj[prop] = source[prop];
|
||
}
|
||
}
|
||
});
|
||
return obj;
|
||
};
|
||
|
||
// Create a (shallow-cloned) duplicate of an object.
|
||
_.clone = function(obj) {
|
||
if (!_.isObject(obj)) return obj;
|
||
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
|
||
};
|
||
|
||
// Invokes interceptor with the obj, and then returns obj.
|
||
// The primary purpose of this method is to "tap into" a method chain, in
|
||
// order to perform operations on intermediate results within the chain.
|
||
_.tap = function(obj, interceptor) {
|
||
interceptor(obj);
|
||
return obj;
|
||
};
|
||
|
||
// Internal recursive comparison function for `isEqual`.
|
||
var eq = function(a, b, aStack, bStack) {
|
||
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
||
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
|
||
if (a === b) return a !== 0 || 1 / a == 1 / b;
|
||
// A strict comparison is necessary because `null == undefined`.
|
||
if (a == null || b == null) return a === b;
|
||
// Unwrap any wrapped objects.
|
||
if (a instanceof _) a = a._wrapped;
|
||
if (b instanceof _) b = b._wrapped;
|
||
// Compare `[[Class]]` names.
|
||
var className = toString.call(a);
|
||
if (className != toString.call(b)) return false;
|
||
switch (className) {
|
||
// Strings, numbers, dates, and booleans are compared by value.
|
||
case '[object String]':
|
||
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
|
||
// equivalent to `new String("5")`.
|
||
return a == String(b);
|
||
case '[object Number]':
|
||
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
|
||
// other numeric values.
|
||
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
|
||
case '[object Date]':
|
||
case '[object Boolean]':
|
||
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
|
||
// millisecond representations. Note that invalid dates with millisecond representations
|
||
// of `NaN` are not equivalent.
|
||
return +a == +b;
|
||
// RegExps are compared by their source patterns and flags.
|
||
case '[object RegExp]':
|
||
return a.source == b.source &&
|
||
a.global == b.global &&
|
||
a.multiline == b.multiline &&
|
||
a.ignoreCase == b.ignoreCase;
|
||
}
|
||
if (typeof a != 'object' || typeof b != 'object') return false;
|
||
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
||
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
||
var length = aStack.length;
|
||
while (length--) {
|
||
// Linear search. Performance is inversely proportional to the number of
|
||
// unique nested structures.
|
||
if (aStack[length] == a) return bStack[length] == b;
|
||
}
|
||
// Add the first object to the stack of traversed objects.
|
||
aStack.push(a);
|
||
bStack.push(b);
|
||
var size = 0, result = true;
|
||
// Recursively compare objects and arrays.
|
||
if (className == '[object Array]') {
|
||
// Compare array lengths to determine if a deep comparison is necessary.
|
||
size = a.length;
|
||
result = size == b.length;
|
||
if (result) {
|
||
// Deep compare the contents, ignoring non-numeric properties.
|
||
while (size--) {
|
||
if (!(result = eq(a[size], b[size], aStack, bStack))) break;
|
||
}
|
||
}
|
||
} else {
|
||
// Objects with different constructors are not equivalent, but `Object`s
|
||
// from different frames are.
|
||
var aCtor = a.constructor, bCtor = b.constructor;
|
||
if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
|
||
_.isFunction(bCtor) && (bCtor instanceof bCtor))) {
|
||
return false;
|
||
}
|
||
// Deep compare objects.
|
||
for (var key in a) {
|
||
if (_.has(a, key)) {
|
||
// Count the expected number of properties.
|
||
size++;
|
||
// Deep compare each member.
|
||
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
|
||
}
|
||
}
|
||
// Ensure that both objects contain the same number of properties.
|
||
if (result) {
|
||
for (key in b) {
|
||
if (_.has(b, key) && !(size--)) break;
|
||
}
|
||
result = !size;
|
||
}
|
||
}
|
||
// Remove the first object from the stack of traversed objects.
|
||
aStack.pop();
|
||
bStack.pop();
|
||
return result;
|
||
};
|
||
|
||
// Perform a deep comparison to check if two objects are equal.
|
||
_.isEqual = function(a, b) {
|
||
return eq(a, b, [], []);
|
||
};
|
||
|
||
// Is a given array, string, or object empty?
|
||
// An "empty" object has no enumerable own-properties.
|
||
_.isEmpty = function(obj) {
|
||
if (obj == null) return true;
|
||
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
|
||
for (var key in obj) if (_.has(obj, key)) return false;
|
||
return true;
|
||
};
|
||
|
||
// Is a given value a DOM element?
|
||
_.isElement = function(obj) {
|
||
return !!(obj && obj.nodeType === 1);
|
||
};
|
||
|
||
// Is a given value an array?
|
||
// Delegates to ECMA5's native Array.isArray
|
||
_.isArray = nativeIsArray || function(obj) {
|
||
return toString.call(obj) == '[object Array]';
|
||
};
|
||
|
||
// Is a given variable an object?
|
||
_.isObject = function(obj) {
|
||
return obj === Object(obj);
|
||
};
|
||
|
||
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
|
||
each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
|
||
_['is' + name] = function(obj) {
|
||
return toString.call(obj) == '[object ' + name + ']';
|
||
};
|
||
});
|
||
|
||
// Define a fallback version of the method in browsers (ahem, IE), where
|
||
// there isn't any inspectable "Arguments" type.
|
||
if (!_.isArguments(arguments)) {
|
||
_.isArguments = function(obj) {
|
||
return !!(obj && _.has(obj, 'callee'));
|
||
};
|
||
}
|
||
|
||
// Optimize `isFunction` if appropriate.
|
||
if (typeof (/./) !== 'function') {
|
||
_.isFunction = function(obj) {
|
||
return typeof obj === 'function';
|
||
};
|
||
}
|
||
|
||
// Is a given object a finite number?
|
||
_.isFinite = function(obj) {
|
||
return isFinite(obj) && !isNaN(parseFloat(obj));
|
||
};
|
||
|
||
// Is the given value `NaN`? (NaN is the only number which does not equal itself).
|
||
_.isNaN = function(obj) {
|
||
return _.isNumber(obj) && obj != +obj;
|
||
};
|
||
|
||
// Is a given value a boolean?
|
||
_.isBoolean = function(obj) {
|
||
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
|
||
};
|
||
|
||
// Is a given value equal to null?
|
||
_.isNull = function(obj) {
|
||
return obj === null;
|
||
};
|
||
|
||
// Is a given variable undefined?
|
||
_.isUndefined = function(obj) {
|
||
return obj === void 0;
|
||
};
|
||
|
||
// Shortcut function for checking if an object has a given property directly
|
||
// on itself (in other words, not on a prototype).
|
||
_.has = function(obj, key) {
|
||
return hasOwnProperty.call(obj, key);
|
||
};
|
||
|
||
// Utility Functions
|
||
// -----------------
|
||
|
||
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
|
||
// previous owner. Returns a reference to the Underscore object.
|
||
_.noConflict = function() {
|
||
root._ = previousUnderscore;
|
||
return this;
|
||
};
|
||
|
||
// Keep the identity function around for default iterators.
|
||
_.identity = function(value) {
|
||
return value;
|
||
};
|
||
|
||
// Run a function **n** times.
|
||
_.times = function(n, iterator, context) {
|
||
var accum = Array(n);
|
||
for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
|
||
return accum;
|
||
};
|
||
|
||
// Return a random integer between min and max (inclusive).
|
||
_.random = function(min, max) {
|
||
if (max == null) {
|
||
max = min;
|
||
min = 0;
|
||
}
|
||
return min + (0 | Math.random() * (max - min + 1));
|
||
};
|
||
|
||
// List of HTML entities for escaping.
|
||
var entityMap = {
|
||
escape: {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": ''',
|
||
'/': '/'
|
||
}
|
||
};
|
||
entityMap.unescape = _.invert(entityMap.escape);
|
||
|
||
// Regexes containing the keys and values listed immediately above.
|
||
var entityRegexes = {
|
||
escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
|
||
unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
|
||
};
|
||
|
||
// Functions for escaping and unescaping strings to/from HTML interpolation.
|
||
_.each(['escape', 'unescape'], function(method) {
|
||
_[method] = function(string) {
|
||
if (string == null) return '';
|
||
return ('' + string).replace(entityRegexes[method], function(match) {
|
||
return entityMap[method][match];
|
||
});
|
||
};
|
||
});
|
||
|
||
// If the value of the named property is a function then invoke it;
|
||
// otherwise, return it.
|
||
_.result = function(object, property) {
|
||
if (object == null) return null;
|
||
var value = object[property];
|
||
return _.isFunction(value) ? value.call(object) : value;
|
||
};
|
||
|
||
// Add your own custom functions to the Underscore object.
|
||
_.mixin = function(obj) {
|
||
each(_.functions(obj), function(name){
|
||
var func = _[name] = obj[name];
|
||
_.prototype[name] = function() {
|
||
var args = [this._wrapped];
|
||
push.apply(args, arguments);
|
||
return result.call(this, func.apply(_, args));
|
||
};
|
||
});
|
||
};
|
||
|
||
// Generate a unique integer id (unique within the entire client session).
|
||
// Useful for temporary DOM ids.
|
||
var idCounter = 0;
|
||
_.uniqueId = function(prefix) {
|
||
var id = '' + ++idCounter;
|
||
return prefix ? prefix + id : id;
|
||
};
|
||
|
||
// By default, Underscore uses ERB-style template delimiters, change the
|
||
// following template settings to use alternative delimiters.
|
||
_.templateSettings = {
|
||
evaluate : /<%([\s\S]+?)%>/g,
|
||
interpolate : /<%=([\s\S]+?)%>/g,
|
||
escape : /<%-([\s\S]+?)%>/g
|
||
};
|
||
|
||
// When customizing `templateSettings`, if you don't want to define an
|
||
// interpolation, evaluation or escaping regex, we need one that is
|
||
// guaranteed not to match.
|
||
var noMatch = /(.)^/;
|
||
|
||
// Certain characters need to be escaped so that they can be put into a
|
||
// string literal.
|
||
var escapes = {
|
||
"'": "'",
|
||
'\\': '\\',
|
||
'\r': 'r',
|
||
'\n': 'n',
|
||
'\t': 't',
|
||
'\u2028': 'u2028',
|
||
'\u2029': 'u2029'
|
||
};
|
||
|
||
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
|
||
|
||
// JavaScript micro-templating, similar to John Resig's implementation.
|
||
// Underscore templating handles arbitrary delimiters, preserves whitespace,
|
||
// and correctly escapes quotes within interpolated code.
|
||
_.template = function(text, data, settings) {
|
||
settings = _.defaults({}, settings, _.templateSettings);
|
||
|
||
// Combine delimiters into one regular expression via alternation.
|
||
var matcher = new RegExp([
|
||
(settings.escape || noMatch).source,
|
||
(settings.interpolate || noMatch).source,
|
||
(settings.evaluate || noMatch).source
|
||
].join('|') + '|$', 'g');
|
||
|
||
// Compile the template source, escaping string literals appropriately.
|
||
var index = 0;
|
||
var source = "__p+='";
|
||
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
|
||
source += text.slice(index, offset)
|
||
.replace(escaper, function(match) { return '\\' + escapes[match]; });
|
||
|
||
if (escape) {
|
||
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
|
||
}
|
||
if (interpolate) {
|
||
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
|
||
}
|
||
if (evaluate) {
|
||
source += "';\n" + evaluate + "\n__p+='";
|
||
}
|
||
index = offset + match.length;
|
||
return match;
|
||
});
|
||
source += "';\n";
|
||
|
||
// If a variable is not specified, place data values in local scope.
|
||
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
|
||
|
||
source = "var __t,__p='',__j=Array.prototype.join," +
|
||
"print=function(){__p+=__j.call(arguments,'');};\n" +
|
||
source + "return __p;\n";
|
||
|
||
try {
|
||
var render = new Function(settings.variable || 'obj', '_', source);
|
||
} catch (e) {
|
||
e.source = source;
|
||
throw e;
|
||
}
|
||
|
||
if (data) return render(data, _);
|
||
var template = function(data) {
|
||
return render.call(this, data, _);
|
||
};
|
||
|
||
// Provide the compiled function source as a convenience for precompilation.
|
||
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
|
||
|
||
return template;
|
||
};
|
||
|
||
// Add a "chain" function, which will delegate to the wrapper.
|
||
_.chain = function(obj) {
|
||
return _(obj).chain();
|
||
};
|
||
|
||
// OOP
|
||
// ---------------
|
||
// If Underscore is called as a function, it returns a wrapped object that
|
||
// can be used OO-style. This wrapper holds altered versions of all the
|
||
// underscore functions. Wrapped objects may be chained.
|
||
|
||
// Helper function to continue chaining intermediate results.
|
||
var result = function(obj) {
|
||
return this._chain ? _(obj).chain() : obj;
|
||
};
|
||
|
||
// Add all of the Underscore functions to the wrapper object.
|
||
_.mixin(_);
|
||
|
||
// Add all mutator Array functions to the wrapper.
|
||
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
|
||
var method = ArrayProto[name];
|
||
_.prototype[name] = function() {
|
||
var obj = this._wrapped;
|
||
method.apply(obj, arguments);
|
||
if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
|
||
return result.call(this, obj);
|
||
};
|
||
});
|
||
|
||
// Add all accessor Array functions to the wrapper.
|
||
each(['concat', 'join', 'slice'], function(name) {
|
||
var method = ArrayProto[name];
|
||
_.prototype[name] = function() {
|
||
return result.call(this, method.apply(this._wrapped, arguments));
|
||
};
|
||
});
|
||
|
||
_.extend(_.prototype, {
|
||
|
||
// Start chaining a wrapped Underscore object.
|
||
chain: function() {
|
||
this._chain = true;
|
||
return this;
|
||
},
|
||
|
||
// Extracts the result from a wrapped and chained object.
|
||
value: function() {
|
||
return this._wrapped;
|
||
}
|
||
|
||
});
|
||
|
||
}).call(this);
|
||
|
||
});
|
||
|
||
require.define("/src/js/util/constants.js",function(require,module,exports,__dirname,__filename,process,global){/**
|
||
* Constants....!!!
|
||
*/
|
||
var TIME = {
|
||
betweenCommandsDelay: 400
|
||
};
|
||
|
||
// useful for locks, etc
|
||
var GLOBAL = {
|
||
isAnimating: false
|
||
};
|
||
|
||
var VIEWPORT = {
|
||
minZoom: 0.66,
|
||
maxZoom: 1.25
|
||
};
|
||
|
||
var GRAPHICS = {
|
||
arrowHeadSize: 8,
|
||
|
||
nodeRadius: 17,
|
||
curveControlPointOffset: 50,
|
||
defaultEasing: 'easeInOut',
|
||
defaultAnimationTime: 400,
|
||
|
||
//rectFill: '#FF3A3A',
|
||
rectFill: 'hsb(0.8816909813322127,0.7,1)',
|
||
headRectFill: '#2831FF',
|
||
rectStroke: '#FFF',
|
||
rectStrokeWidth: '3',
|
||
|
||
multiBranchY: 20,
|
||
upstreamHeadOpacity: 0.5,
|
||
upstreamNoneOpacity: 0.2,
|
||
edgeUpstreamHeadOpacity: 0.4,
|
||
edgeUpstreamNoneOpacity: 0.15,
|
||
|
||
visBranchStrokeWidth: 2,
|
||
visBranchStrokeColorNone: '#333',
|
||
|
||
defaultNodeFill: 'hsba(0.5,0.8,0.7,1)',
|
||
defaultNodeStrokeWidth: 2,
|
||
defaultNodeStroke: '#FFF',
|
||
|
||
orphanNodeFill: 'hsb(0.5,0.8,0.7)'
|
||
};
|
||
|
||
exports.GLOBAL = GLOBAL;
|
||
exports.TIME = TIME;
|
||
exports.GRAPHICS = GRAPHICS;
|
||
exports.VIEWPORT = VIEWPORT;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/util/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
exports.isBrowser = function() {
|
||
var inBrowser = String(typeof window) !== 'undefined';
|
||
return inBrowser;
|
||
};
|
||
|
||
exports.splitTextCommand = function(value, func, context) {
|
||
func = _.bind(func, context);
|
||
_.each(value.split(';'), function(command, index) {
|
||
command = _.escape(command);
|
||
command = command
|
||
.replace(/^(\s+)/, '')
|
||
.replace(/(\s+)$/, '')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, "'");
|
||
|
||
if (index > 0 && !command.length) {
|
||
return;
|
||
}
|
||
func(command);
|
||
});
|
||
};
|
||
|
||
});
|
||
|
||
require.define("/src/js/level/sandbox.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;
|
||
|
||
var util = require('../util');
|
||
var Main = require('../app');
|
||
|
||
var Visualization = require('../visuals/visualization').Visualization;
|
||
var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
|
||
var DisabledMap = require('../level/disabledMap').DisabledMap;
|
||
var Command = require('../models/commandModel').Command;
|
||
var GitShim = require('../git/gitShim').GitShim;
|
||
|
||
var ModalTerminal = require('../views').ModalTerminal;
|
||
var ModalAlert = require('../views').ModalAlert;
|
||
|
||
var MultiView = require('../views/multiView').MultiView;
|
||
|
||
var Sandbox = Backbone.View.extend({
|
||
// tag name here is purely vestigial. I made this a view
|
||
// simply to use inheritance and have a nice event system in place
|
||
tagName: 'div',
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
|
||
this.initVisualization(options);
|
||
this.initCommandCollection(options);
|
||
this.initParseWaterfall(options);
|
||
this.initGitShim(options);
|
||
|
||
if (!options.wait) {
|
||
this.takeControl();
|
||
}
|
||
},
|
||
|
||
getDefaultVisEl: function() {
|
||
return $('#canvasWrapper')[0];
|
||
},
|
||
|
||
initVisualization: function(options) {
|
||
this.mainVis = new Visualization({
|
||
el: options.el || this.getDefaultVisEl()
|
||
});
|
||
},
|
||
|
||
initCommandCollection: function(options) {
|
||
// don't add it to just any collection -- adding to the
|
||
// CommandUI collection will put in history
|
||
this.commandCollection = Main.getCommandUI().commandCollection;
|
||
},
|
||
|
||
initParseWaterfall: function(options) {
|
||
this.parseWaterfall = new ParseWaterfall();
|
||
},
|
||
|
||
initGitShim: function(options) {
|
||
/*
|
||
this.gitShim = new GitShim({
|
||
beforeCB: function() { console.log('before'); },
|
||
afterCB: function() { console.log('after'); },
|
||
afterDeferHandler: function(deferred) { deferred.resolve(); },
|
||
});*/
|
||
},
|
||
|
||
takeControl: function() {
|
||
// we will be handling commands that are submitted, mainly to add the sanadbox
|
||
// functionality (which is included by default in ParseWaterfall())
|
||
Main.getEventBaton().stealBaton('commandSubmitted', this.commandSubmitted, this);
|
||
// we obviously take care of sandbox commands
|
||
Main.getEventBaton().stealBaton('processSandboxCommand', this.processSandboxCommand, this);
|
||
|
||
this.insertGitShim();
|
||
},
|
||
|
||
insertGitShim: function() {
|
||
// and our git shim goes in after the git engine is ready so it doesn't steal the baton
|
||
// too early
|
||
if (this.gitShim) {
|
||
this.mainVis.customEvents.on('gitEngineReady', function() {
|
||
this.gitShim.insertShim();
|
||
},this);
|
||
}
|
||
},
|
||
|
||
commandSubmitted: function(value) {
|
||
// allow other things to see this command (aka command history on terminal)
|
||
Main.getEvents().trigger('commandSubmittedPassive', value);
|
||
|
||
util.splitTextCommand(value, function(command) {
|
||
this.commandCollection.add(new Command({
|
||
rawStr: command,
|
||
parseWaterfall: this.parseWaterfall
|
||
}));
|
||
}, this);
|
||
},
|
||
|
||
processSandboxCommand: function(command, deferred) {
|
||
var commandMap = {
|
||
help: this.helpDialog
|
||
};
|
||
var method = commandMap[command.get('method')];
|
||
if (!method) { throw new Error('no method for that wut'); }
|
||
|
||
method.apply(this, [command, deferred]);
|
||
},
|
||
|
||
helpDialog: function(command, deferred) {
|
||
var helpDialog = new MultiView({
|
||
childViews: require('../dialogs/sandbox').helpDialog
|
||
});
|
||
helpDialog.getPromise().then(_.bind(function() {
|
||
// the view has been closed, lets go ahead and resolve our command
|
||
command.finishWith(deferred);
|
||
}, this))
|
||
.done();
|
||
}
|
||
});
|
||
|
||
exports.Sandbox = Sandbox;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/app/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var Constants = require('../util/constants');
|
||
var util = require('../util');
|
||
|
||
/**
|
||
* Globals
|
||
*/
|
||
var events = _.clone(Backbone.Events);
|
||
var commandUI;
|
||
var sandbox;
|
||
var eventBaton;
|
||
|
||
///////////////////////////////////////////////////////////////////////
|
||
|
||
var init = function() {
|
||
/**
|
||
* There is a decent amount of bootstrapping we need just to hook
|
||
* everything up. The init() method takes on these responsibilities,
|
||
* including but not limited to:
|
||
* - setting up Events and EventBaton
|
||
* - calling the constructor for the main visualization
|
||
* - initializing the command input bar
|
||
* - handling window.focus and zoom events
|
||
**/
|
||
var Sandbox = require('../level/sandbox').Sandbox;
|
||
var Level = require('../level').Level;
|
||
var EventBaton = require('../util/eventBaton').EventBaton;
|
||
|
||
eventBaton = new EventBaton();
|
||
commandUI = new CommandUI();
|
||
sandbox = new Level();
|
||
|
||
// we always want to focus the text area to collect input
|
||
var focusTextArea = function() {
|
||
$('#commandTextField').focus();
|
||
};
|
||
focusTextArea();
|
||
|
||
$(window).focus(function(e) {
|
||
eventBaton.trigger('windowFocus', e);
|
||
});
|
||
$(document).click(function(e) {
|
||
eventBaton.trigger('documentClick', e);
|
||
});
|
||
|
||
// zoom level measure, I wish there was a jquery event for this :/
|
||
require('../util/zoomLevel').setupZoomPoll(function(level) {
|
||
eventBaton.trigger('zoomChange', level);
|
||
}, this);
|
||
|
||
eventBaton.stealBaton('zoomChange', function(level) {
|
||
if (level > Constants.VIEWPORT.maxZoom ||
|
||
level < Constants.VIEWPORT.minZoom) {
|
||
var Views = require('../views');
|
||
var view = new Views.ZoomAlertWindow();
|
||
}
|
||
});
|
||
|
||
// the default action on window focus and document click is to just focus the text area
|
||
eventBaton.stealBaton('windowFocus', focusTextArea);
|
||
eventBaton.stealBaton('documentClick', focusTextArea);
|
||
|
||
// but when the input is fired in the text area, we pipe that to whoever is
|
||
// listenining
|
||
var makeKeyListener = function(name) {
|
||
return function() {
|
||
var args = [name];
|
||
_.each(arguments, function(arg) {
|
||
args.push(arg);
|
||
});
|
||
eventBaton.trigger.apply(eventBaton, args);
|
||
};
|
||
};
|
||
|
||
$('#commandTextField').on('keydown', makeKeyListener('keydown'));
|
||
$('#commandTextField').on('keyup', makeKeyListener('keyup'));
|
||
|
||
/* hacky demo functionality */
|
||
if (/\?demo/.test(window.location.href)) {
|
||
setTimeout(function() {
|
||
eventBaton.trigger('commandSubmitted', "gc; git checkout HEAD~1; git commit; git checkout -b bugFix; gc; gc; git rebase -i HEAD~2; git rebase master; git checkout master; gc; gc; git merge bugFix");
|
||
}, 500);
|
||
}
|
||
};
|
||
|
||
$(document).ready(init);
|
||
|
||
/**
|
||
* the UI method simply bootstraps the command buffer and
|
||
* command prompt views. It only interacts with user input
|
||
* and simply pipes commands to the main events system
|
||
**/
|
||
function CommandUI() {
|
||
var Collections = require('../models/collections');
|
||
var CommandViews = require('../views/commandViews');
|
||
|
||
this.commandCollection = new Collections.CommandCollection();
|
||
this.commandBuffer = new Collections.CommandBuffer({
|
||
collection: this.commandCollection
|
||
});
|
||
|
||
this.commandPromptView = new CommandViews.CommandPromptView({
|
||
el: $('#commandLineBar')
|
||
});
|
||
|
||
this.commandLineHistoryView = new CommandViews.CommandLineHistoryView({
|
||
el: $('#commandLineHistory'),
|
||
collection: this.commandCollection
|
||
});
|
||
}
|
||
|
||
exports.getEvents = function() {
|
||
return events;
|
||
};
|
||
|
||
exports.getSandbox = function() {
|
||
return sandbox;
|
||
};
|
||
|
||
exports.getEventBaton = function() {
|
||
return eventBaton;
|
||
};
|
||
|
||
exports.getCommandUI = function() {
|
||
return commandUI;
|
||
};
|
||
|
||
exports.init = init;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/level/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var Q = require('q');
|
||
|
||
var util = require('../util');
|
||
var Main = require('../app');
|
||
|
||
var Sandbox = require('../level/sandbox').Sandbox;
|
||
|
||
var Visualization = require('../visuals/visualization').Visualization;
|
||
var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
|
||
var DisabledMap = require('../level/disabledMap').DisabledMap;
|
||
var Command = require('../models/commandModel').Command;
|
||
var GitShim = require('../git/gitShim').GitShim;
|
||
|
||
var ModalTerminal = require('../views').ModalTerminal;
|
||
var ModalAlert = require('../views').ModalAlert;
|
||
var MultiView = require('../views/multiView').MultiView;
|
||
|
||
var TreeCompare = require('../git/treeCompare').TreeCompare;
|
||
|
||
var Level = Sandbox.extend({
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
options.level = options.level || {};
|
||
|
||
this.gitCommandsIssued = 0;
|
||
this.solved = false;
|
||
// possible options on how stringent to be go here
|
||
this.treeCompare = new TreeCompare();
|
||
|
||
this.goalTreeString = options.level.goalTree;
|
||
if (!this.goalTreeString) {
|
||
console.warn('woah no goal, using random other one');
|
||
this.goalTreeString = '{"branches":{"master":{"target":"C2","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C30"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"}}';
|
||
//this.goalTreeString = '{"branches":{"master":{"target":"C2","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"}}';
|
||
}
|
||
|
||
Sandbox.prototype.initialize.apply(this, [options]);
|
||
},
|
||
|
||
initVisualization: function(options) {
|
||
if (!options.level.startTree) {
|
||
console.warn('No start tree specified for this level!!! using default...');
|
||
}
|
||
this.mainVis = new Visualization({
|
||
el: options.el || this.getDefaultVisEl(),
|
||
treeString: options.level.startTree
|
||
});
|
||
},
|
||
|
||
initParseWaterfall: function(options) {
|
||
this.parseWaterfall = new ParseWaterfall();
|
||
|
||
// if we want to disable certain commands...
|
||
if (options.level.disabledMap) {
|
||
// disable these other commands
|
||
this.parseWaterfall.addFirst(
|
||
new DisabledMap({
|
||
disabledMap: options.level.disabledMap
|
||
}).getInstantCommands()
|
||
);
|
||
}
|
||
},
|
||
|
||
initGitShim: function(options) {
|
||
// ok we definitely want a shim here
|
||
this.gitShim = new GitShim({
|
||
afterCB: _.bind(this.afterCommandCB, this),
|
||
afterDeferHandler: _.bind(this.afterCommandDefer, this)
|
||
});
|
||
},
|
||
|
||
afterCommandCB: function(command) {
|
||
// TODO check if error, but not warning
|
||
this.gitCommandsIssued++;
|
||
},
|
||
|
||
afterCommandDefer: function(defer) {
|
||
if (this.solved) { return; }
|
||
// ok so lets see if they solved it...
|
||
var current = this.mainVis.gitEngine.exportTree();
|
||
var solved = this.treeCompare.compareTrees(current, this.goalTreeString);
|
||
|
||
if (!solved) {
|
||
defer.resolve();
|
||
return;
|
||
}
|
||
|
||
// woohoo!!! they solved the level, lets animate and such
|
||
this.levelSolved(defer);
|
||
},
|
||
|
||
levelSolved: function(defer) {
|
||
this.mainVis.gitVisuals.finishAnimation()
|
||
.then(function() {
|
||
defer.resolve();
|
||
});
|
||
}
|
||
});
|
||
|
||
exports.Level = Level;
|
||
|
||
|
||
});
|
||
|
||
require.define("/node_modules/q/package.json",function(require,module,exports,__dirname,__filename,process,global){module.exports = {"main":"q.js"}
|
||
});
|
||
|
||
require.define("/node_modules/q/q.js",function(require,module,exports,__dirname,__filename,process,global){// vim:ts=4:sts=4:sw=4:
|
||
/*!
|
||
*
|
||
* Copyright 2009-2012 Kris Kowal under the terms of the MIT
|
||
* license found at http://github.com/kriskowal/q/raw/master/LICENSE
|
||
*
|
||
* With parts by Tyler Close
|
||
* Copyright 2007-2009 Tyler Close under the terms of the MIT X license found
|
||
* at http://www.opensource.org/licenses/mit-license.html
|
||
* Forked at ref_send.js version: 2009-05-11
|
||
*
|
||
* With parts by Mark Miller
|
||
* Copyright (C) 2011 Google Inc.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* 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.
|
||
*
|
||
*/
|
||
|
||
(function (definition) {
|
||
// Turn off strict mode for this function so we can assign to global.Q
|
||
/*jshint strict: false*/
|
||
|
||
// This file will function properly as a <script> tag, or a module
|
||
// using CommonJS and NodeJS or RequireJS module formats. In
|
||
// Common/Node/RequireJS, the module exports the Q API and when
|
||
// executed as a simple <script>, it creates a Q global instead.
|
||
|
||
// Montage Require
|
||
if (typeof bootstrap === "function") {
|
||
bootstrap("promise", definition);
|
||
|
||
// CommonJS
|
||
} else if (typeof exports === "object") {
|
||
definition(void 0, exports);
|
||
|
||
// RequireJS
|
||
} else if (typeof define === "function") {
|
||
define(definition);
|
||
|
||
// SES (Secure EcmaScript)
|
||
} else if (typeof ses !== "undefined") {
|
||
if (!ses.ok()) {
|
||
return;
|
||
} else {
|
||
ses.makeQ = function () {
|
||
var Q = {};
|
||
return definition(void 0, Q);
|
||
};
|
||
}
|
||
|
||
// <script>
|
||
} else {
|
||
definition(void 0, Q = {});
|
||
}
|
||
|
||
})(function (require, exports) {
|
||
"use strict";
|
||
|
||
// All code after this point will be filtered from stack traces reported
|
||
// by Q.
|
||
var qStartingLine = captureLine();
|
||
var qFileName;
|
||
|
||
// shims
|
||
|
||
// used for fallback "defend" and in "allResolved"
|
||
var noop = function () {};
|
||
|
||
// for the security conscious, defend may be a deep freeze as provided
|
||
// by cajaVM. Otherwise we try to provide a shallow freeze just to
|
||
// discourage promise changes that are not compatible with secure
|
||
// usage. If Object.freeze does not exist, fall back to doing nothing
|
||
// (no op).
|
||
var defend = Object.freeze || noop;
|
||
if (typeof cajaVM !== "undefined") {
|
||
defend = cajaVM.def;
|
||
}
|
||
|
||
// use the fastest possible means to execute a task in a future turn
|
||
// of the event loop.
|
||
var nextTick;
|
||
if (typeof process !== "undefined") {
|
||
// node
|
||
nextTick = process.nextTick;
|
||
} else if (typeof setImmediate === "function") {
|
||
// In IE10, or use https://github.com/NobleJS/setImmediate
|
||
nextTick = setImmediate;
|
||
} else if (typeof MessageChannel !== "undefined") {
|
||
// modern browsers
|
||
// http://www.nonblocking.io/2011/06/windownexttick.html
|
||
var channel = new MessageChannel();
|
||
// linked list of tasks (single, with head node)
|
||
var head = {}, tail = head;
|
||
channel.port1.onmessage = function () {
|
||
head = head.next;
|
||
var task = head.task;
|
||
delete head.task;
|
||
task();
|
||
};
|
||
nextTick = function (task) {
|
||
tail = tail.next = {task: task};
|
||
channel.port2.postMessage(0);
|
||
};
|
||
} else {
|
||
// old browsers
|
||
nextTick = function (task) {
|
||
setTimeout(task, 0);
|
||
};
|
||
}
|
||
|
||
// Attempt to make generics safe in the face of downstream
|
||
// modifications.
|
||
// There is no situation where this is necessary.
|
||
// If you need a security guarantee, these primordials need to be
|
||
// deeply frozen anyway, and if you don’t need a security guarantee,
|
||
// this is just plain paranoid.
|
||
// However, this does have the nice side-effect of reducing the size
|
||
// of the code by reducing x.call() to merely x(), eliminating many
|
||
// hard-to-minify characters.
|
||
// See Mark Miller’s explanation of what this does.
|
||
// http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming
|
||
var uncurryThis;
|
||
// I have kept both variations because the first is theoretically
|
||
// faster, if bind is available.
|
||
if (Function.prototype.bind) {
|
||
var Function_bind = Function.prototype.bind;
|
||
uncurryThis = Function_bind.bind(Function_bind.call);
|
||
} else {
|
||
uncurryThis = function (f) {
|
||
return function () {
|
||
return f.call.apply(f, arguments);
|
||
};
|
||
};
|
||
}
|
||
|
||
var array_slice = uncurryThis(Array.prototype.slice);
|
||
|
||
var array_reduce = uncurryThis(
|
||
Array.prototype.reduce || function (callback, basis) {
|
||
var index = 0,
|
||
length = this.length;
|
||
// concerning the initial value, if one is not provided
|
||
if (arguments.length === 1) {
|
||
// seek to the first value in the array, accounting
|
||
// for the possibility that is is a sparse array
|
||
do {
|
||
if (index in this) {
|
||
basis = this[index++];
|
||
break;
|
||
}
|
||
if (++index >= length) {
|
||
throw new TypeError();
|
||
}
|
||
} while (1);
|
||
}
|
||
// reduce
|
||
for (; index < length; index++) {
|
||
// account for the possibility that the array is sparse
|
||
if (index in this) {
|
||
basis = callback(basis, this[index], index);
|
||
}
|
||
}
|
||
return basis;
|
||
}
|
||
);
|
||
|
||
var array_indexOf = uncurryThis(
|
||
Array.prototype.indexOf || function (value) {
|
||
// not a very good shim, but good enough for our one use of it
|
||
for (var i = 0; i < this.length; i++) {
|
||
if (this[i] === value) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
);
|
||
|
||
var array_map = uncurryThis(
|
||
Array.prototype.map || function (callback, thisp) {
|
||
var self = this;
|
||
var collect = [];
|
||
array_reduce(self, function (undefined, value, index) {
|
||
collect.push(callback.call(thisp, value, index, self));
|
||
}, void 0);
|
||
return collect;
|
||
}
|
||
);
|
||
|
||
var object_create = Object.create || function (prototype) {
|
||
function Type() { }
|
||
Type.prototype = prototype;
|
||
return new Type();
|
||
};
|
||
|
||
var object_keys = Object.keys || function (object) {
|
||
var keys = [];
|
||
for (var key in object) {
|
||
keys.push(key);
|
||
}
|
||
return keys;
|
||
};
|
||
|
||
var object_toString = Object.prototype.toString;
|
||
|
||
// generator related shims
|
||
|
||
function isStopIteration(exception) {
|
||
return (
|
||
object_toString(exception) === "[object StopIteration]" ||
|
||
exception instanceof QReturnValue
|
||
);
|
||
}
|
||
|
||
var QReturnValue;
|
||
if (typeof ReturnValue !== "undefined") {
|
||
QReturnValue = ReturnValue;
|
||
} else {
|
||
QReturnValue = function (value) {
|
||
this.value = value;
|
||
};
|
||
}
|
||
|
||
// long stack traces
|
||
|
||
var STACK_JUMP_SEPARATOR = "From previous event:";
|
||
|
||
function makeStackTraceLong(error, promise) {
|
||
// If possible (that is, if in V8), transform the error stack
|
||
// trace by removing Node and Q cruft, then concatenating with
|
||
// the stack trace of the promise we are ``done``ing. See #57.
|
||
if (promise.stack &&
|
||
typeof error === "object" &&
|
||
error !== null &&
|
||
error.stack &&
|
||
error.stack.indexOf(STACK_JUMP_SEPARATOR) === -1
|
||
) {
|
||
error.stack = filterStackString(error.stack) +
|
||
"\n" + STACK_JUMP_SEPARATOR + "\n" +
|
||
filterStackString(promise.stack);
|
||
}
|
||
}
|
||
|
||
function filterStackString(stackString) {
|
||
var lines = stackString.split("\n");
|
||
var desiredLines = [];
|
||
for (var i = 0; i < lines.length; ++i) {
|
||
var line = lines[i];
|
||
|
||
if (!isInternalFrame(line) && !isNodeFrame(line)) {
|
||
desiredLines.push(line);
|
||
}
|
||
}
|
||
return desiredLines.join("\n");
|
||
}
|
||
|
||
function isNodeFrame(stackLine) {
|
||
return stackLine.indexOf("(module.js:") !== -1 ||
|
||
stackLine.indexOf("(node.js:") !== -1;
|
||
}
|
||
|
||
function isInternalFrame(stackLine) {
|
||
var pieces = /at .+ \((.*):(\d+):\d+\)/.exec(stackLine);
|
||
|
||
if (!pieces) {
|
||
return false;
|
||
}
|
||
|
||
var fileName = pieces[1];
|
||
var lineNumber = pieces[2];
|
||
|
||
return fileName === qFileName &&
|
||
lineNumber >= qStartingLine &&
|
||
lineNumber <= qEndingLine;
|
||
}
|
||
|
||
// discover own file name and line number range for filtering stack
|
||
// traces
|
||
function captureLine() {
|
||
if (Error.captureStackTrace) {
|
||
var fileName, lineNumber;
|
||
|
||
var oldPrepareStackTrace = Error.prepareStackTrace;
|
||
|
||
Error.prepareStackTrace = function (error, frames) {
|
||
fileName = frames[1].getFileName();
|
||
lineNumber = frames[1].getLineNumber();
|
||
};
|
||
|
||
// teases call of temporary prepareStackTrace
|
||
// JSHint and Closure Compiler generate known warnings here
|
||
/*jshint expr: true */
|
||
new Error().stack;
|
||
|
||
Error.prepareStackTrace = oldPrepareStackTrace;
|
||
qFileName = fileName;
|
||
return lineNumber;
|
||
}
|
||
}
|
||
|
||
function deprecate(callback, name, alternative) {
|
||
return function () {
|
||
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
||
console.warn(name + " is deprecated, use " + alternative + " instead.", new Error("").stack);
|
||
}
|
||
return callback.apply(callback, arguments);
|
||
};
|
||
}
|
||
|
||
// end of shims
|
||
// beginning of real work
|
||
|
||
/**
|
||
* Performs a task in a future turn of the event loop.
|
||
* @param {Function} task
|
||
*/
|
||
exports.nextTick = nextTick;
|
||
|
||
/**
|
||
* Constructs a {promise, resolve} object.
|
||
*
|
||
* The resolver is a callback to invoke with a more resolved value for the
|
||
* promise. To fulfill the promise, invoke the resolver with any value that is
|
||
* not a function. To reject the promise, invoke the resolver with a rejection
|
||
* object. To put the promise in the same state as another promise, invoke the
|
||
* resolver with that other promise.
|
||
*/
|
||
exports.defer = defer;
|
||
function defer() {
|
||
// if "pending" is an "Array", that indicates that the promise has not yet
|
||
// been resolved. If it is "undefined", it has been resolved. Each
|
||
// element of the pending array is itself an array of complete arguments to
|
||
// forward to the resolved promise. We coerce the resolution value to a
|
||
// promise using the ref promise because it handles both fully
|
||
// resolved values and other promises gracefully.
|
||
var pending = [], progressListeners = [], value;
|
||
|
||
var deferred = object_create(defer.prototype);
|
||
var promise = object_create(makePromise.prototype);
|
||
|
||
promise.promiseSend = function (op, _, __, progress) {
|
||
var args = array_slice(arguments);
|
||
if (pending) {
|
||
pending.push(args);
|
||
if (op === "when" && progress) {
|
||
progressListeners.push(progress);
|
||
}
|
||
} else {
|
||
nextTick(function () {
|
||
value.promiseSend.apply(value, args);
|
||
});
|
||
}
|
||
};
|
||
|
||
promise.valueOf = function () {
|
||
if (pending) {
|
||
return promise;
|
||
}
|
||
return value.valueOf();
|
||
};
|
||
|
||
if (Error.captureStackTrace) {
|
||
Error.captureStackTrace(promise, defer);
|
||
|
||
// Reify the stack into a string by using the accessor; this prevents
|
||
// memory leaks as per GH-111. At the same time, cut off the first line;
|
||
// it's always just "[object Promise]\n", as per the `toString`.
|
||
promise.stack = promise.stack.substring(promise.stack.indexOf("\n") + 1);
|
||
}
|
||
|
||
function become(resolvedValue) {
|
||
if (!pending) {
|
||
return;
|
||
}
|
||
value = resolve(resolvedValue);
|
||
array_reduce(pending, function (undefined, pending) {
|
||
nextTick(function () {
|
||
value.promiseSend.apply(value, pending);
|
||
});
|
||
}, void 0);
|
||
pending = void 0;
|
||
progressListeners = void 0;
|
||
}
|
||
|
||
defend(promise);
|
||
|
||
deferred.promise = promise;
|
||
deferred.resolve = become;
|
||
deferred.reject = function (exception) {
|
||
become(reject(exception));
|
||
};
|
||
deferred.notify = function (progress) {
|
||
if (pending) {
|
||
array_reduce(progressListeners, function (undefined, progressListener) {
|
||
nextTick(function () {
|
||
progressListener(progress);
|
||
});
|
||
}, void 0);
|
||
}
|
||
};
|
||
|
||
return deferred;
|
||
}
|
||
|
||
/**
|
||
* Creates a Node-style callback that will resolve or reject the deferred
|
||
* promise.
|
||
* @returns a nodeback
|
||
*/
|
||
defer.prototype.makeNodeResolver = function () {
|
||
var self = this;
|
||
return function (error, value) {
|
||
if (error) {
|
||
self.reject(error);
|
||
} else if (arguments.length > 2) {
|
||
self.resolve(array_slice(arguments, 1));
|
||
} else {
|
||
self.resolve(value);
|
||
}
|
||
};
|
||
};
|
||
// XXX deprecated
|
||
defer.prototype.node = deprecate(defer.prototype.makeNodeResolver, "node", "makeNodeResolver");
|
||
|
||
/**
|
||
* @param makePromise {Function} a function that returns nothing and accepts
|
||
* the resolve, reject, and notify functions for a deferred.
|
||
* @returns a promise that may be resolved with the given resolve and reject
|
||
* functions, or rejected by a thrown exception in makePromise
|
||
*/
|
||
exports.promise = promise;
|
||
function promise(makePromise) {
|
||
var deferred = defer();
|
||
fcall(
|
||
makePromise,
|
||
deferred.resolve,
|
||
deferred.reject,
|
||
deferred.notify
|
||
).fail(deferred.reject);
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Constructs a Promise with a promise descriptor object and optional fallback
|
||
* function. The descriptor contains methods like when(rejected), get(name),
|
||
* put(name, value), post(name, args), and delete(name), which all
|
||
* return either a value, a promise for a value, or a rejection. The fallback
|
||
* accepts the operation name, a resolver, and any further arguments that would
|
||
* have been forwarded to the appropriate method above had a method been
|
||
* provided with the proper name. The API makes no guarantees about the nature
|
||
* of the returned object, apart from that it is usable whereever promises are
|
||
* bought and sold.
|
||
*/
|
||
exports.makePromise = makePromise;
|
||
function makePromise(descriptor, fallback, valueOf, exception) {
|
||
if (fallback === void 0) {
|
||
fallback = function (op) {
|
||
return reject(new Error("Promise does not support operation: " + op));
|
||
};
|
||
}
|
||
|
||
var promise = object_create(makePromise.prototype);
|
||
|
||
promise.promiseSend = function (op, resolved /* ...args */) {
|
||
var args = array_slice(arguments, 2);
|
||
var result;
|
||
try {
|
||
if (descriptor[op]) {
|
||
result = descriptor[op].apply(promise, args);
|
||
} else {
|
||
result = fallback.apply(promise, [op].concat(args));
|
||
}
|
||
} catch (exception) {
|
||
result = reject(exception);
|
||
}
|
||
if (resolved) {
|
||
resolved(result);
|
||
}
|
||
};
|
||
|
||
if (valueOf) {
|
||
promise.valueOf = valueOf;
|
||
}
|
||
|
||
if (exception) {
|
||
promise.exception = exception;
|
||
}
|
||
|
||
defend(promise);
|
||
|
||
return promise;
|
||
}
|
||
|
||
// provide thenables, CommonJS/Promises/A
|
||
makePromise.prototype.then = function (fulfilled, rejected, progressed) {
|
||
return when(this, fulfilled, rejected, progressed);
|
||
};
|
||
|
||
makePromise.prototype.thenResolve = function (value) {
|
||
return when(this, function () { return value; });
|
||
};
|
||
|
||
// Chainable methods
|
||
array_reduce(
|
||
[
|
||
"isResolved", "isFulfilled", "isRejected",
|
||
"when", "spread", "send",
|
||
"get", "put", "del",
|
||
"post", "invoke",
|
||
"keys",
|
||
"apply", "call", "bind",
|
||
"fapply", "fcall", "fbind",
|
||
"all", "allResolved",
|
||
"view", "viewInfo",
|
||
"timeout", "delay",
|
||
"catch", "finally", "fail", "fin", "progress", "end", "done",
|
||
"nfcall", "nfapply", "nfbind",
|
||
"ncall", "napply", "nbind",
|
||
"npost", "ninvoke",
|
||
"nend", "nodeify"
|
||
],
|
||
function (undefined, name) {
|
||
makePromise.prototype[name] = function () {
|
||
return exports[name].apply(
|
||
exports,
|
||
[this].concat(array_slice(arguments))
|
||
);
|
||
};
|
||
},
|
||
void 0
|
||
);
|
||
|
||
makePromise.prototype.toSource = function () {
|
||
return this.toString();
|
||
};
|
||
|
||
makePromise.prototype.toString = function () {
|
||
return "[object Promise]";
|
||
};
|
||
|
||
defend(makePromise.prototype);
|
||
|
||
/**
|
||
* If an object is not a promise, it is as "near" as possible.
|
||
* If a promise is rejected, it is as "near" as possible too.
|
||
* If it’s a fulfilled promise, the fulfillment value is nearer.
|
||
* If it’s a deferred promise and the deferred has been resolved, the
|
||
* resolution is "nearer".
|
||
* @param object
|
||
* @returns most resolved (nearest) form of the object
|
||
*/
|
||
exports.nearer = valueOf;
|
||
function valueOf(value) {
|
||
if (isPromise(value)) {
|
||
return value.valueOf();
|
||
}
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* @returns whether the given object is a promise.
|
||
* Otherwise it is a fulfilled value.
|
||
*/
|
||
exports.isPromise = isPromise;
|
||
function isPromise(object) {
|
||
return object && typeof object.promiseSend === "function";
|
||
}
|
||
|
||
/**
|
||
* @returns whether the given object is a resolved promise.
|
||
*/
|
||
exports.isResolved = isResolved;
|
||
function isResolved(object) {
|
||
return isFulfilled(object) || isRejected(object);
|
||
}
|
||
|
||
/**
|
||
* @returns whether the given object is a value or fulfilled
|
||
* promise.
|
||
*/
|
||
exports.isFulfilled = isFulfilled;
|
||
function isFulfilled(object) {
|
||
return !isPromise(valueOf(object));
|
||
}
|
||
|
||
/**
|
||
* @returns whether the given object is a rejected promise.
|
||
*/
|
||
exports.isRejected = isRejected;
|
||
function isRejected(object) {
|
||
object = valueOf(object);
|
||
return isPromise(object) && 'exception' in object;
|
||
}
|
||
|
||
var rejections = [];
|
||
var errors = [];
|
||
var errorsDisplayed;
|
||
function displayErrors() {
|
||
if (
|
||
!errorsDisplayed &&
|
||
typeof window !== "undefined" &&
|
||
!window.Touch &&
|
||
window.console
|
||
) {
|
||
// This promise library consumes exceptions thrown in handlers so
|
||
// they can be handled by a subsequent promise. The rejected
|
||
// promises get added to this array when they are created, and
|
||
// removed when they are handled.
|
||
console.log("Should be empty:", errors);
|
||
}
|
||
errorsDisplayed = true;
|
||
}
|
||
|
||
/**
|
||
* Constructs a rejected promise.
|
||
* @param exception value describing the failure
|
||
*/
|
||
exports.reject = reject;
|
||
function reject(exception) {
|
||
exception = exception || new Error();
|
||
var rejection = makePromise({
|
||
"when": function (rejected) {
|
||
// note that the error has been handled
|
||
if (rejected) {
|
||
var at = array_indexOf(rejections, this);
|
||
if (at !== -1) {
|
||
errors.splice(at, 1);
|
||
rejections.splice(at, 1);
|
||
}
|
||
}
|
||
return rejected ? rejected(exception) : reject(exception);
|
||
}
|
||
}, function fallback() {
|
||
return reject(exception);
|
||
}, function valueOf() {
|
||
return this;
|
||
}, exception);
|
||
// note that the error has not been handled
|
||
displayErrors();
|
||
rejections.push(rejection);
|
||
errors.push(exception);
|
||
return rejection;
|
||
}
|
||
|
||
/**
|
||
* Constructs a promise for an immediate reference.
|
||
* @param value immediate reference
|
||
*/
|
||
exports.begin = resolve; // XXX experimental
|
||
exports.resolve = resolve;
|
||
exports.ref = deprecate(resolve, "ref", "resolve"); // XXX deprecated, use resolve
|
||
function resolve(object) {
|
||
// If the object is already a Promise, return it directly. This enables
|
||
// the resolve function to both be used to created references from objects,
|
||
// but to tolerably coerce non-promises to promises.
|
||
if (isPromise(object)) {
|
||
return object;
|
||
}
|
||
// In order to break infinite recursion or loops between `then` and
|
||
// `resolve`, it is necessary to attempt to extract fulfilled values
|
||
// out of foreign promise implementations before attempting to wrap
|
||
// them as unresolved promises. It is my hope that other
|
||
// implementations will implement `valueOf` to synchronously extract
|
||
// the fulfillment value from their fulfilled promises. If the
|
||
// other promise library does not implement `valueOf`, the
|
||
// implementations on primordial prototypes are harmless.
|
||
object = valueOf(object);
|
||
// assimilate thenables, CommonJS/Promises/A
|
||
if (object && typeof object.then === "function") {
|
||
var deferred = defer();
|
||
object.then(deferred.resolve, deferred.reject, deferred.notify);
|
||
return deferred.promise;
|
||
}
|
||
return makePromise({
|
||
"when": function () {
|
||
return object;
|
||
},
|
||
"get": function (name) {
|
||
return object[name];
|
||
},
|
||
"put": function (name, value) {
|
||
object[name] = value;
|
||
return object;
|
||
},
|
||
"del": function (name) {
|
||
delete object[name];
|
||
return object;
|
||
},
|
||
"post": function (name, value) {
|
||
return object[name].apply(object, value);
|
||
},
|
||
"apply": function (self, args) {
|
||
return object.apply(self, args);
|
||
},
|
||
"fapply": function (args) {
|
||
return object.apply(void 0, args);
|
||
},
|
||
"viewInfo": function () {
|
||
var on = object;
|
||
var properties = {};
|
||
|
||
function fixFalsyProperty(name) {
|
||
if (!properties[name]) {
|
||
properties[name] = typeof on[name];
|
||
}
|
||
}
|
||
|
||
while (on) {
|
||
Object.getOwnPropertyNames(on).forEach(fixFalsyProperty);
|
||
on = Object.getPrototypeOf(on);
|
||
}
|
||
return {
|
||
"type": typeof object,
|
||
"properties": properties
|
||
};
|
||
},
|
||
"keys": function () {
|
||
return object_keys(object);
|
||
}
|
||
}, void 0, function valueOf() {
|
||
return object;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Annotates an object such that it will never be
|
||
* transferred away from this process over any promise
|
||
* communication channel.
|
||
* @param object
|
||
* @returns promise a wrapping of that object that
|
||
* additionally responds to the "isDef" message
|
||
* without a rejection.
|
||
*/
|
||
exports.master = master;
|
||
function master(object) {
|
||
return makePromise({
|
||
"isDef": function () {}
|
||
}, function fallback() {
|
||
var args = array_slice(arguments);
|
||
return send.apply(void 0, [object].concat(args));
|
||
}, function () {
|
||
return valueOf(object);
|
||
});
|
||
}
|
||
|
||
exports.viewInfo = viewInfo;
|
||
function viewInfo(object, info) {
|
||
object = resolve(object);
|
||
if (info) {
|
||
return makePromise({
|
||
"viewInfo": function () {
|
||
return info;
|
||
}
|
||
}, function fallback() {
|
||
var args = array_slice(arguments);
|
||
return send.apply(void 0, [object].concat(args));
|
||
}, function () {
|
||
return valueOf(object);
|
||
});
|
||
} else {
|
||
return send(object, "viewInfo");
|
||
}
|
||
}
|
||
|
||
exports.view = view;
|
||
function view(object) {
|
||
return viewInfo(object).when(function (info) {
|
||
var view;
|
||
if (info.type === "function") {
|
||
view = function () {
|
||
return apply(object, void 0, arguments);
|
||
};
|
||
} else {
|
||
view = {};
|
||
}
|
||
var properties = info.properties || {};
|
||
object_keys(properties).forEach(function (name) {
|
||
if (properties[name] === "function") {
|
||
view[name] = function () {
|
||
return post(object, name, arguments);
|
||
};
|
||
}
|
||
});
|
||
return resolve(view);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Registers an observer on a promise.
|
||
*
|
||
* Guarantees:
|
||
*
|
||
* 1. that fulfilled and rejected will be called only once.
|
||
* 2. that either the fulfilled callback or the rejected callback will be
|
||
* called, but not both.
|
||
* 3. that fulfilled and rejected will not be called in this turn.
|
||
*
|
||
* @param value promise or immediate reference to observe
|
||
* @param fulfilled function to be called with the fulfilled value
|
||
* @param rejected function to be called with the rejection exception
|
||
* @param progressed function to be called on any progress notifications
|
||
* @return promise for the return value from the invoked callback
|
||
*/
|
||
exports.when = when;
|
||
function when(value, fulfilled, rejected, progressed) {
|
||
var deferred = defer();
|
||
var done = false; // ensure the untrusted promise makes at most a
|
||
// single call to one of the callbacks
|
||
|
||
function _fulfilled(value) {
|
||
try {
|
||
return fulfilled ? fulfilled(value) : value;
|
||
} catch (exception) {
|
||
return reject(exception);
|
||
}
|
||
}
|
||
|
||
function _rejected(exception) {
|
||
if (rejected) {
|
||
makeStackTraceLong(exception, resolvedValue);
|
||
try {
|
||
return rejected(exception);
|
||
} catch (newException) {
|
||
return reject(newException);
|
||
}
|
||
}
|
||
return reject(exception);
|
||
}
|
||
|
||
function _progressed(value) {
|
||
return progressed ? progressed(value) : value;
|
||
}
|
||
|
||
var resolvedValue = resolve(value);
|
||
nextTick(function () {
|
||
resolvedValue.promiseSend("when", function (value) {
|
||
if (done) {
|
||
return;
|
||
}
|
||
done = true;
|
||
|
||
deferred.resolve(_fulfilled(value));
|
||
}, function (exception) {
|
||
if (done) {
|
||
return;
|
||
}
|
||
done = true;
|
||
|
||
deferred.resolve(_rejected(exception));
|
||
});
|
||
});
|
||
|
||
// Progress propagator need to be attached in the current tick.
|
||
resolvedValue.promiseSend("when", void 0, void 0, function (value) {
|
||
deferred.notify(_progressed(value));
|
||
});
|
||
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Spreads the values of a promised array of arguments into the
|
||
* fulfillment callback.
|
||
* @param fulfilled callback that receives variadic arguments from the
|
||
* promised array
|
||
* @param rejected callback that receives the exception if the promise
|
||
* is rejected.
|
||
* @returns a promise for the return value or thrown exception of
|
||
* either callback.
|
||
*/
|
||
exports.spread = spread;
|
||
function spread(promise, fulfilled, rejected) {
|
||
return when(promise, function (valuesOrPromises) {
|
||
return all(valuesOrPromises).then(function (values) {
|
||
return fulfilled.apply(void 0, values);
|
||
}, rejected);
|
||
}, rejected);
|
||
}
|
||
|
||
/**
|
||
* The async function is a decorator for generator functions, turning
|
||
* them into asynchronous generators. This presently only works in
|
||
* Firefox/Spidermonkey, however, this code does not cause syntax
|
||
* errors in older engines. This code should continue to work and
|
||
* will in fact improve over time as the language improves.
|
||
*
|
||
* Decorates a generator function such that:
|
||
* - it may yield promises
|
||
* - execution will continue when that promise is fulfilled
|
||
* - the value of the yield expression will be the fulfilled value
|
||
* - it returns a promise for the return value (when the generator
|
||
* stops iterating)
|
||
* - the decorated function returns a promise for the return value
|
||
* of the generator or the first rejected promise among those
|
||
* yielded.
|
||
* - if an error is thrown in the generator, it propagates through
|
||
* every following yield until it is caught, or until it escapes
|
||
* the generator function altogether, and is translated into a
|
||
* rejection for the promise returned by the decorated generator.
|
||
* - in present implementations of generators, when a generator
|
||
* function is complete, it throws ``StopIteration``, ``return`` is
|
||
* a syntax error in the presence of ``yield``, so there is no
|
||
* observable return value. There is a proposal[1] to add support
|
||
* for ``return``, which would permit the value to be carried by a
|
||
* ``StopIteration`` instance, in which case it would fulfill the
|
||
* promise returned by the asynchronous generator. This can be
|
||
* emulated today by throwing StopIteration explicitly with a value
|
||
* property.
|
||
*
|
||
* [1]: http://wiki.ecmascript.org/doku.php?id=strawman:async_functions#reference_implementation
|
||
*
|
||
*/
|
||
exports.async = async;
|
||
function async(makeGenerator) {
|
||
return function () {
|
||
// when verb is "send", arg is a value
|
||
// when verb is "throw", arg is an exception
|
||
function continuer(verb, arg) {
|
||
var result;
|
||
try {
|
||
result = generator[verb](arg);
|
||
} catch (exception) {
|
||
if (isStopIteration(exception)) {
|
||
return exception.value;
|
||
} else {
|
||
return reject(exception);
|
||
}
|
||
}
|
||
return when(result, callback, errback);
|
||
}
|
||
var generator = makeGenerator.apply(this, arguments);
|
||
var callback = continuer.bind(continuer, "send");
|
||
var errback = continuer.bind(continuer, "throw");
|
||
return callback();
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Throws a ReturnValue exception to stop an asynchronous generator.
|
||
* Only useful presently in Firefox/SpiderMonkey since generators are
|
||
* implemented.
|
||
* @param value the return value for the surrounding generator
|
||
* @throws ReturnValue exception with the value.
|
||
* @example
|
||
* Q.async(function () {
|
||
* var foo = yield getFooPromise();
|
||
* var bar = yield getBarPromise();
|
||
* Q.return(foo + bar);
|
||
* })
|
||
*/
|
||
exports['return'] = _return;
|
||
function _return(value) {
|
||
throw new QReturnValue(value);
|
||
}
|
||
|
||
/**
|
||
* The promised function decorator ensures that any promise arguments
|
||
* are resolved and passed as values (`this` is also resolved and passed
|
||
* as a value). It will also ensure that the result of a function is
|
||
* always a promise.
|
||
*
|
||
* @example
|
||
* var add = Q.promised(function (a, b) {
|
||
* return a + b;
|
||
* });
|
||
* add(Q.resolve(a), Q.resolve(B));
|
||
*
|
||
* @param {function} callback The function to decorate
|
||
* @returns {function} a function that has been decorated.
|
||
*/
|
||
exports.promised = promised;
|
||
function promised(callback) {
|
||
return function () {
|
||
return all([this, all(arguments)]).spread(function (self, args) {
|
||
return callback.apply(self, args);
|
||
});
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Constructs a promise method that can be used to safely observe resolution of
|
||
* a promise for an arbitrarily named method like "propfind" in a future turn.
|
||
*/
|
||
exports.sender = deprecate(sender, "sender", "dispatcher"); // XXX deprecated, use dispatcher
|
||
exports.Method = deprecate(sender, "Method", "dispatcher"); // XXX deprecated, use dispatcher
|
||
function sender(op) {
|
||
return function (object) {
|
||
var args = array_slice(arguments, 1);
|
||
return send.apply(void 0, [object, op].concat(args));
|
||
};
|
||
}
|
||
|
||
/**
|
||
* sends a message to a value in a future turn
|
||
* @param object* the recipient
|
||
* @param op the name of the message operation, e.g., "when",
|
||
* @param ...args further arguments to be forwarded to the operation
|
||
* @returns result {Promise} a promise for the result of the operation
|
||
*/
|
||
exports.send = deprecate(send, "send", "dispatch"); // XXX deprecated, use dispatch
|
||
function send(object, op) {
|
||
var deferred = defer();
|
||
var args = array_slice(arguments, 2);
|
||
object = resolve(object);
|
||
nextTick(function () {
|
||
object.promiseSend.apply(
|
||
object,
|
||
[op, deferred.resolve].concat(args)
|
||
);
|
||
});
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* sends a message to a value in a future turn
|
||
* @param object* the recipient
|
||
* @param op the name of the message operation, e.g., "when",
|
||
* @param args further arguments to be forwarded to the operation
|
||
* @returns result {Promise} a promise for the result of the operation
|
||
*/
|
||
exports.dispatch = dispatch;
|
||
function dispatch(object, op, args) {
|
||
var deferred = defer();
|
||
object = resolve(object);
|
||
nextTick(function () {
|
||
object.promiseSend.apply(
|
||
object,
|
||
[op, deferred.resolve].concat(args)
|
||
);
|
||
});
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Constructs a promise method that can be used to safely observe resolution of
|
||
* a promise for an arbitrarily named method like "propfind" in a future turn.
|
||
*
|
||
* "dispatcher" constructs methods like "get(promise, name)" and "put(promise)".
|
||
*/
|
||
exports.dispatcher = dispatcher;
|
||
function dispatcher(op) {
|
||
return function (object) {
|
||
var args = array_slice(arguments, 1);
|
||
return dispatch(object, op, args);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Gets the value of a property in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @param name name of property to get
|
||
* @return promise for the property value
|
||
*/
|
||
exports.get = dispatcher("get");
|
||
|
||
/**
|
||
* Sets the value of a property in a future turn.
|
||
* @param object promise or immediate reference for object object
|
||
* @param name name of property to set
|
||
* @param value new value of property
|
||
* @return promise for the return value
|
||
*/
|
||
exports.put = dispatcher("put");
|
||
|
||
/**
|
||
* Deletes a property in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @param name name of property to delete
|
||
* @return promise for the return value
|
||
*/
|
||
exports["delete"] = // XXX experimental
|
||
exports.del = dispatcher("del");
|
||
|
||
/**
|
||
* Invokes a method in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @param name name of method to invoke
|
||
* @param value a value to post, typically an array of
|
||
* invocation arguments for promises that
|
||
* are ultimately backed with `resolve` values,
|
||
* as opposed to those backed with URLs
|
||
* wherein the posted value can be any
|
||
* JSON serializable object.
|
||
* @return promise for the return value
|
||
*/
|
||
// bound locally because it is used by other methods
|
||
var post = exports.post = dispatcher("post");
|
||
|
||
/**
|
||
* Invokes a method in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @param name name of method to invoke
|
||
* @param ...args array of invocation arguments
|
||
* @return promise for the return value
|
||
*/
|
||
exports.invoke = function (value, name) {
|
||
var args = array_slice(arguments, 2);
|
||
return post(value, name, args);
|
||
};
|
||
|
||
/**
|
||
* Applies the promised function in a future turn.
|
||
* @param object promise or immediate reference for target function
|
||
* @param thisp the `this` object for the call
|
||
* @param args array of application arguments
|
||
*/
|
||
// XXX deprecated, use fapply
|
||
var apply = exports.apply = deprecate(dispatcher("apply"), "apply", "fapply");
|
||
|
||
/**
|
||
* Applies the promised function in a future turn.
|
||
* @param object promise or immediate reference for target function
|
||
* @param args array of application arguments
|
||
*/
|
||
var fapply = exports.fapply = dispatcher("fapply");
|
||
|
||
/**
|
||
* Calls the promised function in a future turn.
|
||
* @param object promise or immediate reference for target function
|
||
* @param thisp the `this` object for the call
|
||
* @param ...args array of application arguments
|
||
*/
|
||
// XXX deprecated, use fcall
|
||
exports.call = deprecate(call, "call", "fcall");
|
||
function call(value, thisp) {
|
||
var args = array_slice(arguments, 2);
|
||
return apply(value, thisp, args);
|
||
}
|
||
|
||
/**
|
||
* Calls the promised function in a future turn.
|
||
* @param object promise or immediate reference for target function
|
||
* @param ...args array of application arguments
|
||
*/
|
||
exports["try"] = fcall; // XXX experimental
|
||
exports.fcall = fcall;
|
||
function fcall(value) {
|
||
var args = array_slice(arguments, 1);
|
||
return fapply(value, args);
|
||
}
|
||
|
||
/**
|
||
* Binds the promised function, transforming return values into a fulfilled
|
||
* promise and thrown errors into a rejected one.
|
||
* @param object promise or immediate reference for target function
|
||
* @param thisp the `this` object for the call
|
||
* @param ...args array of application arguments
|
||
*/
|
||
exports.bind = deprecate(bind, "bind", "fbind"); // XXX deprecated, use fbind
|
||
function bind(value, thisp) {
|
||
var args = array_slice(arguments, 2);
|
||
return function bound() {
|
||
var allArgs = args.concat(array_slice(arguments));
|
||
return apply(value, thisp, allArgs);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Binds the promised function, transforming return values into a fulfilled
|
||
* promise and thrown errors into a rejected one.
|
||
* @param object promise or immediate reference for target function
|
||
* @param ...args array of application arguments
|
||
*/
|
||
exports.fbind = fbind;
|
||
function fbind(value) {
|
||
var args = array_slice(arguments, 1);
|
||
return function fbound() {
|
||
var allArgs = args.concat(array_slice(arguments));
|
||
return fapply(value, allArgs);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Requests the names of the owned properties of a promised
|
||
* object in a future turn.
|
||
* @param object promise or immediate reference for target object
|
||
* @return promise for the keys of the eventually resolved object
|
||
*/
|
||
exports.keys = dispatcher("keys");
|
||
|
||
/**
|
||
* Turns an array of promises into a promise for an array. If any of
|
||
* the promises gets rejected, the whole array is rejected immediately.
|
||
* @param {Array*} an array (or promise for an array) of values (or
|
||
* promises for values)
|
||
* @returns a promise for an array of the corresponding values
|
||
*/
|
||
// By Mark Miller
|
||
// http://wiki.ecmascript.org/doku.php?id=strawman:concurrency&rev=1308776521#allfulfilled
|
||
exports.all = all;
|
||
function all(promises) {
|
||
return when(promises, function (promises) {
|
||
var countDown = promises.length;
|
||
if (countDown === 0) {
|
||
return resolve(promises);
|
||
}
|
||
var deferred = defer();
|
||
array_reduce(promises, function (undefined, promise, index) {
|
||
if (isFulfilled(promise)) {
|
||
promises[index] = valueOf(promise);
|
||
if (--countDown === 0) {
|
||
deferred.resolve(promises);
|
||
}
|
||
} else {
|
||
when(promise, function (value) {
|
||
promises[index] = value;
|
||
if (--countDown === 0) {
|
||
deferred.resolve(promises);
|
||
}
|
||
})
|
||
.fail(deferred.reject);
|
||
}
|
||
}, void 0);
|
||
return deferred.promise;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Waits for all promises to be resolved, either fulfilled or
|
||
* rejected. This is distinct from `all` since that would stop
|
||
* waiting at the first rejection. The promise returned by
|
||
* `allResolved` will never be rejected.
|
||
* @param promises a promise for an array (or an array) of promises
|
||
* (or values)
|
||
* @return a promise for an array of promises
|
||
*/
|
||
exports.allResolved = allResolved;
|
||
function allResolved(promises) {
|
||
return when(promises, function (promises) {
|
||
return when(all(array_map(promises, function (promise) {
|
||
return when(promise, noop, noop);
|
||
})), function () {
|
||
return array_map(promises, resolve);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Captures the failure of a promise, giving an oportunity to recover
|
||
* with a callback. If the given promise is fulfilled, the returned
|
||
* promise is fulfilled.
|
||
* @param {Any*} promise for something
|
||
* @param {Function} callback to fulfill the returned promise if the
|
||
* given promise is rejected
|
||
* @returns a promise for the return value of the callback
|
||
*/
|
||
exports["catch"] = // XXX experimental
|
||
exports.fail = fail;
|
||
function fail(promise, rejected) {
|
||
return when(promise, void 0, rejected);
|
||
}
|
||
|
||
/**
|
||
* Attaches a listener that can respond to progress notifications from a
|
||
* promise's originating deferred. This listener receives the exact arguments
|
||
* passed to ``deferred.notify``.
|
||
* @param {Any*} promise for something
|
||
* @param {Function} callback to receive any progress notifications
|
||
* @returns the given promise, unchanged
|
||
*/
|
||
exports.progress = progress;
|
||
function progress(promise, progressed) {
|
||
return when(promise, void 0, void 0, progressed);
|
||
}
|
||
|
||
/**
|
||
* Provides an opportunity to observe the rejection of a promise,
|
||
* regardless of whether the promise is fulfilled or rejected. Forwards
|
||
* the resolution to the returned promise when the callback is done.
|
||
* The callback can return a promise to defer completion.
|
||
* @param {Any*} promise
|
||
* @param {Function} callback to observe the resolution of the given
|
||
* promise, takes no arguments.
|
||
* @returns a promise for the resolution of the given promise when
|
||
* ``fin`` is done.
|
||
*/
|
||
exports["finally"] = // XXX experimental
|
||
exports.fin = fin;
|
||
function fin(promise, callback) {
|
||
return when(promise, function (value) {
|
||
return when(callback(), function () {
|
||
return value;
|
||
});
|
||
}, function (exception) {
|
||
return when(callback(), function () {
|
||
return reject(exception);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Terminates a chain of promises, forcing rejections to be
|
||
* thrown as exceptions.
|
||
* @param {Any*} promise at the end of a chain of promises
|
||
* @returns nothing
|
||
*/
|
||
exports.end = deprecate(done, "end", "done"); // XXX deprecated, use done
|
||
exports.done = done;
|
||
function done(promise, fulfilled, rejected, progress) {
|
||
function onUnhandledError(error) {
|
||
// forward to a future turn so that ``when``
|
||
// does not catch it and turn it into a rejection.
|
||
nextTick(function () {
|
||
makeStackTraceLong(error, promise);
|
||
|
||
if (exports.onerror) {
|
||
exports.onerror(error);
|
||
} else {
|
||
throw error;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Avoid unnecessary `nextTick`ing via an unnecessary `when`.
|
||
var promiseToHandle = fulfilled || rejected || progress ?
|
||
when(promise, fulfilled, rejected, progress) :
|
||
promise;
|
||
|
||
fail(promiseToHandle, onUnhandledError);
|
||
}
|
||
|
||
/**
|
||
* Causes a promise to be rejected if it does not get fulfilled before
|
||
* some milliseconds time out.
|
||
* @param {Any*} promise
|
||
* @param {Number} milliseconds timeout
|
||
* @returns a promise for the resolution of the given promise if it is
|
||
* fulfilled before the timeout, otherwise rejected.
|
||
*/
|
||
exports.timeout = timeout;
|
||
function timeout(promise, ms) {
|
||
var deferred = defer();
|
||
var timeoutId = setTimeout(function () {
|
||
deferred.reject(new Error("Timed out after " + ms + " ms"));
|
||
}, ms);
|
||
|
||
when(promise, function (value) {
|
||
clearTimeout(timeoutId);
|
||
deferred.resolve(value);
|
||
}, function (exception) {
|
||
clearTimeout(timeoutId);
|
||
deferred.reject(exception);
|
||
});
|
||
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Returns a promise for the given value (or promised value) after some
|
||
* milliseconds.
|
||
* @param {Any*} promise
|
||
* @param {Number} milliseconds
|
||
* @returns a promise for the resolution of the given promise after some
|
||
* time has elapsed.
|
||
*/
|
||
exports.delay = delay;
|
||
function delay(promise, timeout) {
|
||
if (timeout === void 0) {
|
||
timeout = promise;
|
||
promise = void 0;
|
||
}
|
||
var deferred = defer();
|
||
setTimeout(function () {
|
||
deferred.resolve(promise);
|
||
}, timeout);
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Passes a continuation to a Node function, which is called with the given
|
||
* arguments provided as an array, and returns a promise.
|
||
*
|
||
* var readFile = require("fs").readFile;
|
||
* Q.nfapply(readFile, [__filename])
|
||
* .then(function (content) {
|
||
* })
|
||
*
|
||
*/
|
||
exports.nfapply = nfapply;
|
||
function nfapply(callback, args) {
|
||
var nodeArgs = array_slice(args);
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
|
||
fapply(callback, nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Passes a continuation to a Node function, which is called with the given
|
||
* arguments provided individually, and returns a promise.
|
||
*
|
||
* var readFile = require("fs").readFile;
|
||
* Q.nfcall(readFile, __filename)
|
||
* .then(function (content) {
|
||
* })
|
||
*
|
||
*/
|
||
exports.nfcall = nfcall;
|
||
function nfcall(callback/*, ...args */) {
|
||
var nodeArgs = array_slice(arguments, 1);
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
|
||
fapply(callback, nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Wraps a NodeJS continuation passing function and returns an equivalent
|
||
* version that returns a promise.
|
||
*
|
||
* Q.nfbind(FS.readFile, __filename)("utf-8")
|
||
* .then(console.log)
|
||
* .done()
|
||
*
|
||
*/
|
||
exports.nfbind = nfbind;
|
||
function nfbind(callback/*, ...args */) {
|
||
var baseArgs = array_slice(arguments, 1);
|
||
return function () {
|
||
var nodeArgs = baseArgs.concat(array_slice(arguments));
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
|
||
fapply(callback, nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Passes a continuation to a Node function, which is called with a given
|
||
* `this` value and arguments provided as an array, and returns a promise.
|
||
*
|
||
* var FS = (require)("fs");
|
||
* Q.napply(FS.readFile, FS, [__filename])
|
||
* .then(function (content) {
|
||
* })
|
||
*
|
||
*/
|
||
exports.napply = deprecate(napply, "napply", "npost");
|
||
function napply(callback, thisp, args) {
|
||
return nbind(callback, thisp).apply(void 0, args);
|
||
}
|
||
|
||
/**
|
||
* Passes a continuation to a Node function, which is called with a given
|
||
* `this` value and arguments provided individually, and returns a promise.
|
||
*
|
||
* var FS = (require)("fs");
|
||
* Q.ncall(FS.readFile, FS, __filename)
|
||
* .then(function (content) {
|
||
* })
|
||
*
|
||
*/
|
||
exports.ncall = deprecate(ncall, "ncall", "ninvoke");
|
||
function ncall(callback, thisp /*, ...args*/) {
|
||
var args = array_slice(arguments, 2);
|
||
return napply(callback, thisp, args);
|
||
}
|
||
|
||
/**
|
||
* Wraps a NodeJS continuation passing function and returns an equivalent
|
||
* version that returns a promise.
|
||
*
|
||
* Q.nbind(FS.readFile, FS)(__filename)
|
||
* .then(console.log)
|
||
* .done()
|
||
*
|
||
*/
|
||
exports.nbind = deprecate(nbind, "nbind", "nfbind");
|
||
function nbind(callback /* thisp, ...args*/) {
|
||
if (arguments.length > 1) {
|
||
var thisp = arguments[1];
|
||
var args = array_slice(arguments, 2);
|
||
|
||
var originalCallback = callback;
|
||
callback = function () {
|
||
var combinedArgs = args.concat(array_slice(arguments));
|
||
return originalCallback.apply(thisp, combinedArgs);
|
||
};
|
||
}
|
||
return function () {
|
||
var deferred = defer();
|
||
var args = array_slice(arguments);
|
||
// add a continuation that resolves the promise
|
||
args.push(deferred.makeNodeResolver());
|
||
// trap exceptions thrown by the callback
|
||
fapply(callback, args)
|
||
.fail(deferred.reject);
|
||
return deferred.promise;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Calls a method of a Node-style object that accepts a Node-style
|
||
* callback with a given array of arguments, plus a provided callback.
|
||
* @param object an object that has the named method
|
||
* @param {String} name name of the method of object
|
||
* @param {Array} args arguments to pass to the method; the callback
|
||
* will be provided by Q and appended to these arguments.
|
||
* @returns a promise for the value or error
|
||
*/
|
||
exports.npost = npost;
|
||
function npost(object, name, args) {
|
||
var nodeArgs = array_slice(args);
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
|
||
post(object, name, nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
}
|
||
|
||
/**
|
||
* Calls a method of a Node-style object that accepts a Node-style
|
||
* callback, forwarding the given variadic arguments, plus a provided
|
||
* callback argument.
|
||
* @param object an object that has the named method
|
||
* @param {String} name name of the method of object
|
||
* @param ...args arguments to pass to the method; the callback will
|
||
* be provided by Q and appended to these arguments.
|
||
* @returns a promise for the value or error
|
||
*/
|
||
exports.ninvoke = ninvoke;
|
||
function ninvoke(object, name /*, ...args*/) {
|
||
var nodeArgs = array_slice(arguments, 2);
|
||
var deferred = defer();
|
||
nodeArgs.push(deferred.makeNodeResolver());
|
||
|
||
post(object, name, nodeArgs).fail(deferred.reject);
|
||
return deferred.promise;
|
||
}
|
||
|
||
exports.nend = deprecate(nodeify, "nend", "nodeify"); // XXX deprecated, use nodeify
|
||
exports.nodeify = nodeify;
|
||
function nodeify(promise, nodeback) {
|
||
if (nodeback) {
|
||
promise.then(function (value) {
|
||
nextTick(function () {
|
||
nodeback(null, value);
|
||
});
|
||
}, function (error) {
|
||
nextTick(function () {
|
||
nodeback(error);
|
||
});
|
||
});
|
||
} else {
|
||
return promise;
|
||
}
|
||
}
|
||
|
||
// All code before this point will be filtered from stack traces.
|
||
var qEndingLine = captureLine();
|
||
|
||
});
|
||
|
||
});
|
||
|
||
require.define("/src/js/visuals/visualization.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
|
||
var Collections = require('../models/collections');
|
||
var CommitCollection = Collections.CommitCollection;
|
||
var BranchCollection = Collections.BranchCollection;
|
||
|
||
var GitVisuals = require('../visuals').GitVisuals;
|
||
|
||
var Visualization = Backbone.View.extend({
|
||
initialize: function(options) {
|
||
var _this = this;
|
||
this.customEvents = _.clone(Backbone.Events);
|
||
|
||
new Raphael(10, 10, 200, 200, function() {
|
||
|
||
// for some reason raphael calls this function with a predefined
|
||
// context...
|
||
// so switch it
|
||
_this.paperInitialize(this, options);
|
||
});
|
||
},
|
||
|
||
paperInitialize: function(paper, options) {
|
||
options = options || {};
|
||
this.treeString = options.treeString;
|
||
this.paper = paper;
|
||
|
||
var Main = require('../app');
|
||
this.events = options.events || Main.getEvents();
|
||
this.eventBaton = options.eventBaton || Main.getEventBaton();
|
||
|
||
this.commitCollection = new CommitCollection();
|
||
this.branchCollection = new BranchCollection();
|
||
|
||
this.gitVisuals = new GitVisuals({
|
||
commitCollection: this.commitCollection,
|
||
branchCollection: this.branchCollection,
|
||
paper: this.paper
|
||
});
|
||
|
||
var GitEngine = require('../git').GitEngine;
|
||
this.gitEngine = new GitEngine({
|
||
collection: this.commitCollection,
|
||
branches: this.branchCollection,
|
||
gitVisuals: this.gitVisuals,
|
||
eventBaton: this.eventBaton
|
||
});
|
||
this.gitEngine.init();
|
||
this.gitVisuals.assignGitEngine(this.gitEngine);
|
||
|
||
this.myResize();
|
||
$(window).on('resize', _.bind(this.myResize, this));
|
||
this.gitVisuals.drawTreeFirstTime();
|
||
|
||
if (this.treeString) {
|
||
this.gitEngine.loadTreeFromString(this.treeString);
|
||
}
|
||
|
||
this.setTreeOpacity(0);
|
||
this.fadeTreeIn();
|
||
|
||
this.customEvents.trigger('gitEngineReady');
|
||
},
|
||
|
||
setTreeOpacity: function(level) {
|
||
$(this.paper.canvas).css('opacity', 0);
|
||
},
|
||
|
||
fadeTreeIn: function() {
|
||
$(this.paper.canvas).animate({opacity: 1}, 300);
|
||
},
|
||
|
||
myResize: function() {
|
||
var smaller = 1;
|
||
var el = this.el;
|
||
|
||
var left = el.offsetLeft;
|
||
var top = el.offsetTop;
|
||
var width = el.clientWidth - smaller;
|
||
var height = el.clientHeight - smaller;
|
||
|
||
$(this.paper.canvas).css({
|
||
left: left + 'px',
|
||
top: top + 'px'
|
||
});
|
||
this.paper.setSize(width, height);
|
||
this.gitVisuals.canvasResize(width, height);
|
||
}
|
||
});
|
||
|
||
exports.Visualization = Visualization;
|
||
|
||
});
|
||
|
||
require.define("/src/js/models/collections.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Q = require('q');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
|
||
var Commit = require('../git').Commit;
|
||
var Branch = require('../git').Branch;
|
||
|
||
var Command = require('../models/commandModel').Command;
|
||
var CommandEntry = require('../models/commandModel').CommandEntry;
|
||
var TIME = require('../util/constants').TIME;
|
||
|
||
var CommitCollection = Backbone.Collection.extend({
|
||
model: Commit
|
||
});
|
||
|
||
var CommandCollection = Backbone.Collection.extend({
|
||
model: Command
|
||
});
|
||
|
||
var BranchCollection = Backbone.Collection.extend({
|
||
model: Branch
|
||
});
|
||
|
||
var CommandEntryCollection = Backbone.Collection.extend({
|
||
model: CommandEntry,
|
||
localStorage: (Backbone.LocalStorage) ? new Backbone.LocalStorage('CommandEntries') : null
|
||
});
|
||
|
||
var CommandBuffer = Backbone.Model.extend({
|
||
defaults: {
|
||
collection: null
|
||
},
|
||
|
||
initialize: function(options) {
|
||
options.collection.bind('add', this.addCommand, this);
|
||
|
||
this.buffer = [];
|
||
this.timeout = null;
|
||
},
|
||
|
||
addCommand: function(command) {
|
||
this.buffer.push(command);
|
||
this.touchBuffer();
|
||
},
|
||
|
||
touchBuffer: function() {
|
||
// touch buffer just essentially means we just check if our buffer is being
|
||
// processed. if it's not, we immediately process the first item
|
||
// and then set the timeout.
|
||
if (this.timeout) {
|
||
// timeout existence implies its being processed
|
||
return;
|
||
}
|
||
this.setTimeout();
|
||
},
|
||
|
||
|
||
setTimeout: function() {
|
||
this.timeout = setTimeout(_.bind(function() {
|
||
this.sipFromBuffer();
|
||
}, this), TIME.betweenCommandsDelay);
|
||
},
|
||
|
||
popAndProcess: function() {
|
||
var popped = this.buffer.shift(0);
|
||
|
||
// find a command with no error (aka unprocessed)
|
||
while (popped.get('error') && this.buffer.length) {
|
||
popped = this.buffer.pop();
|
||
}
|
||
if (!popped.get('error')) {
|
||
this.processCommand(popped);
|
||
} else {
|
||
// no more commands to process
|
||
this.clear();
|
||
}
|
||
},
|
||
|
||
processCommand: function(command) {
|
||
command.set('status', 'processing');
|
||
|
||
var deferred = Q.defer();
|
||
deferred.promise.then(_.bind(function() {
|
||
this.setTimeout();
|
||
}, this));
|
||
|
||
var eventName = command.get('eventName');
|
||
if (!eventName) {
|
||
throw new Error('I need an event to trigger when this guy is parsed and ready');
|
||
}
|
||
|
||
var Main = require('../app');
|
||
Main.getEventBaton().trigger(eventName, command, deferred);
|
||
},
|
||
|
||
clear: function() {
|
||
clearTimeout(this.timeout);
|
||
this.timeout = null;
|
||
},
|
||
|
||
sipFromBuffer: function() {
|
||
if (!this.buffer.length) {
|
||
this.clear();
|
||
return;
|
||
}
|
||
|
||
this.popAndProcess();
|
||
}
|
||
});
|
||
|
||
exports.CommitCollection = CommitCollection;
|
||
exports.CommandCollection = CommandCollection;
|
||
exports.BranchCollection = BranchCollection;
|
||
exports.CommandEntryCollection = CommandEntryCollection;
|
||
exports.CommandBuffer = CommandBuffer;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/git/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
var Q = require('q');
|
||
|
||
var AnimationFactoryModule = require('../visuals/animation/animationFactory');
|
||
var AnimationQueue = require('../visuals/animation').AnimationQueue;
|
||
var TreeCompare = require('./treeCompare').TreeCompare;
|
||
|
||
var Errors = require('../util/errors');
|
||
var GitError = Errors.GitError;
|
||
var CommandResult = Errors.CommandResult;
|
||
|
||
function GitEngine(options) {
|
||
this.rootCommit = null;
|
||
this.refs = {};
|
||
this.HEAD = null;
|
||
|
||
this.branchCollection = options.branches;
|
||
this.commitCollection = options.collection;
|
||
this.gitVisuals = options.gitVisuals;
|
||
|
||
this.eventBaton = options.eventBaton;
|
||
this.eventBaton.stealBaton('processGitCommand', this.dispatch, this);
|
||
|
||
this.animationFactory = options.animationFactory ||
|
||
new AnimationFactoryModule.AnimationFactory();
|
||
|
||
// global variable to keep track of the options given
|
||
// along with the command call.
|
||
this.commandOptions = {};
|
||
this.generalArgs = [];
|
||
|
||
|
||
// backbone or something uses _.uniqueId, so we make our own here
|
||
this.uniqueId = (function() {
|
||
var n = 0;
|
||
return function(prepend) {
|
||
return prepend? prepend + n++ : n++;
|
||
};
|
||
})();
|
||
}
|
||
|
||
GitEngine.prototype.defaultInit = function() {
|
||
var defaultTree = JSON.parse(unescape("%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%2C%22type%22%3A%22branch%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%22C0%22%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C1%22%7D%7D%2C%22HEAD%22%3A%7B%22id%22%3A%22HEAD%22%2C%22target%22%3A%22master%22%2C%22type%22%3A%22general%20ref%22%7D%7D"));
|
||
this.loadTree(defaultTree);
|
||
};
|
||
|
||
GitEngine.prototype.init = function() {
|
||
// make an initial commit and a master branch
|
||
this.rootCommit = this.makeCommit(null, null, {rootCommit: true});
|
||
this.commitCollection.add(this.rootCommit);
|
||
|
||
var master = this.makeBranch('master', this.rootCommit);
|
||
this.HEAD = new Ref({
|
||
id: 'HEAD',
|
||
target: master
|
||
});
|
||
this.refs[this.HEAD.get('id')] = this.HEAD;
|
||
|
||
// commit once to get things going
|
||
this.commit();
|
||
};
|
||
|
||
GitEngine.prototype.exportTree = function() {
|
||
// need to export all commits, their connectivity / messages, branches, and state of head.
|
||
// this would be simple if didn't have circular structures.... :P
|
||
// thus, we need to loop through and "flatten" our graph of objects referencing one another
|
||
var totalExport = {
|
||
branches: {},
|
||
commits: {},
|
||
HEAD: null
|
||
};
|
||
|
||
_.each(this.branchCollection.toJSON(), function(branch) {
|
||
branch.target = branch.target.get('id');
|
||
branch.visBranch = undefined;
|
||
|
||
totalExport.branches[branch.id] = branch;
|
||
});
|
||
|
||
_.each(this.commitCollection.toJSON(), function(commit) {
|
||
// clear out the fields that reference objects and create circular structure
|
||
_.each(Commit.prototype.constants.circularFields, function(field) {
|
||
commit[field] = undefined;
|
||
}, this);
|
||
|
||
// convert parents
|
||
var parents = [];
|
||
_.each(commit.parents, function(par) {
|
||
parents.push(par.get('id'));
|
||
});
|
||
commit.parents = parents;
|
||
|
||
totalExport.commits[commit.id] = commit;
|
||
}, this);
|
||
|
||
var HEAD = this.HEAD.toJSON();
|
||
HEAD.visBranch = undefined;
|
||
HEAD.lastTarget = HEAD.lastLastTarget = HEAD.visBranch = undefined;
|
||
HEAD.target = HEAD.target.get('id');
|
||
totalExport.HEAD = HEAD;
|
||
|
||
return totalExport;
|
||
};
|
||
|
||
GitEngine.prototype.printTree = function(tree) {
|
||
tree = tree || this.exportTree();
|
||
TreeCompare.prototype.reduceTreeFields([tree]);
|
||
|
||
var str = JSON.stringify(tree);
|
||
if (/'/.test(str)) {
|
||
// escape it to make it more copy paste friendly
|
||
str = escape(str);
|
||
}
|
||
return str;
|
||
};
|
||
|
||
GitEngine.prototype.printAndCopyTree = function() {
|
||
window.prompt('Copy the tree string below', this.printTree());
|
||
};
|
||
|
||
GitEngine.prototype.loadTree = function(tree) {
|
||
// deep copy in case we use it a bunch
|
||
tree = $.extend(true, {}, tree);
|
||
|
||
// first clear everything
|
||
this.removeAll();
|
||
|
||
this.instantiateFromTree(tree);
|
||
|
||
this.reloadGraphics();
|
||
};
|
||
|
||
GitEngine.prototype.loadTreeFromString = function(treeString) {
|
||
this.loadTree(JSON.parse(unescape(treeString)));
|
||
};
|
||
|
||
GitEngine.prototype.instantiateFromTree = function(tree) {
|
||
// now we do the loading part
|
||
var createdSoFar = {};
|
||
|
||
_.each(tree.commits, function(commitJSON) {
|
||
var commit = this.getOrMakeRecursive(tree, createdSoFar, commitJSON.id);
|
||
this.commitCollection.add(commit);
|
||
}, this);
|
||
|
||
_.each(tree.branches, function(branchJSON) {
|
||
var branch = this.getOrMakeRecursive(tree, createdSoFar, branchJSON.id);
|
||
|
||
this.branchCollection.add(branch, {silent: true});
|
||
}, this);
|
||
|
||
var HEAD = this.getOrMakeRecursive(tree, createdSoFar, tree.HEAD.id);
|
||
this.HEAD = HEAD;
|
||
|
||
this.rootCommit = createdSoFar['C0'];
|
||
if (!this.rootCommit) {
|
||
throw new Error('Need root commit of C0 for calculations');
|
||
}
|
||
this.refs = createdSoFar;
|
||
|
||
this.branchCollection.each(function(branch) {
|
||
this.gitVisuals.addBranch(branch);
|
||
}, this);
|
||
};
|
||
|
||
GitEngine.prototype.reloadGraphics = function() {
|
||
// get the root commit, no better way to do it
|
||
var rootCommit = null;
|
||
this.commitCollection.each(function(commit) {
|
||
if (commit.get('id') == 'C0') {
|
||
rootCommit = commit;
|
||
}
|
||
});
|
||
this.gitVisuals.rootCommit = rootCommit;
|
||
|
||
// this just basically makes the HEAD branch. the head branch really should have been
|
||
// a member of a collection and not this annoying edge case stuff... one day
|
||
this.gitVisuals.initHeadBranch();
|
||
|
||
// when the paper is ready
|
||
this.gitVisuals.drawTreeFromReload();
|
||
|
||
this.gitVisuals.refreshTreeHarsh();
|
||
};
|
||
|
||
GitEngine.prototype.getOrMakeRecursive = function(tree, createdSoFar, objID) {
|
||
if (createdSoFar[objID]) {
|
||
// base case
|
||
return createdSoFar[objID];
|
||
}
|
||
|
||
var getType = function(tree, id) {
|
||
if (tree.commits[id]) {
|
||
return 'commit';
|
||
} else if (tree.branches[id]) {
|
||
return 'branch';
|
||
} else if (id == 'HEAD') {
|
||
return 'HEAD';
|
||
}
|
||
throw new Error("bad type for " + id);
|
||
};
|
||
|
||
// figure out what type
|
||
var type = getType(tree, objID);
|
||
|
||
if (type == 'HEAD') {
|
||
var headJSON = tree.HEAD;
|
||
var HEAD = new Ref(_.extend(
|
||
tree.HEAD,
|
||
{
|
||
target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target)
|
||
}
|
||
));
|
||
createdSoFar[objID] = HEAD;
|
||
return HEAD;
|
||
}
|
||
|
||
if (type == 'branch') {
|
||
var branchJSON = tree.branches[objID];
|
||
|
||
var branch = new Branch(_.extend(
|
||
tree.branches[objID],
|
||
{
|
||
target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target)
|
||
}
|
||
));
|
||
createdSoFar[objID] = branch;
|
||
return branch;
|
||
}
|
||
|
||
if (type == 'commit') {
|
||
// for commits, we need to grab all the parents
|
||
var commitJSON = tree.commits[objID];
|
||
|
||
var parentObjs = [];
|
||
_.each(commitJSON.parents, function(parentID) {
|
||
parentObjs.push(this.getOrMakeRecursive(tree, createdSoFar, parentID));
|
||
}, this);
|
||
|
||
var commit = new Commit(_.extend(
|
||
commitJSON,
|
||
{
|
||
parents: parentObjs,
|
||
gitVisuals: this.gitVisuals
|
||
}
|
||
));
|
||
createdSoFar[objID] = commit;
|
||
return commit;
|
||
}
|
||
|
||
throw new Error('ruh rho!! unsupported tyep for ' + objID);
|
||
};
|
||
|
||
GitEngine.prototype.removeAll = function() {
|
||
this.branchCollection.reset();
|
||
this.commitCollection.reset();
|
||
this.refs = {};
|
||
this.HEAD = null;
|
||
this.rootCommit = null;
|
||
|
||
this.gitVisuals.resetAll();
|
||
};
|
||
|
||
GitEngine.prototype.getDetachedHead = function() {
|
||
// detached head is if HEAD points to a commit instead of a branch...
|
||
var target = this.HEAD.get('target');
|
||
var targetType = target.get('type');
|
||
return targetType !== 'branch';
|
||
};
|
||
|
||
GitEngine.prototype.validateBranchName = function(name) {
|
||
name = name.replace(/\s/g, '');
|
||
if (!/^[a-zA-Z0-9]+$/.test(name)) {
|
||
throw new GitError({
|
||
msg: 'woah bad branch name!! This is not ok: ' + name
|
||
});
|
||
}
|
||
if (/[hH][eE][aA][dD]/.test(name)) {
|
||
throw new GitError({
|
||
msg: 'branch name of "head" is ambiguous, dont name it that'
|
||
});
|
||
}
|
||
if (name.length > 9) {
|
||
name = name.slice(0, 9);
|
||
this.command.addWarning(
|
||
'Sorry, we need to keep branch names short for the visuals. Your branch ' +
|
||
'name was truncated to 9 characters, resulting in ' + name
|
||
);
|
||
}
|
||
return name;
|
||
};
|
||
|
||
GitEngine.prototype.makeBranch = function(id, target) {
|
||
id = this.validateBranchName(id);
|
||
if (this.refs[id]) {
|
||
throw new GitError({
|
||
msg: 'that branch id either matches a commit hash or already exists!'
|
||
});
|
||
}
|
||
|
||
var branch = new Branch({
|
||
target: target,
|
||
id: id
|
||
});
|
||
this.branchCollection.add(branch);
|
||
this.refs[branch.get('id')] = branch;
|
||
return branch;
|
||
};
|
||
|
||
GitEngine.prototype.getHead = function() {
|
||
return _.clone(this.HEAD);
|
||
};
|
||
|
||
GitEngine.prototype.getBranches = function() {
|
||
var toReturn = [];
|
||
this.branchCollection.each(function(branch) {
|
||
toReturn.push({
|
||
id: branch.get('id'),
|
||
selected: this.HEAD.get('target') === branch,
|
||
target: branch.get('target'),
|
||
obj: branch
|
||
});
|
||
}, this);
|
||
return toReturn;
|
||
};
|
||
|
||
GitEngine.prototype.printBranchesWithout = function(without) {
|
||
var commitToBranches = this.getUpstreamBranchSet();
|
||
var commitID = this.getCommitFromRef(without).get('id');
|
||
|
||
var toPrint = [];
|
||
_.each(commitToBranches[commitID], function(branchJSON) {
|
||
branchJSON.selected = this.HEAD.get('target').get('id') == branchJSON.id;
|
||
toPrint.push(branchJSON);
|
||
}, this);
|
||
this.printBranches(toPrint);
|
||
};
|
||
|
||
GitEngine.prototype.printBranches = function(branches) {
|
||
var result = '';
|
||
_.each(branches, function(branch) {
|
||
result += (branch.selected ? '* ' : '') + branch.id + '\n';
|
||
});
|
||
throw new CommandResult({
|
||
msg: result
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.makeCommit = function(parents, id, options) {
|
||
// ok we need to actually manually create commit IDs now because
|
||
// people like nikita (thanks for finding this!) could
|
||
// make branches named C2 before creating the commit C2
|
||
if (!id) {
|
||
id = this.uniqueId('C');
|
||
while (this.refs[id]) {
|
||
id = this.uniqueId('C');
|
||
}
|
||
}
|
||
|
||
var commit = new Commit(_.extend({
|
||
parents: parents,
|
||
id: id,
|
||
gitVisuals: this.gitVisuals
|
||
},
|
||
options || {}
|
||
));
|
||
|
||
this.refs[commit.get('id')] = commit;
|
||
this.commitCollection.add(commit);
|
||
return commit;
|
||
};
|
||
|
||
GitEngine.prototype.acceptNoGeneralArgs = function() {
|
||
if (this.generalArgs.length) {
|
||
throw new GitError({
|
||
msg: "That command accepts no general arguments"
|
||
});
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.validateArgBounds = function(args, lower, upper, option) {
|
||
// this is a little utility class to help arg validation that happens over and over again
|
||
var what = (option === undefined) ?
|
||
'git ' + this.command.get('method') :
|
||
this.command.get('method') + ' ' + option + ' ';
|
||
what = 'with ' + what;
|
||
|
||
if (args.length < lower) {
|
||
throw new GitError({
|
||
msg: 'I expect at least ' + String(lower) + ' argument(s) ' + what
|
||
});
|
||
}
|
||
if (args.length > upper) {
|
||
throw new GitError({
|
||
msg: 'I expect at most ' + String(upper) + ' argument(s) ' + what
|
||
});
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.oneArgImpliedHead = function(args, option) {
|
||
// for log, show, etc
|
||
this.validateArgBounds(args, 0, 1, option);
|
||
if (args.length === 0) {
|
||
args.push('HEAD');
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.twoArgsImpliedHead = function(args, option) {
|
||
// our args we expect to be between 1 and 2
|
||
this.validateArgBounds(args, 1, 2, option);
|
||
// and if it's one, add a HEAD to the back
|
||
if (args.length == 1) {
|
||
args.push('HEAD');
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.revertStarter = function() {
|
||
this.validateArgBounds(this.generalArgs, 1, NaN);
|
||
|
||
var response = this.revert(this.generalArgs);
|
||
|
||
if (response) {
|
||
this.animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.revert = function(whichCommits) {
|
||
// for each commit, we want to revert it
|
||
var toRebase = [];
|
||
_.each(whichCommits, function(stringRef) {
|
||
toRebase.push(this.getCommitFromRef(stringRef));
|
||
}, this);
|
||
|
||
// we animate reverts now!! we use the rebase animation though so that's
|
||
// why the terminology is like it is
|
||
var animationResponse = {};
|
||
animationResponse.destinationBranch = this.resolveID(toRebase[0]);
|
||
animationResponse.toRebaseArray = toRebase.slice(0);
|
||
animationResponse.rebaseSteps = [];
|
||
|
||
var beforeSnapshot = this.gitVisuals.genSnapshot();
|
||
var afterSnapshot;
|
||
|
||
// now make a bunch of commits on top of where we are
|
||
var base = this.getCommitFromRef('HEAD');
|
||
_.each(toRebase, function(oldCommit) {
|
||
var newId = this.rebaseAltID(oldCommit.get('id'));
|
||
|
||
var newCommit = this.makeCommit([base], newId, {
|
||
commitMessage: 'Reverting ' + this.resolveName(oldCommit) +
|
||
': "' + oldCommit.get('commitMessage') + '"'
|
||
});
|
||
|
||
base = newCommit;
|
||
|
||
// animation stuff
|
||
afterSnapshot = this.gitVisuals.genSnapshot();
|
||
animationResponse.rebaseSteps.push({
|
||
oldCommit: oldCommit,
|
||
newCommit: newCommit,
|
||
beforeSnapshot: beforeSnapshot,
|
||
afterSnapshot: afterSnapshot
|
||
});
|
||
beforeSnapshot = afterSnapshot;
|
||
}, this);
|
||
// done! update our location
|
||
this.setTargetLocation('HEAD', base);
|
||
|
||
// animation
|
||
return animationResponse;
|
||
};
|
||
|
||
GitEngine.prototype.resetStarter = function() {
|
||
if (this.commandOptions['--soft']) {
|
||
throw new GitError({
|
||
msg: "You can't use --soft because there is no concept of stashing" +
|
||
" changes or staging files, so you will lose your progress." +
|
||
" Try using interactive rebasing (or just rebasing) to move commits."
|
||
});
|
||
}
|
||
if (this.commandOptions['--hard']) {
|
||
this.command.addWarning(
|
||
'Nice! You are using --hard. The default behavior is a hard reset in ' +
|
||
"this demo, so don't worry about specifying the option explicity"
|
||
);
|
||
// dont absorb the arg off of --hard
|
||
this.generalArgs = this.generalArgs.concat(this.commandOptions['--hard']);
|
||
}
|
||
|
||
this.validateArgBounds(this.generalArgs, 1, 1);
|
||
|
||
if (this.getDetachedHead()) {
|
||
throw new GitError({
|
||
msg: "Cant reset in detached head! Use checkout if you want to move"
|
||
});
|
||
}
|
||
|
||
this.reset(this.generalArgs[0]);
|
||
};
|
||
|
||
GitEngine.prototype.reset = function(target) {
|
||
this.setTargetLocation('HEAD', this.getCommitFromRef(target));
|
||
};
|
||
|
||
GitEngine.prototype.cherrypickStarter = function() {
|
||
this.validateArgBounds(this.generalArgs, 1, 1);
|
||
var newCommit = this.cherrypick(this.generalArgs[0]);
|
||
|
||
this.animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
|
||
};
|
||
|
||
GitEngine.prototype.cherrypick = function(ref) {
|
||
var commit = this.getCommitFromRef(ref);
|
||
// check if we already have that
|
||
var set = this.getUpstreamSet('HEAD');
|
||
if (set[commit.get('id')]) {
|
||
throw new GitError({
|
||
msg: "We already have that commit in our changes history! You can't cherry-pick it " +
|
||
"if it shows up in git log."
|
||
});
|
||
}
|
||
|
||
// alter the ID slightly
|
||
var id = this.rebaseAltID(commit.get('id'));
|
||
|
||
// now commit with that id onto HEAD
|
||
var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id);
|
||
this.setTargetLocation(this.HEAD, newCommit);
|
||
return newCommit;
|
||
};
|
||
|
||
GitEngine.prototype.commitStarter = function() {
|
||
this.acceptNoGeneralArgs();
|
||
if (this.commandOptions['-am'] && (
|
||
this.commandOptions['-a'] || this.commandOptions['-m'])) {
|
||
throw new GitError({
|
||
msg: "You can't have -am with another -m or -a!"
|
||
});
|
||
}
|
||
|
||
var msg = null;
|
||
var args = null;
|
||
if (this.commandOptions['-a']) {
|
||
this.command.addWarning('No need to add files in this demo');
|
||
}
|
||
|
||
if (this.commandOptions['-am']) {
|
||
args = this.commandOptions['-am'];
|
||
this.validateArgBounds(args, 1, 1, '-am');
|
||
|
||
this.command.addWarning("Don't worry about adding files in this demo. I'll take " +
|
||
"down your commit message anyways, but you can commit without a message " +
|
||
"in this demo as well");
|
||
msg = args[0];
|
||
}
|
||
|
||
if (this.commandOptions['-m']) {
|
||
args = this.commandOptions['-m'];
|
||
this.validateArgBounds(args, 1, 1, '-m');
|
||
msg = args[0];
|
||
}
|
||
|
||
var newCommit = this.commit();
|
||
if (msg) {
|
||
msg = msg
|
||
.replace(/"/g, '"')
|
||
.replace(/^"/g, '')
|
||
.replace(/"$/g, '');
|
||
|
||
newCommit.set('commitMessage', msg);
|
||
}
|
||
this.animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
|
||
};
|
||
|
||
GitEngine.prototype.commit = function() {
|
||
var targetCommit = this.getCommitFromRef(this.HEAD);
|
||
var id = null;
|
||
|
||
// if we want to ammend, go one above
|
||
if (this.commandOptions['--amend']) {
|
||
targetCommit = this.resolveID('HEAD~1');
|
||
id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id'));
|
||
}
|
||
|
||
var newCommit = this.makeCommit([targetCommit], id);
|
||
if (this.getDetachedHead()) {
|
||
this.command.addWarning('Warning!! Detached HEAD state');
|
||
}
|
||
|
||
this.setTargetLocation(this.HEAD, newCommit);
|
||
return newCommit;
|
||
};
|
||
|
||
GitEngine.prototype.resolveName = function(someRef) {
|
||
// first get the obj
|
||
var obj = this.resolveID(someRef);
|
||
if (obj.get('type') == 'commit') {
|
||
return 'commit ' + obj.get('id');
|
||
}
|
||
if (obj.get('type') == 'branch') {
|
||
return 'branch "' + obj.get('id') + '"';
|
||
}
|
||
// we are dealing with HEAD
|
||
return this.resolveName(obj.get('target'));
|
||
};
|
||
|
||
GitEngine.prototype.resolveID = function(idOrTarget) {
|
||
if (idOrTarget === null || idOrTarget === undefined) {
|
||
throw new Error('Dont call this with null / undefined');
|
||
}
|
||
|
||
if (typeof idOrTarget !== 'string') {
|
||
return idOrTarget;
|
||
}
|
||
return this.resolveStringRef(idOrTarget);
|
||
};
|
||
|
||
GitEngine.prototype.resolveStringRef = function(ref) {
|
||
if (this.refs[ref]) {
|
||
return this.refs[ref];
|
||
}
|
||
|
||
// may be something like HEAD~2 or master^^
|
||
var relativeRefs = [
|
||
[/^([a-zA-Z0-9]+)~(\d+)\s*$/, function(matches) {
|
||
return parseInt(matches[2], 10);
|
||
}],
|
||
[/^([a-zA-Z0-9]+)(\^+)\s*$/, function(matches) {
|
||
return matches[2].length;
|
||
}]
|
||
];
|
||
|
||
var startRef = null;
|
||
var numBack = null;
|
||
_.each(relativeRefs, function(config) {
|
||
var regex = config[0];
|
||
var parse = config[1];
|
||
if (regex.test(ref)) {
|
||
var matches = regex.exec(ref);
|
||
numBack = parse(matches);
|
||
startRef = matches[1];
|
||
}
|
||
}, this);
|
||
|
||
if (!startRef) {
|
||
throw new GitError({
|
||
msg: 'unknown ref ' + ref
|
||
});
|
||
}
|
||
if (!this.refs[startRef]) {
|
||
throw new GitError({
|
||
msg: 'the ref ' + startRef +' does not exist.'
|
||
});
|
||
}
|
||
var commit = this.getCommitFromRef(startRef);
|
||
|
||
return this.numBackFrom(commit, numBack);
|
||
};
|
||
|
||
GitEngine.prototype.getCommitFromRef = function(ref) {
|
||
var start = this.resolveID(ref);
|
||
|
||
// works for both HEAD and just a single layer. aka branch
|
||
while (start.get('type') !== 'commit') {
|
||
start = start.get('target');
|
||
}
|
||
return start;
|
||
};
|
||
|
||
GitEngine.prototype.getType = function(ref) {
|
||
return this.resolveID(ref).get('type');
|
||
};
|
||
|
||
GitEngine.prototype.setTargetLocation = function(ref, target) {
|
||
if (this.getType(ref) == 'commit') {
|
||
// nothing to do
|
||
return;
|
||
}
|
||
|
||
// sets whatever ref is (branch, HEAD, etc) to a target. so if
|
||
// you pass in HEAD, and HEAD is pointing to a branch, it will update
|
||
// the branch to that commit, not the HEAD
|
||
ref = this.getOneBeforeCommit(ref);
|
||
ref.set('target', target);
|
||
};
|
||
|
||
GitEngine.prototype.getUpstreamBranchSet = function() {
|
||
// this is expensive!! so only call once in a while
|
||
var commitToSet = {};
|
||
|
||
var inArray = function(arr, id) {
|
||
var found = false;
|
||
_.each(arr, function(wrapper) {
|
||
if (wrapper.id == id) {
|
||
found = true;
|
||
}
|
||
});
|
||
|
||
return found;
|
||
};
|
||
|
||
var bfsSearch = function(commit) {
|
||
var set = [];
|
||
var pQueue = [commit];
|
||
while (pQueue.length) {
|
||
var popped = pQueue.pop();
|
||
set.push(popped.get('id'));
|
||
|
||
if (popped.get('parents') && popped.get('parents').length) {
|
||
pQueue = pQueue.concat(popped.get('parents'));
|
||
}
|
||
}
|
||
return set;
|
||
};
|
||
|
||
this.branchCollection.each(function(branch) {
|
||
var set = bfsSearch(branch.get('target'));
|
||
_.each(set, function(id) {
|
||
commitToSet[id] = commitToSet[id] || [];
|
||
|
||
// only add it if it's not there, so hue blending is ok
|
||
if (!inArray(commitToSet[id], branch.get('id'))) {
|
||
commitToSet[id].push({
|
||
obj: branch,
|
||
id: branch.get('id')
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
return commitToSet;
|
||
};
|
||
|
||
GitEngine.prototype.getUpstreamHeadSet = function() {
|
||
var set = this.getUpstreamSet('HEAD');
|
||
var including = this.getCommitFromRef('HEAD').get('id');
|
||
|
||
set[including] = true;
|
||
return set;
|
||
};
|
||
|
||
GitEngine.prototype.getOneBeforeCommit = function(ref) {
|
||
// you can call this command on HEAD in detached, HEAD, or on a branch
|
||
// and it will return the ref that is one above a commit. aka
|
||
// it resolves HEAD to something that we can move the ref with
|
||
var start = this.resolveID(ref);
|
||
if (start === this.HEAD && !this.getDetachedHead()) {
|
||
start = start.get('target');
|
||
}
|
||
return start;
|
||
};
|
||
|
||
GitEngine.prototype.numBackFrom = function(commit, numBack) {
|
||
// going back '3' from a given ref is not trivial, for you might have
|
||
// a bunch of merge commits and such. like this situation:
|
||
//
|
||
// * merge master into new
|
||
// |\
|
||
// | \* commit here
|
||
// |* \ commit there
|
||
// | |* commit here
|
||
// \ /
|
||
// | * root
|
||
//
|
||
//
|
||
// hence we need to do a BFS search, with the commit date being the
|
||
// value to sort off of (rather than just purely the level)
|
||
if (numBack === 0) {
|
||
return commit;
|
||
}
|
||
|
||
// we use a special sorting function here that
|
||
// prefers the later commits over the earlier ones
|
||
var sortQueue = _.bind(function(queue) {
|
||
queue.sort(this.dateSortFunc);
|
||
}, this);
|
||
|
||
var pQueue = [].concat(commit.get('parents') || []);
|
||
sortQueue(pQueue);
|
||
numBack--;
|
||
|
||
while (pQueue.length && numBack !== 0) {
|
||
var popped = pQueue.shift(0);
|
||
var parents = popped.get('parents');
|
||
|
||
if (parents && parents.length) {
|
||
pQueue = pQueue.concat(parents);
|
||
}
|
||
|
||
sortQueue(pQueue);
|
||
numBack--;
|
||
}
|
||
|
||
if (numBack !== 0 || pQueue.length === 0) {
|
||
throw new GitError({
|
||
msg: "Sorry, I can't go that many commits back"
|
||
});
|
||
}
|
||
return pQueue.shift(0);
|
||
};
|
||
|
||
GitEngine.prototype.scrapeBaseID = function(id) {
|
||
var results = /^C(\d+)/.exec(id);
|
||
|
||
if (!results) {
|
||
throw new Error('regex failed on ' + id);
|
||
}
|
||
|
||
return 'C' + results[1];
|
||
};
|
||
|
||
GitEngine.prototype.rebaseAltID = function(id) {
|
||
// this function alters an ID to add a quote to the end,
|
||
// indicating that it was rebased. it also checks existence
|
||
var regexMap = [
|
||
[/^C(\d+)[']{0,2}$/, function(bits) {
|
||
// this id can use another quote, so just add it
|
||
return bits[0] + "'";
|
||
}],
|
||
[/^C(\d+)[']{3}$/, function(bits) {
|
||
// here we switch from C''' to C'^4
|
||
return bits[0].slice(0, -3) + "'^4";
|
||
}],
|
||
[/^C(\d+)['][\^](\d+)$/, function(bits) {
|
||
return 'C' + String(bits[1]) + "'^" + String(Number(bits[2]) + 1);
|
||
}]
|
||
];
|
||
|
||
for (var i = 0; i < regexMap.length; i++) {
|
||
var regex = regexMap[i][0];
|
||
var func = regexMap[i][1];
|
||
var results = regex.exec(id);
|
||
if (results) {
|
||
var newId = func(results);
|
||
// if this id exists, continue down the rabbit hole
|
||
if (this.refs[newId]) {
|
||
return this.rebaseAltID(newId);
|
||
} else {
|
||
return newId;
|
||
}
|
||
}
|
||
}
|
||
throw new Error('could not modify the id ' + id);
|
||
};
|
||
|
||
GitEngine.prototype.idSortFunc = function(cA, cB) {
|
||
// commit IDs can come in many forms:
|
||
// C4
|
||
// C4' (from a rebase)
|
||
// C4'' (from multiple rebases)
|
||
// C4'^3 (from a BUNCH of rebases)
|
||
|
||
var scale = 1000;
|
||
|
||
var regexMap = [
|
||
[/^C(\d+)$/, function(bits) {
|
||
// return the 4 from C4
|
||
return scale * bits[1];
|
||
}],
|
||
[/^C(\d+)([']+)$/, function(bits) {
|
||
// return the 4 from C4, plus the length of the quotes
|
||
return scale * bits[1] + bits[2].length;
|
||
}],
|
||
[/^C(\d+)['][\^](\d+)$/, function(bits) {
|
||
return scale * bits[1] + Number(bits[2]);
|
||
}]
|
||
];
|
||
|
||
var getNumToSort = function(id) {
|
||
for (var i = 0; i < regexMap.length; i++) {
|
||
var regex = regexMap[i][0];
|
||
var func = regexMap[i][1];
|
||
var results = regex.exec(id);
|
||
if (results) {
|
||
return func(results);
|
||
}
|
||
}
|
||
throw new Error('Could not parse commit ID ' + id);
|
||
};
|
||
|
||
return getNumToSort(cA.get('id')) - getNumToSort(cB.get('id'));
|
||
};
|
||
|
||
GitEngine.prototype.dateSortFunc = function(cA, cB) {
|
||
var dateA = new Date(cA.get('createTime'));
|
||
var dateB = new Date(cB.get('createTime'));
|
||
return dateA - dateB;
|
||
};
|
||
|
||
GitEngine.prototype.rebaseInteractiveStarter = function() {
|
||
var args = this.commandOptions['-i'];
|
||
this.twoArgsImpliedHead(args, ' -i');
|
||
|
||
this.rebaseInteractive(args[0], args[1]);
|
||
};
|
||
|
||
GitEngine.prototype.rebaseStarter = function() {
|
||
if (this.commandOptions['-i']) {
|
||
this.rebaseInteractiveStarter();
|
||
return;
|
||
}
|
||
|
||
this.twoArgsImpliedHead(this.generalArgs);
|
||
|
||
var response = this.rebase(this.generalArgs[0], this.generalArgs[1]);
|
||
|
||
if (response === undefined) {
|
||
// was a fastforward or already up to date. returning now
|
||
// will trigger the refresh animation by not adding anything to
|
||
// the animation queue
|
||
return;
|
||
}
|
||
|
||
this.animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
|
||
};
|
||
|
||
GitEngine.prototype.rebase = function(targetSource, currentLocation) {
|
||
// first some conditions
|
||
if (this.isUpstreamOf(targetSource, currentLocation)) {
|
||
this.command.setResult('Branch already up-to-date');
|
||
|
||
// git for some reason always checks out the branch you are rebasing,
|
||
// no matter the result of the rebase
|
||
this.checkout(currentLocation);
|
||
|
||
// returning instead of throwing makes a tree refresh
|
||
return;
|
||
}
|
||
|
||
if (this.isUpstreamOf(currentLocation, targetSource)) {
|
||
// just set the target of this current location to the source
|
||
this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
|
||
// we need the refresh tree animation to happen, so set the result directly
|
||
// instead of throwing
|
||
this.command.setResult('Fast-forwarding...');
|
||
|
||
this.checkout(currentLocation);
|
||
return;
|
||
}
|
||
|
||
// now the part of actually rebasing.
|
||
// We need to get the downstream set of targetSource first.
|
||
// then we BFS from currentLocation, using the downstream set as our stopping point.
|
||
// we need to BFS because we need to include all commits below
|
||
// pop these commits on top of targetSource and modify their ids with quotes
|
||
var stopSet = this.getUpstreamSet(targetSource);
|
||
|
||
// now BFS from here on out
|
||
var toRebaseRough = [];
|
||
var pQueue = [this.getCommitFromRef(currentLocation)];
|
||
|
||
while (pQueue.length) {
|
||
var popped = pQueue.pop();
|
||
|
||
// if its in the set, dont add it
|
||
if (stopSet[popped.get('id')]) {
|
||
continue;
|
||
}
|
||
|
||
// it's not in the set, so we need to rebase this commit
|
||
toRebaseRough.push(popped);
|
||
toRebaseRough.sort(this.dateSortFunc);
|
||
|
||
// keep searching
|
||
pQueue = pQueue.concat(popped.get('parents'));
|
||
}
|
||
|
||
return this.rebaseFinish(toRebaseRough, stopSet, targetSource, currentLocation);
|
||
};
|
||
|
||
GitEngine.prototype.rebaseInteractive = function(targetSource, currentLocation) {
|
||
// there are a reduced set of checks now, so we can't exactly use parts of the rebase function
|
||
// but it will look similar.
|
||
|
||
// first if we are upstream of the target
|
||
if (this.isUpstreamOf(currentLocation, targetSource)) {
|
||
throw new GitError({
|
||
msg: 'Nothing to do... (git throws a "noop" status here); ' +
|
||
'Your source is upstream of your rebase target'
|
||
});
|
||
}
|
||
|
||
// now get the stop set
|
||
var stopSet = this.getUpstreamSet(targetSource);
|
||
|
||
var toRebaseRough = [];
|
||
// standard BFS
|
||
var pQueue = [this.getCommitFromRef(currentLocation)];
|
||
|
||
while (pQueue.length) {
|
||
var popped = pQueue.pop();
|
||
|
||
if (stopSet[popped.get('id')]) {
|
||
continue;
|
||
}
|
||
|
||
toRebaseRough.push(popped);
|
||
pQueue = pQueue.concat(popped.get('parents'));
|
||
pQueue.sort(this.dateSortFunc);
|
||
}
|
||
|
||
// throw out merge's real fast and see if we have anything to do
|
||
var toRebase = [];
|
||
_.each(toRebaseRough, function(commit) {
|
||
if (commit.get('parents').length == 1) {
|
||
toRebase.push(commit);
|
||
}
|
||
});
|
||
|
||
if (!toRebase.length) {
|
||
throw new GitError({
|
||
msg: 'No commits to rebase! Everything is a merge commit'
|
||
});
|
||
}
|
||
|
||
// now do stuff :D since all our validation checks have passed, we are going to defer animation
|
||
// and actually launch the dialog
|
||
this.animationQueue.set('defer', true);
|
||
|
||
var deferred = Q.defer();
|
||
deferred.promise
|
||
.then(_.bind(function(userSpecifiedRebase) {
|
||
// first, they might have dropped everything (annoying)
|
||
if (!userSpecifiedRebase.length) {
|
||
throw new CommandResult({
|
||
msg: 'Nothing to do...'
|
||
});
|
||
}
|
||
|
||
// finish the rebase crap and animate!
|
||
var animationData = this.rebaseFinish(userSpecifiedRebase, {}, targetSource, currentLocation);
|
||
this.animationFactory.rebaseAnimation(this.animationQueue, animationData, this, this.gitVisuals);
|
||
this.animationQueue.start();
|
||
}, this))
|
||
.fail(_.bind(function(err) {
|
||
this.filterError(err);
|
||
this.command.set('error', err);
|
||
this.animationQueue.start();
|
||
}, this))
|
||
.done();
|
||
|
||
var InteractiveRebaseView = require('../views/rebaseView').InteractiveRebaseView;
|
||
// interactive rebase view will reject or resolve our promise
|
||
new InteractiveRebaseView({
|
||
deferred: deferred,
|
||
toRebase: toRebase
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.rebaseFinish = function(toRebaseRough, stopSet, targetSource, currentLocation) {
|
||
// now we have the all the commits between currentLocation and the set of target to rebase.
|
||
var animationResponse = {};
|
||
animationResponse.destinationBranch = this.resolveID(targetSource);
|
||
|
||
// we need to throw out merge commits
|
||
var toRebase = [];
|
||
_.each(toRebaseRough, function(commit) {
|
||
if (commit.get('parents').length == 1) {
|
||
toRebase.push(commit);
|
||
}
|
||
});
|
||
|
||
// we ALSO need to throw out commits that will do the same changes. like
|
||
// if the upstream set has a commit C4 and we have C4', we dont rebase the C4' again.
|
||
// get this by doing ID scraping
|
||
var changesAlreadyMade = {};
|
||
_.each(stopSet, function(val, key) {
|
||
changesAlreadyMade[this.scrapeBaseID(key)] = val; // val == true
|
||
}, this);
|
||
|
||
// now get rid of the commits that will redo same changes
|
||
toRebaseRough = toRebase;
|
||
toRebase = [];
|
||
_.each(toRebaseRough, function(commit) {
|
||
var baseID = this.scrapeBaseID(commit.get('id'));
|
||
if (!changesAlreadyMade[baseID]) {
|
||
toRebase.push(commit);
|
||
}
|
||
}, this);
|
||
|
||
if (!toRebase.length) {
|
||
throw new GitError({
|
||
msg: 'No Commits to Rebase! Everything else is merge commits or changes already have been applied'
|
||
});
|
||
}
|
||
|
||
// now reverse it once more to get it in the right order
|
||
toRebase.reverse();
|
||
animationResponse.toRebaseArray = toRebase.slice(0);
|
||
|
||
// now pop all of these commits onto targetLocation
|
||
var base = this.getCommitFromRef(targetSource);
|
||
|
||
// do the rebase, and also maintain all our animation info during this
|
||
animationResponse.rebaseSteps = [];
|
||
var beforeSnapshot = this.gitVisuals.genSnapshot();
|
||
var afterSnapshot;
|
||
_.each(toRebase, function(old) {
|
||
var newId = this.rebaseAltID(old.get('id'));
|
||
|
||
var newCommit = this.makeCommit([base], newId);
|
||
base = newCommit;
|
||
|
||
// animation info
|
||
afterSnapshot = this.gitVisuals.genSnapshot();
|
||
animationResponse.rebaseSteps.push({
|
||
oldCommit: old,
|
||
newCommit: newCommit,
|
||
beforeSnapshot: beforeSnapshot,
|
||
afterSnapshot: afterSnapshot
|
||
});
|
||
beforeSnapshot = afterSnapshot;
|
||
}, this);
|
||
|
||
if (this.resolveID(currentLocation).get('type') == 'commit') {
|
||
// we referenced a commit like git rebase C2 C1, so we have
|
||
// to manually check out C1'
|
||
|
||
var steps = animationResponse.rebaseSteps;
|
||
var newestCommit = steps[steps.length - 1].newCommit;
|
||
|
||
this.checkout(newestCommit);
|
||
} else {
|
||
// now we just need to update the rebased branch is
|
||
this.setTargetLocation(currentLocation, base);
|
||
this.checkout(currentLocation);
|
||
}
|
||
|
||
// for animation
|
||
return animationResponse;
|
||
};
|
||
|
||
GitEngine.prototype.mergeStarter = function() {
|
||
this.twoArgsImpliedHead(this.generalArgs);
|
||
|
||
var newCommit = this.merge(this.generalArgs[0], this.generalArgs[1]);
|
||
|
||
if (newCommit === undefined) {
|
||
// its just a fast forwrard
|
||
this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
|
||
return;
|
||
}
|
||
|
||
this.animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
|
||
};
|
||
|
||
GitEngine.prototype.merge = function(targetSource, currentLocation) {
|
||
// first some conditions
|
||
if (this.isUpstreamOf(targetSource, currentLocation) ||
|
||
this.getCommitFromRef(targetSource) === this.getCommitFromRef(currentLocation)) {
|
||
throw new CommandResult({
|
||
msg: 'Branch already up-to-date'
|
||
});
|
||
}
|
||
|
||
if (this.isUpstreamOf(currentLocation, targetSource)) {
|
||
// just set the target of this current location to the source
|
||
this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
|
||
// get fresh animation to happen
|
||
this.command.setResult('Fast-forwarding...');
|
||
return;
|
||
}
|
||
|
||
// now the part of making a merge commit
|
||
var parent1 = this.getCommitFromRef(currentLocation);
|
||
var parent2 = this.getCommitFromRef(targetSource);
|
||
|
||
// we need a fancy commit message
|
||
var msg = 'Merge ' + this.resolveName(targetSource) +
|
||
' into ' + this.resolveName(currentLocation);
|
||
|
||
// since we specify parent 1 as the first parent, it is the "main" parent
|
||
// and the node will be displayed below that branch / commit / whatever
|
||
var mergeCommit = this.makeCommit(
|
||
[parent1, parent2],
|
||
null,
|
||
{
|
||
commitMessage: msg
|
||
}
|
||
);
|
||
|
||
this.setTargetLocation(currentLocation, mergeCommit);
|
||
return mergeCommit;
|
||
};
|
||
|
||
GitEngine.prototype.checkoutStarter = function() {
|
||
var args = null;
|
||
if (this.commandOptions['-b']) {
|
||
// the user is really trying to just make a branch and then switch to it. so first:
|
||
args = this.commandOptions['-b'];
|
||
this.twoArgsImpliedHead(args, '-b');
|
||
|
||
var validId = this.validateBranchName(args[0]);
|
||
this.branch(validId, args[1]);
|
||
this.checkout(validId);
|
||
return;
|
||
}
|
||
|
||
if (this.commandOptions['-']) {
|
||
// get the heads last location
|
||
var lastPlace = this.HEAD.get('lastLastTarget');
|
||
if (!lastPlace) {
|
||
throw new GitError({
|
||
msg: 'Need a previous location to do - switching'
|
||
});
|
||
}
|
||
this.HEAD.set('target', lastPlace);
|
||
return;
|
||
}
|
||
|
||
if (this.commandOptions['-B']) {
|
||
args = this.commandOptions['-B'];
|
||
this.twoArgsImpliedHead(args, '-B');
|
||
|
||
this.forceBranch(args[0], args[1]);
|
||
this.checkout(args[0]);
|
||
return;
|
||
}
|
||
|
||
this.validateArgBounds(this.generalArgs, 1, 1);
|
||
|
||
this.checkout(this.unescapeQuotes(this.generalArgs[0]));
|
||
};
|
||
|
||
GitEngine.prototype.checkout = function(idOrTarget) {
|
||
var target = this.resolveID(idOrTarget);
|
||
if (target.get('id') === 'HEAD') {
|
||
// git checkout HEAD is a
|
||
// meaningless command but i used to do this back in the day
|
||
return;
|
||
}
|
||
|
||
var type = target.get('type');
|
||
if (type !== 'branch' && type !== 'commit') {
|
||
throw new GitError({
|
||
msg: 'can only checkout branches and commits!'
|
||
});
|
||
}
|
||
|
||
this.HEAD.set('target', target);
|
||
};
|
||
|
||
GitEngine.prototype.branchStarter = function() {
|
||
var args = null;
|
||
// handle deletion first
|
||
if (this.commandOptions['-d'] || this.commandOptions['-D']) {
|
||
var names = this.commandOptions['-d'] || this.commandOptions['-D'];
|
||
this.validateArgBounds(names, 1, NaN, '-d');
|
||
|
||
_.each(names, function(name) {
|
||
this.deleteBranch(name);
|
||
}, this);
|
||
return;
|
||
}
|
||
|
||
if (this.commandOptions['--contains']) {
|
||
args = this.commandOptions['--contains'];
|
||
this.validateArgBounds(args, 1, 1, '--contains');
|
||
this.printBranchesWithout(args[0]);
|
||
return;
|
||
}
|
||
|
||
if (this.commandOptions['-f']) {
|
||
args = this.commandOptions['-f'];
|
||
this.twoArgsImpliedHead(args, '-f');
|
||
|
||
// we want to force a branch somewhere
|
||
this.forceBranch(args[0], args[1]);
|
||
return;
|
||
}
|
||
|
||
|
||
if (this.generalArgs.length === 0) {
|
||
this.printBranches(this.getBranches());
|
||
return;
|
||
}
|
||
|
||
this.twoArgsImpliedHead(this.generalArgs);
|
||
this.branch(this.generalArgs[0], this.generalArgs[1]);
|
||
};
|
||
|
||
GitEngine.prototype.forceBranch = function(branchName, where) {
|
||
// if branchname doesn't exist...
|
||
if (!this.refs[branchName]) {
|
||
this.branch(branchName, where);
|
||
}
|
||
|
||
var branch = this.resolveID(branchName);
|
||
if (branch.get('type') !== 'branch') {
|
||
throw new GitError({
|
||
msg: "Can't force move anything but a branch!!"
|
||
});
|
||
}
|
||
|
||
var whereCommit = this.getCommitFromRef(where);
|
||
|
||
this.setTargetLocation(branch, whereCommit);
|
||
};
|
||
|
||
GitEngine.prototype.branch = function(name, ref) {
|
||
var target = this.getCommitFromRef(ref);
|
||
this.makeBranch(name, target);
|
||
};
|
||
|
||
GitEngine.prototype.deleteBranch = function(name) {
|
||
// trying to delete, lets check our refs
|
||
var target = this.resolveID(name);
|
||
if (target.get('type') !== 'branch') {
|
||
throw new GitError({
|
||
msg: "You can't delete things that arent branches with branch command"
|
||
});
|
||
}
|
||
if (target.get('id') == 'master') {
|
||
throw new GitError({
|
||
msg: "You can't delete the master branch!"
|
||
});
|
||
}
|
||
if (this.HEAD.get('target') === target) {
|
||
throw new GitError({
|
||
msg: "Cannot delete the branch you are currently on"
|
||
});
|
||
}
|
||
|
||
// now we know it's a branch
|
||
var branch = target;
|
||
|
||
this.branchCollection.remove(branch);
|
||
this.refs[branch.get('id')] = undefined;
|
||
delete this.refs[branch.get('id')];
|
||
|
||
if (branch.get('visBranch')) {
|
||
branch.get('visBranch').remove();
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.unescapeQuotes = function(str) {
|
||
return str.replace(/'/g, "'");
|
||
};
|
||
|
||
GitEngine.prototype.filterError = function(err) {
|
||
if (!(err instanceof GitError ||
|
||
err instanceof CommandResult)) {
|
||
throw err;
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.dispatch = function(command, deferred) {
|
||
// current command, options, and args are stored in the gitEngine
|
||
// for easy reference during processing.
|
||
this.command = command;
|
||
this.commandOptions = command.get('supportedMap');
|
||
this.generalArgs = command.get('generalArgs');
|
||
|
||
// set up the animation queue
|
||
var whenDone = _.bind(function() {
|
||
command.finishWith(deferred);
|
||
}, this);
|
||
this.animationQueue = new AnimationQueue({
|
||
callback: whenDone
|
||
});
|
||
|
||
try {
|
||
var methodName = command.get('method').replace(/-/g, '') + 'Starter';
|
||
this[methodName]();
|
||
} catch (err) {
|
||
this.filterError(err);
|
||
// short circuit animation by just setting error and returning
|
||
command.set('error', err);
|
||
deferred.resolve();
|
||
return;
|
||
}
|
||
|
||
// only add the refresh if we didn't do manual animations
|
||
if (!this.animationQueue.get('animations').length && !this.animationQueue.get('defer')) {
|
||
this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
|
||
}
|
||
|
||
// animation queue will call the callback when its done
|
||
if (!this.animationQueue.get('defer')) {
|
||
this.animationQueue.start();
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.showStarter = function() {
|
||
this.oneArgImpliedHead(this.generalArgs);
|
||
|
||
this.show(this.generalArgs[0]);
|
||
};
|
||
|
||
GitEngine.prototype.show = function(ref) {
|
||
var commit = this.getCommitFromRef(ref);
|
||
|
||
throw new CommandResult({
|
||
msg: commit.getShowEntry()
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.statusStarter = function() {
|
||
var lines = [];
|
||
if (this.getDetachedHead()) {
|
||
lines.push('Detached Head!');
|
||
} else {
|
||
var branchName = this.HEAD.get('target').get('id');
|
||
lines.push('On branch ' + branchName);
|
||
}
|
||
lines.push('Changes to be committed:');
|
||
lines.push('');
|
||
lines.push(' modified: cal/OskiCostume.stl');
|
||
lines.push('');
|
||
lines.push('Ready to commit! (as always in this demo)');
|
||
|
||
var msg = '';
|
||
_.each(lines, function(line) {
|
||
msg += '# ' + line + '\n';
|
||
});
|
||
|
||
throw new CommandResult({
|
||
msg: msg
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.logStarter = function() {
|
||
if (this.generalArgs.length == 2) {
|
||
// do fancy git log branchA ^branchB
|
||
if (this.generalArgs[1][0] == '^') {
|
||
this.logWithout(this.generalArgs[0], this.generalArgs[1]);
|
||
} else {
|
||
throw new GitError({
|
||
msg: 'I need a not branch (^branchName) when getting two arguments!'
|
||
});
|
||
}
|
||
}
|
||
|
||
this.oneArgImpliedHead(this.generalArgs);
|
||
this.log(this.generalArgs[0]);
|
||
};
|
||
|
||
GitEngine.prototype.logWithout = function(ref, omitBranch) {
|
||
// slice off the ^branch
|
||
omitBranch = omitBranch.slice(1);
|
||
this.log(ref, this.getUpstreamSet(omitBranch));
|
||
};
|
||
|
||
GitEngine.prototype.log = function(ref, omitSet) {
|
||
// omit set is for doing stuff like git log branchA ^branchB
|
||
omitSet = omitSet || {};
|
||
// first get the commit we referenced
|
||
var commit = this.getCommitFromRef(ref);
|
||
|
||
// then get as many far back as we can from here, order by commit date
|
||
var toDump = [];
|
||
var pQueue = [commit];
|
||
|
||
var seen = {};
|
||
|
||
while (pQueue.length) {
|
||
var popped = pQueue.shift(0);
|
||
if (seen[popped.get('id')] || omitSet[popped.get('id')]) {
|
||
continue;
|
||
}
|
||
seen[popped.get('id')] = true;
|
||
|
||
toDump.push(popped);
|
||
|
||
if (popped.get('parents') && popped.get('parents').length) {
|
||
pQueue = pQueue.concat(popped.get('parents'));
|
||
}
|
||
}
|
||
|
||
// now go through and collect logs
|
||
var bigLogStr = '';
|
||
_.each(toDump, function(c) {
|
||
bigLogStr += c.getLogEntry();
|
||
}, this);
|
||
|
||
throw new CommandResult({
|
||
msg: bigLogStr
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.addStarter = function() {
|
||
throw new CommandResult({
|
||
msg: "This demo is meant to demonstrate git branching, so don't worry about " +
|
||
"adding / staging files. Just go ahead and commit away!"
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.getCommonAncestor = function(ancestor, cousin) {
|
||
if (this.isUpstreamOf(cousin, ancestor)) {
|
||
throw new Error('Dont use common ancestor if we are upstream!');
|
||
}
|
||
|
||
var upstreamSet = this.getUpstreamSet(ancestor);
|
||
// now BFS off of cousin until you find something
|
||
|
||
var queue = [this.getCommitFromRef(cousin)];
|
||
while (queue.length) {
|
||
var here = queue.pop();
|
||
if (upstreamSet[here.get('id')]) {
|
||
return here;
|
||
}
|
||
queue = queue.concat(here.get('parents'));
|
||
}
|
||
throw new Error('something has gone very wrong... two nodes arent connected!');
|
||
};
|
||
|
||
GitEngine.prototype.isUpstreamOf = function(child, ancestor) {
|
||
child = this.getCommitFromRef(child);
|
||
|
||
// basically just do a completely BFS search on ancestor to the root, then
|
||
// check for membership of child in that set of explored nodes
|
||
var upstream = this.getUpstreamSet(ancestor);
|
||
return upstream[child.get('id')] !== undefined;
|
||
};
|
||
|
||
GitEngine.prototype.getUpstreamSet = function(ancestor) {
|
||
var commit = this.getCommitFromRef(ancestor);
|
||
var ancestorID = commit.get('id');
|
||
var queue = [commit];
|
||
|
||
var exploredSet = {};
|
||
exploredSet[ancestorID] = true;
|
||
|
||
var addToExplored = function(rent) {
|
||
exploredSet[rent.get('id')] = true;
|
||
queue.push(rent);
|
||
};
|
||
|
||
while (queue.length) {
|
||
var here = queue.pop();
|
||
var rents = here.get('parents');
|
||
|
||
_.each(rents, addToExplored);
|
||
}
|
||
return exploredSet;
|
||
};
|
||
|
||
|
||
var Ref = Backbone.Model.extend({
|
||
initialize: function() {
|
||
if (!this.get('target')) {
|
||
throw new Error('must be initialized with target');
|
||
}
|
||
if (!this.get('id')) {
|
||
throw new Error('must be given an id');
|
||
}
|
||
this.set('type', 'general ref');
|
||
|
||
if (this.get('id') == 'HEAD') {
|
||
this.set('lastLastTarget', null);
|
||
this.set('lastTarget', this.get('target'));
|
||
// have HEAD remember where it is for checkout -
|
||
this.on('change:target', this.targetChanged, this);
|
||
}
|
||
},
|
||
|
||
targetChanged: function(model, targetValue, ev) {
|
||
// push our little 3 stack back. we need to do this because
|
||
// backbone doesn't give you what the value WAS, only what it was changed
|
||
// TO
|
||
this.set('lastLastTarget', this.get('lastTarget'));
|
||
this.set('lastTarget', targetValue);
|
||
},
|
||
|
||
toString: function() {
|
||
return 'a ' + this.get('type') + 'pointing to ' + String(this.get('target'));
|
||
}
|
||
});
|
||
|
||
var Branch = Ref.extend({
|
||
defaults: {
|
||
visBranch: null
|
||
},
|
||
|
||
initialize: function() {
|
||
Ref.prototype.initialize.call(this);
|
||
this.set('type', 'branch');
|
||
}
|
||
});
|
||
|
||
var Commit = Backbone.Model.extend({
|
||
defaults: {
|
||
type: 'commit',
|
||
children: null,
|
||
parents: null,
|
||
author: 'Peter Cottle',
|
||
createTime: null,
|
||
commitMessage: null,
|
||
visNode: null,
|
||
gitVisuals: null
|
||
},
|
||
|
||
constants: {
|
||
circularFields: ['gitVisuals', 'visNode', 'children']
|
||
},
|
||
|
||
getLogEntry: function() {
|
||
// for now we are just joining all these things with newlines which
|
||
// will get placed by paragraph tags. Not really a fan of this, but
|
||
// it's better than making an entire template and all that jazz
|
||
return [
|
||
'Author: ' + this.get('author'),
|
||
'Date: ' + this.get('createTime'),
|
||
'<br/>',
|
||
this.get('commitMessage'),
|
||
'<br/>',
|
||
'Commit: ' + this.get('id')
|
||
].join('\n' ) + '\n';
|
||
},
|
||
|
||
getShowEntry: function() {
|
||
// same deal as above, show log entry and some fake changes
|
||
return [
|
||
this.getLogEntry(),
|
||
'diff --git a/bigGameResults.html b/bigGameResults.html',
|
||
'--- bigGameResults.html',
|
||
'+++ bigGameResults.html',
|
||
'@@ 13,27 @@ Winner, Score',
|
||
'- Stanfurd, 14-7',
|
||
'+ Cal, 21-14'
|
||
].join('\n') + '\n';
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (!this.get('id')) {
|
||
throw new Error('Need ID!!');
|
||
}
|
||
|
||
if (!this.get('createTime')) {
|
||
this.set('createTime', new Date().toString());
|
||
}
|
||
if (!this.get('commitMessage')) {
|
||
this.set('commitMessage', 'Quick Commit. Go Bears!');
|
||
}
|
||
|
||
this.set('children', []);
|
||
|
||
// root commits have no parents
|
||
if (!this.get('rootCommit')) {
|
||
if (!this.get('parents') || !this.get('parents').length) {
|
||
throw new Error('needs parents');
|
||
}
|
||
}
|
||
},
|
||
|
||
addNodeToVisuals: function() {
|
||
var visNode = this.get('gitVisuals').addNode(this.get('id'), this);
|
||
this.set('visNode', visNode);
|
||
},
|
||
|
||
addEdgeToVisuals: function(parent) {
|
||
this.get('gitVisuals').addEdge(this.get('id'), parent.get('id'));
|
||
},
|
||
|
||
isMainParent: function(parent) {
|
||
var index = this.get('parents').indexOf(parent);
|
||
return index === 0;
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.validateAtInit();
|
||
this.addNodeToVisuals();
|
||
|
||
_.each(this.get('parents'), function(parent) {
|
||
parent.get('children').push(this);
|
||
this.addEdgeToVisuals(parent);
|
||
}, this);
|
||
}
|
||
});
|
||
|
||
exports.GitEngine = GitEngine;
|
||
exports.Commit = Commit;
|
||
exports.Branch = Branch;
|
||
exports.Ref = Ref;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/visuals/animation/animationFactory.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var Animation = require('./index').Animation;
|
||
var GRAPHICS = require('../../util/constants').GRAPHICS;
|
||
|
||
/******************
|
||
* This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time.
|
||
* The tricky thing is that when a new commit has to be "born," say in the middle of a rebase
|
||
* or something, it must animate out from the parent position to it's birth position.
|
||
|
||
* These two positions though may not be where the commit finally ends up. So we actually need to take a snapshot of the tree,
|
||
* store all those positions, take a snapshot of the tree after a layout refresh afterwards, and then animate between those two spots.
|
||
* and then essentially animate the entire tree too.
|
||
*/
|
||
|
||
// essentially a static class
|
||
var AnimationFactory = function() {
|
||
|
||
};
|
||
|
||
AnimationFactory.prototype.genCommitBirthAnimation = function(animationQueue, commit, gitVisuals) {
|
||
if (!animationQueue) {
|
||
throw new Error("Need animation queue to add closure to!");
|
||
}
|
||
|
||
var time = GRAPHICS.defaultAnimationTime * 1.0;
|
||
var bounceTime = time * 2;
|
||
|
||
// essentially refresh the entire tree, but do a special thing for the commit
|
||
var visNode = commit.get('visNode');
|
||
|
||
var animation = function() {
|
||
// this takes care of refs and all that jazz, and updates all the positions
|
||
gitVisuals.refreshTree(time);
|
||
|
||
visNode.setBirth();
|
||
visNode.parentInFront();
|
||
gitVisuals.visBranchesFront();
|
||
|
||
visNode.animateUpdatedPosition(bounceTime, 'bounce');
|
||
visNode.animateOutgoingEdges(time);
|
||
};
|
||
|
||
animationQueue.add(new Animation({
|
||
closure: animation,
|
||
duration: Math.max(time, bounceTime)
|
||
}));
|
||
};
|
||
|
||
AnimationFactory.prototype.overrideOpacityDepth2 = function(attr, opacity) {
|
||
opacity = (opacity === undefined) ? 1 : opacity;
|
||
|
||
var newAttr = {};
|
||
|
||
_.each(attr, function(partObj, partName) {
|
||
newAttr[partName] = {};
|
||
_.each(partObj, function(val, key) {
|
||
if (key == 'opacity') {
|
||
newAttr[partName][key] = opacity;
|
||
} else {
|
||
newAttr[partName][key] = val;
|
||
}
|
||
});
|
||
});
|
||
return newAttr;
|
||
};
|
||
|
||
AnimationFactory.prototype.overrideOpacityDepth3 = function(snapShot, opacity) {
|
||
var newSnap = {};
|
||
|
||
_.each(snapShot, function(visObj, visID) {
|
||
newSnap[visID] = this.overrideOpacityDepth2(visObj, opacity);
|
||
}, this);
|
||
return newSnap;
|
||
};
|
||
|
||
AnimationFactory.prototype.genCommitBirthClosureFromSnapshot = function(step, gitVisuals) {
|
||
var time = GRAPHICS.defaultAnimationTime * 1.0;
|
||
var bounceTime = time * 1.5;
|
||
|
||
var visNode = step.newCommit.get('visNode');
|
||
var afterAttrWithOpacity = this.overrideOpacityDepth2(step.afterSnapshot[visNode.getID()]);
|
||
var afterSnapWithOpacity = this.overrideOpacityDepth3(step.afterSnapshot);
|
||
|
||
var animation = function() {
|
||
visNode.setBirthFromSnapshot(step.beforeSnapshot);
|
||
visNode.parentInFront();
|
||
gitVisuals.visBranchesFront();
|
||
|
||
visNode.animateToAttr(afterAttrWithOpacity, bounceTime, 'bounce');
|
||
visNode.animateOutgoingEdgesToAttr(afterSnapWithOpacity, bounceTime);
|
||
};
|
||
|
||
return animation;
|
||
};
|
||
|
||
AnimationFactory.prototype.refreshTree = function(animationQueue, gitVisuals) {
|
||
animationQueue.add(new Animation({
|
||
closure: function() {
|
||
gitVisuals.refreshTree();
|
||
}
|
||
}));
|
||
};
|
||
|
||
AnimationFactory.prototype.rebaseAnimation = function(animationQueue, rebaseResponse,
|
||
gitEngine, gitVisuals) {
|
||
|
||
this.rebaseHighlightPart(animationQueue, rebaseResponse, gitEngine);
|
||
this.rebaseBirthPart(animationQueue, rebaseResponse, gitEngine, gitVisuals);
|
||
};
|
||
|
||
AnimationFactory.prototype.rebaseHighlightPart = function(animationQueue, rebaseResponse, gitEngine) {
|
||
var fullTime = GRAPHICS.defaultAnimationTime * 0.66;
|
||
var slowTime = fullTime * 2.0;
|
||
|
||
// we want to highlight all the old commits
|
||
var oldCommits = rebaseResponse.toRebaseArray;
|
||
// we are either highlighting to a visBranch or a visNode
|
||
var visBranch = rebaseResponse.destinationBranch.get('visBranch');
|
||
if (!visBranch) {
|
||
// in the case where we rebase onto a commit
|
||
visBranch = rebaseResponse.destinationBranch.get('visNode');
|
||
}
|
||
|
||
_.each(oldCommits, function(oldCommit) {
|
||
var visNode = oldCommit.get('visNode');
|
||
animationQueue.add(new Animation({
|
||
closure: function() {
|
||
visNode.highlightTo(visBranch, slowTime, 'easeInOut');
|
||
},
|
||
duration: fullTime * 1.5
|
||
}));
|
||
|
||
}, this);
|
||
|
||
this.delay(animationQueue, fullTime * 2);
|
||
};
|
||
|
||
AnimationFactory.prototype.rebaseBirthPart = function(animationQueue, rebaseResponse,
|
||
gitEngine, gitVisuals) {
|
||
var rebaseSteps = rebaseResponse.rebaseSteps;
|
||
|
||
var newVisNodes = [];
|
||
_.each(rebaseSteps, function(step) {
|
||
var visNode = step.newCommit.get('visNode');
|
||
|
||
newVisNodes.push(visNode);
|
||
visNode.setOpacity(0);
|
||
visNode.setOutgoingEdgesOpacity(0);
|
||
}, this);
|
||
|
||
var previousVisNodes = [];
|
||
_.each(rebaseSteps, function(rebaseStep, index) {
|
||
var toOmit = newVisNodes.slice(index + 1);
|
||
|
||
var snapshotPart = this.genFromToSnapshotAnimation(
|
||
rebaseStep.beforeSnapshot,
|
||
rebaseStep.afterSnapshot,
|
||
toOmit,
|
||
previousVisNodes,
|
||
gitVisuals
|
||
);
|
||
var birthPart = this.genCommitBirthClosureFromSnapshot(rebaseStep, gitVisuals);
|
||
|
||
var animation = function() {
|
||
snapshotPart();
|
||
birthPart();
|
||
};
|
||
|
||
animationQueue.add(new Animation({
|
||
closure: animation,
|
||
duration: GRAPHICS.defaultAnimationTime * 1.5
|
||
}));
|
||
|
||
previousVisNodes.push(rebaseStep.newCommit.get('visNode'));
|
||
}, this);
|
||
|
||
// need to delay to let bouncing finish
|
||
this.delay(animationQueue);
|
||
|
||
this.refreshTree(animationQueue, gitVisuals);
|
||
};
|
||
|
||
AnimationFactory.prototype.delay = function(animationQueue, time) {
|
||
time = time || GRAPHICS.defaultAnimationTime;
|
||
animationQueue.add(new Animation({
|
||
closure: function() { },
|
||
duration: time
|
||
}));
|
||
};
|
||
|
||
AnimationFactory.prototype.genSetAllCommitOpacities = function(visNodes, opacity) {
|
||
// need to slice for closure
|
||
var nodesToAnimate = visNodes.slice(0);
|
||
|
||
return function() {
|
||
_.each(nodesToAnimate, function(visNode) {
|
||
visNode.setOpacity(opacity);
|
||
visNode.setOutgoingEdgesOpacity(opacity);
|
||
});
|
||
};
|
||
};
|
||
|
||
AnimationFactory.prototype.stripObjectsFromSnapshot = function(snapShot, toOmit) {
|
||
var ids = [];
|
||
_.each(toOmit, function(obj) {
|
||
ids.push(obj.getID());
|
||
});
|
||
|
||
var newSnapshot = {};
|
||
_.each(snapShot, function(val, key) {
|
||
if (_.include(ids, key)) {
|
||
// omit
|
||
return;
|
||
}
|
||
newSnapshot[key] = val;
|
||
}, this);
|
||
return newSnapshot;
|
||
};
|
||
|
||
AnimationFactory.prototype.genFromToSnapshotAnimation = function(
|
||
beforeSnapshot,
|
||
afterSnapshot,
|
||
commitsToOmit,
|
||
commitsToFixOpacity,
|
||
gitVisuals) {
|
||
|
||
// we want to omit the commit outgoing edges
|
||
var toOmit = [];
|
||
_.each(commitsToOmit, function(visNode) {
|
||
toOmit.push(visNode);
|
||
toOmit = toOmit.concat(visNode.get('outgoingEdges'));
|
||
});
|
||
|
||
var fixOpacity = function(obj) {
|
||
if (!obj) { return; }
|
||
_.each(obj, function(attr, partName) {
|
||
obj[partName].opacity = 1;
|
||
});
|
||
};
|
||
|
||
// HORRIBLE loop to fix opacities all throughout the snapshot
|
||
_.each([beforeSnapshot, afterSnapshot], function(snapShot) {
|
||
_.each(commitsToFixOpacity, function(visNode) {
|
||
fixOpacity(snapShot[visNode.getID()]);
|
||
_.each(visNode.get('outgoingEdges'), function(visEdge) {
|
||
fixOpacity(snapShot[visEdge.getID()]);
|
||
});
|
||
});
|
||
});
|
||
|
||
return function() {
|
||
gitVisuals.animateAllFromAttrToAttr(beforeSnapshot, afterSnapshot, toOmit);
|
||
};
|
||
};
|
||
|
||
exports.AnimationFactory = AnimationFactory;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/visuals/animation/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var GLOBAL = require('../../util/constants').GLOBAL;
|
||
|
||
var Animation = Backbone.Model.extend({
|
||
defaults: {
|
||
duration: 300,
|
||
closure: null
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (!this.get('closure')) {
|
||
throw new Error('give me a closure!');
|
||
}
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.validateAtInit();
|
||
},
|
||
|
||
run: function() {
|
||
this.get('closure')();
|
||
}
|
||
});
|
||
|
||
var AnimationQueue = Backbone.Model.extend({
|
||
defaults: {
|
||
animations: null,
|
||
index: 0,
|
||
callback: null,
|
||
defer: false
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.set('animations', []);
|
||
if (!options.callback) {
|
||
console.warn('no callback');
|
||
}
|
||
},
|
||
|
||
add: function(animation) {
|
||
if (!animation instanceof Animation) {
|
||
throw new Error("Need animation not something else");
|
||
}
|
||
|
||
this.get('animations').push(animation);
|
||
},
|
||
|
||
start: function() {
|
||
this.set('index', 0);
|
||
|
||
// set the global lock that we are animating
|
||
GLOBAL.isAnimating = true;
|
||
this.next();
|
||
},
|
||
|
||
finish: function() {
|
||
// release lock here
|
||
GLOBAL.isAnimating = false;
|
||
this.get('callback')();
|
||
},
|
||
|
||
next: function() {
|
||
// ok so call the first animation, and then set a timeout to call the next.
|
||
// since an animation is defined as taking a specific amount of time,
|
||
// we can simply just use timeouts rather than promises / deferreds.
|
||
// for graphical displays that require an unknown amount of time, use deferreds
|
||
// but not animation queue (see the finishAnimation for that)
|
||
var animations = this.get('animations');
|
||
var index = this.get('index');
|
||
if (index >= animations.length) {
|
||
this.finish();
|
||
return;
|
||
}
|
||
|
||
var next = animations[index];
|
||
var duration = next.get('duration');
|
||
|
||
next.run();
|
||
|
||
this.set('index', index + 1);
|
||
setTimeout(_.bind(function() {
|
||
this.next();
|
||
}, this), duration);
|
||
}
|
||
});
|
||
|
||
exports.Animation = Animation;
|
||
exports.AnimationQueue = AnimationQueue;
|
||
|
||
});
|
||
|
||
require.define("/src/js/git/treeCompare.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
// static class...
|
||
function TreeCompare() {
|
||
|
||
}
|
||
|
||
TreeCompare.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) {
|
||
var result = true;
|
||
_.each(branches, function(branchName) {
|
||
result = result && this.compareBranchWithinTrees(treeA, treeB, branchName);
|
||
}, this);
|
||
|
||
return result;
|
||
};
|
||
|
||
TreeCompare.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) {
|
||
treeA = this.convertTreeSafe(treeA);
|
||
treeB = this.convertTreeSafe(treeB);
|
||
|
||
this.reduceTreeFields([treeA, treeB]);
|
||
|
||
// we need a recursive comparison function to bubble up the branch
|
||
var recurseCompare = function(commitA, commitB) {
|
||
// this is the short-circuit base case
|
||
var result = _.isEqual(commitA, commitB);
|
||
if (!result) {
|
||
return false;
|
||
}
|
||
|
||
// we loop through each parent ID. we sort the parent ID's beforehand
|
||
// so the index lookup is valid
|
||
_.each(commitA.parents, function(pAid, index) {
|
||
var pBid = commitB.parents[index];
|
||
|
||
var childA = treeA.commits[pAid];
|
||
var childB = treeB.commits[pBid];
|
||
|
||
result = result && recurseCompare(childA, childB);
|
||
}, this);
|
||
// if each of our children recursively are equal, we are good
|
||
return result;
|
||
};
|
||
|
||
var branchA = treeA.branches[branchName];
|
||
var branchB = treeB.branches[branchName];
|
||
|
||
return _.isEqual(branchA, branchB) &&
|
||
recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]);
|
||
};
|
||
|
||
TreeCompare.prototype.convertTreeSafe = function(tree) {
|
||
if (typeof tree == 'string') {
|
||
return JSON.parse(unescape(tree));
|
||
}
|
||
return tree;
|
||
};
|
||
|
||
TreeCompare.prototype.reduceTreeFields = function(trees) {
|
||
var commitSaveFields = [
|
||
'parents',
|
||
'id',
|
||
'rootCommit'
|
||
];
|
||
var commitSortFields = ['children', 'parents'];
|
||
var branchSaveFields = [
|
||
'target',
|
||
'id'
|
||
];
|
||
|
||
// this function saves only the specified fields of a tree
|
||
var saveOnly = function(tree, treeKey, saveFields, sortFields) {
|
||
var objects = tree[treeKey];
|
||
_.each(objects, function(obj, objKey) {
|
||
// our blank slate to copy over
|
||
var blank = {};
|
||
_.each(saveFields, function(field) {
|
||
if (obj[field] !== undefined) {
|
||
blank[field] = obj[field];
|
||
}
|
||
});
|
||
|
||
_.each(sortFields, function(field) {
|
||
// also sort some fields
|
||
if (obj[field]) {
|
||
obj[field].sort();
|
||
blank[field] = obj[field];
|
||
}
|
||
});
|
||
tree[treeKey][objKey] = blank;
|
||
});
|
||
};
|
||
|
||
_.each(trees, function(tree) {
|
||
saveOnly(tree, 'commits', commitSaveFields, commitSortFields);
|
||
saveOnly(tree, 'branches', branchSaveFields);
|
||
|
||
tree.HEAD = {
|
||
target: tree.HEAD.target,
|
||
id: tree.HEAD.id
|
||
};
|
||
});
|
||
};
|
||
|
||
TreeCompare.prototype.compareTrees = function(treeA, treeB) {
|
||
treeA = this.convertTreeSafe(treeA);
|
||
treeB = this.convertTreeSafe(treeB);
|
||
|
||
// now we need to strip out the fields we don't care about, aka things
|
||
// like createTime, message, author
|
||
this.reduceTreeFields([treeA, treeB]);
|
||
|
||
console.log('comparing tree A', treeA, 'to', treeB);
|
||
|
||
return _.isEqual(treeA, treeB);
|
||
};
|
||
|
||
exports.TreeCompare = TreeCompare;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/util/errors.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var MyError = Backbone.Model.extend({
|
||
defaults: {
|
||
type: 'MyError',
|
||
msg: 'Unknown Error'
|
||
},
|
||
toString: function() {
|
||
return this.get('type') + ': ' + this.get('msg');
|
||
},
|
||
|
||
getMsg: function() {
|
||
return this.get('msg') || 'Unknown Error';
|
||
},
|
||
|
||
toResult: function() {
|
||
if (!this.get('msg').length) {
|
||
return '';
|
||
}
|
||
return '<p>' + this.get('msg').replace(/\n/g, '</p><p>') + '</p>';
|
||
}
|
||
});
|
||
|
||
var CommandProcessError = exports.CommandProcessError = MyError.extend({
|
||
defaults: {
|
||
type: 'Command Process Error'
|
||
}
|
||
});
|
||
|
||
var CommandResult = exports.CommandResult = MyError.extend({
|
||
defaults: {
|
||
type: 'Command Result'
|
||
}
|
||
});
|
||
|
||
var Warning = exports.Warning = MyError.extend({
|
||
defaults: {
|
||
type: 'Warning'
|
||
}
|
||
});
|
||
|
||
var GitError = exports.GitError = MyError.extend({
|
||
defaults: {
|
||
type: 'Git Error'
|
||
}
|
||
});
|
||
|
||
var filterError = function(err) {
|
||
if (err instanceof CommandProcessError ||
|
||
err instanceof GitError ||
|
||
err instanceof CommandResult ||
|
||
err instanceof Warning) {
|
||
// yay! one of ours
|
||
return;
|
||
} else {
|
||
throw err;
|
||
}
|
||
};
|
||
|
||
exports.filterError = filterError;
|
||
|
||
});
|
||
|
||
require.define("/src/js/views/rebaseView.js",function(require,module,exports,__dirname,__filename,process,global){var GitError = require('../util/errors').GitError;
|
||
var _ = require('underscore');
|
||
var Q = require('q');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;
|
||
|
||
var ModalTerminal = require('../views').ModalTerminal;
|
||
var ContainedBase = require('../views').ContainedBase;
|
||
var ConfirmCancelView = require('../views').ConfirmCancelView;
|
||
var LeftRightView = require('../views').LeftRightView;
|
||
|
||
var InteractiveRebaseView = ContainedBase.extend({
|
||
tagName: 'div',
|
||
template: _.template($('#interactive-rebase-template').html()),
|
||
|
||
initialize: function(options) {
|
||
this.deferred = options.deferred;
|
||
this.rebaseMap = {};
|
||
this.entryObjMap = {};
|
||
|
||
this.rebaseEntries = new RebaseEntryCollection();
|
||
_.each(options.toRebase, function(commit) {
|
||
var id = commit.get('id');
|
||
this.rebaseMap[id] = commit;
|
||
|
||
// make basic models for each commit
|
||
this.entryObjMap[id] = new RebaseEntry({
|
||
id: id
|
||
});
|
||
this.rebaseEntries.add(this.entryObjMap[id]);
|
||
}, this);
|
||
|
||
this.container = new ModalTerminal({
|
||
title: 'Interactive Rebase'
|
||
});
|
||
this.render();
|
||
|
||
// show the dialog holder
|
||
this.show();
|
||
},
|
||
|
||
confirm: function() {
|
||
this.die();
|
||
|
||
// get our ordering
|
||
var uiOrder = [];
|
||
this.$('ul.rebaseEntries li').each(function(i, obj) {
|
||
uiOrder.push(obj.id);
|
||
});
|
||
|
||
// now get the real array
|
||
var toRebase = [];
|
||
_.each(uiOrder, function(id) {
|
||
// the model pick check
|
||
if (this.entryObjMap[id].get('pick')) {
|
||
toRebase.unshift(this.rebaseMap[id]);
|
||
}
|
||
}, this);
|
||
|
||
toRebase.reverse();
|
||
this.deferred.resolve(toRebase);
|
||
// garbage collection will get us
|
||
this.$el.html('');
|
||
},
|
||
|
||
render: function() {
|
||
var json = {
|
||
num: _.keys(this.rebaseMap).length
|
||
};
|
||
|
||
var destination = this.container.getInsideElement();
|
||
this.$el.html(this.template(json));
|
||
$(destination).append(this.el);
|
||
|
||
// also render each entry
|
||
var listHolder = this.$('ul.rebaseEntries');
|
||
this.rebaseEntries.each(function(entry) {
|
||
new RebaseEntryView({
|
||
el: listHolder,
|
||
model: entry
|
||
});
|
||
}, this);
|
||
|
||
// then make it reorderable..
|
||
listHolder.sortable({
|
||
axis: 'y',
|
||
placeholder: 'rebaseEntry transitionOpacity ui-state-highlight',
|
||
appendTo: 'parent'
|
||
});
|
||
|
||
this.makeButtons();
|
||
},
|
||
|
||
makeButtons: function() {
|
||
// control for button
|
||
var deferred = Q.defer();
|
||
deferred.promise
|
||
.then(_.bind(function() {
|
||
this.confirm();
|
||
}, this))
|
||
.fail(_.bind(function() {
|
||
// empty array does nothing, just like in git
|
||
this.hide();
|
||
this.deferred.resolve([]);
|
||
}, this))
|
||
.done();
|
||
|
||
// finally get our buttons
|
||
new ConfirmCancelView({
|
||
destination: this.$('.confirmCancel'),
|
||
deferred: deferred
|
||
});
|
||
}
|
||
});
|
||
|
||
var RebaseEntry = Backbone.Model.extend({
|
||
defaults: {
|
||
pick: true
|
||
},
|
||
|
||
toggle: function() {
|
||
this.set('pick', !this.get('pick'));
|
||
}
|
||
});
|
||
|
||
var RebaseEntryCollection = Backbone.Collection.extend({
|
||
model: RebaseEntry
|
||
});
|
||
|
||
var RebaseEntryView = Backbone.View.extend({
|
||
tagName: 'li',
|
||
template: _.template($('#interactive-rebase-entry-template').html()),
|
||
|
||
toggle: function() {
|
||
this.model.toggle();
|
||
|
||
// toggle a class also
|
||
this.listEntry.toggleClass('notPicked', !this.model.get('pick'));
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.render();
|
||
},
|
||
|
||
render: function() {
|
||
var json = this.model.toJSON();
|
||
this.$el.append(this.template(this.model.toJSON()));
|
||
|
||
// hacky :( who would have known jquery barfs on ids with %'s and quotes
|
||
this.listEntry = this.$el.children(':last');
|
||
|
||
this.listEntry.delegate('#toggleButton', 'click', _.bind(function() {
|
||
this.toggle();
|
||
}, this));
|
||
}
|
||
});
|
||
|
||
exports.InteractiveRebaseView = InteractiveRebaseView;
|
||
|
||
});
|
||
|
||
require.define("/src/js/views/index.js",function(require,module,exports,__dirname,__filename,process,global){var GitError = require('../util/errors').GitError;
|
||
var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;
|
||
|
||
var Main = require('../app');
|
||
var Constants = require('../util/constants');
|
||
|
||
var BaseView = Backbone.View.extend({
|
||
getDestination: function() {
|
||
return this.destination || this.container.getInsideElement();
|
||
},
|
||
|
||
tearDown: function() {
|
||
this.$el.html('');
|
||
if (this.container) {
|
||
this.container.tearDown();
|
||
}
|
||
},
|
||
|
||
render: function(HTML) {
|
||
// flexibility
|
||
var destination = this.getDestination();
|
||
HTML = HTML || this.template(this.JSON);
|
||
|
||
this.$el.html(HTML);
|
||
$(destination).append(this.el);
|
||
}
|
||
});
|
||
|
||
var ResolveRejectBase = BaseView.extend({
|
||
resolve: function() {
|
||
this.deferred.resolve();
|
||
},
|
||
|
||
reject: function() {
|
||
this.deferred.reject();
|
||
}
|
||
});
|
||
|
||
var PositiveNegativeBase = BaseView.extend({
|
||
positive: function() {
|
||
this.navEvents.trigger('positive');
|
||
},
|
||
|
||
negative: function() {
|
||
this.navEvents.trigger('negative');
|
||
}
|
||
});
|
||
|
||
var ContainedBase = BaseView.extend({
|
||
getAnimationTime: function() { return 700; },
|
||
|
||
show: function() {
|
||
this.container.show();
|
||
},
|
||
|
||
hide: function() {
|
||
this.container.hide();
|
||
},
|
||
|
||
die: function() {
|
||
this.hide();
|
||
setTimeout(_.bind(function() {
|
||
this.tearDown();
|
||
}, this), this.getAnimationTime() * 1.1);
|
||
}
|
||
});
|
||
|
||
var ConfirmCancelView = ResolveRejectBase.extend({
|
||
tagName: 'div',
|
||
className: 'confirmCancelView box horizontal justify',
|
||
template: _.template($('#confirm-cancel-template').html()),
|
||
events: {
|
||
'click .confirmButton': 'resolve',
|
||
'click .cancelButton': 'reject'
|
||
},
|
||
|
||
initialize: function(options) {
|
||
if (!options.destination || !options.deferred) {
|
||
throw new Error('needmore');
|
||
}
|
||
|
||
this.destination = options.destination;
|
||
this.deferred = options.deferred;
|
||
this.JSON = {
|
||
confirm: options.confirm || 'Confirm',
|
||
cancel: options.cancel || 'Cancel'
|
||
};
|
||
|
||
this.render();
|
||
}
|
||
});
|
||
|
||
var LeftRightView = PositiveNegativeBase.extend({
|
||
tagName: 'div',
|
||
className: 'leftRightView box horizontal center',
|
||
template: _.template($('#left-right-template').html()),
|
||
events: {
|
||
'click .left': 'negative',
|
||
'click .right': 'positive'
|
||
},
|
||
|
||
initialize: function(options) {
|
||
if (!options.destination || !options.events) {
|
||
throw new Error('needmore');
|
||
}
|
||
|
||
this.destination = options.destination;
|
||
this.navEvents = options.events;
|
||
this.JSON = {
|
||
showLeft: (options.showLeft === undefined) ? true : options.showLeft,
|
||
lastNav: (options.lastNav === undefined) ? false : options.lastNav
|
||
};
|
||
|
||
this.render();
|
||
}
|
||
});
|
||
|
||
var ModalView = Backbone.View.extend({
|
||
tagName: 'div',
|
||
className: 'modalView box horizontal center transitionOpacityLinear',
|
||
template: _.template($('#modal-view-template').html()),
|
||
|
||
getAnimationTime: function() { return 700; },
|
||
|
||
initialize: function(options) {
|
||
this.shown = false;
|
||
this.render();
|
||
this.stealKeyboard();
|
||
},
|
||
|
||
render: function() {
|
||
// add ourselves to the DOM
|
||
this.$el.html(this.template({}));
|
||
$('body').append(this.el);
|
||
// this doesnt necessarily show us though...
|
||
},
|
||
|
||
stealKeyboard: function() {
|
||
Main.getEventBaton().stealBaton('keydown', this.onKeyDown, this);
|
||
Main.getEventBaton().stealBaton('keyup', this.onKeyUp, this);
|
||
Main.getEventBaton().stealBaton('windowFocus', this.onWindowFocus, this);
|
||
Main.getEventBaton().stealBaton('documentClick', this.onDocumentClick, this);
|
||
|
||
// blur the text input field so keydown events will not be caught by our
|
||
// preventDefaulters, allowing people to still refresh and launch inspector (etc)
|
||
$('#commandTextField').blur();
|
||
},
|
||
|
||
releaseKeyboard: function() {
|
||
Main.getEventBaton().releaseBaton('keydown', this.onKeyDown, this);
|
||
Main.getEventBaton().releaseBaton('keyup', this.onKeyUp, this);
|
||
Main.getEventBaton().releaseBaton('windowFocus', this.onWindowFocus, this);
|
||
Main.getEventBaton().releaseBaton('documentClick', this.onDocumentClick, this);
|
||
|
||
Main.getEventBaton().trigger('windowFocus');
|
||
},
|
||
|
||
onWindowFocus: function(e) {
|
||
console.log('window focus doing nothing', e);
|
||
},
|
||
|
||
onDocumentClick: function(e) {
|
||
console.log('doc click doing nothing', e);
|
||
},
|
||
|
||
onKeyDown: function(e) {
|
||
e.preventDefault();
|
||
},
|
||
|
||
onKeyUp: function(e) {
|
||
e.preventDefault();
|
||
},
|
||
|
||
show: function() {
|
||
this.toggleZ(true);
|
||
// on reflow, change our class to animate. for whatever
|
||
// reason if this is done immediately, chrome might combine
|
||
// the two changes and lose the ability to animate and it looks bad.
|
||
process.nextTick(_.bind(function() {
|
||
this.toggleShow(true);
|
||
}, this));
|
||
},
|
||
|
||
hide: function() {
|
||
this.toggleShow(false);
|
||
setTimeout(_.bind(function() {
|
||
// if we are still hidden...
|
||
if (!this.shown) {
|
||
this.toggleZ(false);
|
||
}
|
||
}, this), this.getAnimationTime());
|
||
},
|
||
|
||
getInsideElement: function() {
|
||
return this.$('.contentHolder');
|
||
},
|
||
|
||
toggleShow: function(value) {
|
||
this.shown = value;
|
||
this.$el.toggleClass('show', value);
|
||
},
|
||
|
||
toggleZ: function(value) {
|
||
this.$el.toggleClass('inFront', value);
|
||
},
|
||
|
||
tearDown: function() {
|
||
this.$el.html('');
|
||
$('body')[0].removeChild(this.el);
|
||
this.releaseKeyboard();
|
||
}
|
||
});
|
||
|
||
var ModalTerminal = ContainedBase.extend({
|
||
tagName: 'div',
|
||
className: 'box flex1',
|
||
template: _.template($('#terminal-window-template').html()),
|
||
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
|
||
this.container = new ModalView();
|
||
this.JSON = {
|
||
title: options.title || 'Heed This Warning!'
|
||
};
|
||
|
||
this.render();
|
||
},
|
||
|
||
getInsideElement: function() {
|
||
return this.$('#inside');
|
||
}
|
||
});
|
||
|
||
var ModalAlert = ContainedBase.extend({
|
||
tagName: 'div',
|
||
template: _.template($('#modal-alert-template').html()),
|
||
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
this.JSON = {
|
||
title: options.title || 'Something to say',
|
||
text: options.text || 'Here is a paragraph',
|
||
markdown: options.markdown
|
||
};
|
||
|
||
if (options.markdowns) {
|
||
this.JSON.markdown = options.markdowns.join('\n');
|
||
}
|
||
|
||
this.container = new ModalTerminal({
|
||
title: 'Alert!'
|
||
});
|
||
this.render();
|
||
},
|
||
|
||
render: function() {
|
||
var HTML = (this.JSON.markdown) ?
|
||
require('markdown').markdown.toHTML(this.JSON.markdown) :
|
||
this.template(this.JSON);
|
||
|
||
// call to super, not super elegant but better than
|
||
// copy paste code
|
||
ModalAlert.__super__.render.apply(this, [HTML]);
|
||
}
|
||
});
|
||
|
||
var ZoomAlertWindow = Backbone.View.extend({
|
||
initialize: function(options) {
|
||
this.grabBatons();
|
||
this.modalAlert = new ModalAlert({
|
||
markdowns: [
|
||
'## That zoom level is not supported :-/',
|
||
'Please zoom back to a supported zoom level with Ctrl + and Ctrl -',
|
||
'',
|
||
'(and of course, pull requests to fix this are appreciated :D)'
|
||
]
|
||
});
|
||
|
||
this.modalAlert.show();
|
||
},
|
||
|
||
grabBatons: function() {
|
||
Main.getEventBaton().stealBaton('zoomChange', this.zoomChange, this);
|
||
},
|
||
|
||
releaseBatons: function() {
|
||
Main.getEventBaton().releaseBaton('zoomChange', this.zoomChange, this);
|
||
},
|
||
|
||
zoomChange: function(level) {
|
||
if (level <= Constants.VIEWPORT.maxZoom &&
|
||
level >= Constants.VIEWPORT.minZoom) {
|
||
this.finish();
|
||
}
|
||
},
|
||
|
||
finish: function() {
|
||
this.releaseBatons();
|
||
this.modalAlert.die();
|
||
}
|
||
});
|
||
|
||
exports.ModalView = ModalView;
|
||
exports.ModalTerminal = ModalTerminal;
|
||
exports.ModalAlert = ModalAlert;
|
||
exports.ContainedBase = ContainedBase;
|
||
exports.ConfirmCancelView = ConfirmCancelView;
|
||
exports.LeftRightView = LeftRightView;
|
||
exports.ZoomAlertWindow = ZoomAlertWindow;
|
||
|
||
|
||
});
|
||
|
||
require.define("/node_modules/markdown/package.json",function(require,module,exports,__dirname,__filename,process,global){module.exports = {"main":"./lib/index.js"}
|
||
});
|
||
|
||
require.define("/node_modules/markdown/lib/index.js",function(require,module,exports,__dirname,__filename,process,global){// super simple module for the most common nodejs use case.
|
||
exports.markdown = require("./markdown");
|
||
exports.parse = exports.markdown.toHTML;
|
||
|
||
});
|
||
|
||
require.define("/node_modules/markdown/lib/markdown.js",function(require,module,exports,__dirname,__filename,process,global){// Released under MIT license
|
||
// Copyright (c) 2009-2010 Dominic Baggott
|
||
// Copyright (c) 2009-2010 Ash Berlin
|
||
// Copyright (c) 2011 Christoph Dorn <christoph@christophdorn.com> (http://www.christophdorn.com)
|
||
|
||
(function( expose ) {
|
||
|
||
/**
|
||
* class Markdown
|
||
*
|
||
* Markdown processing in Javascript done right. We have very particular views
|
||
* on what constitutes 'right' which include:
|
||
*
|
||
* - produces well-formed HTML (this means that em and strong nesting is
|
||
* important)
|
||
*
|
||
* - has an intermediate representation to allow processing of parsed data (We
|
||
* in fact have two, both as [JsonML]: a markdown tree and an HTML tree).
|
||
*
|
||
* - is easily extensible to add new dialects without having to rewrite the
|
||
* entire parsing mechanics
|
||
*
|
||
* - has a good test suite
|
||
*
|
||
* This implementation fulfills all of these (except that the test suite could
|
||
* do with expanding to automatically run all the fixtures from other Markdown
|
||
* implementations.)
|
||
*
|
||
* ##### Intermediate Representation
|
||
*
|
||
* *TODO* Talk about this :) Its JsonML, but document the node names we use.
|
||
*
|
||
* [JsonML]: http://jsonml.org/ "JSON Markup Language"
|
||
**/
|
||
var Markdown = expose.Markdown = function Markdown(dialect) {
|
||
switch (typeof dialect) {
|
||
case "undefined":
|
||
this.dialect = Markdown.dialects.Gruber;
|
||
break;
|
||
case "object":
|
||
this.dialect = dialect;
|
||
break;
|
||
default:
|
||
if (dialect in Markdown.dialects) {
|
||
this.dialect = Markdown.dialects[dialect];
|
||
}
|
||
else {
|
||
throw new Error("Unknown Markdown dialect '" + String(dialect) + "'");
|
||
}
|
||
break;
|
||
}
|
||
this.em_state = [];
|
||
this.strong_state = [];
|
||
this.debug_indent = "";
|
||
};
|
||
|
||
/**
|
||
* parse( markdown, [dialect] ) -> JsonML
|
||
* - markdown (String): markdown string to parse
|
||
* - dialect (String | Dialect): the dialect to use, defaults to gruber
|
||
*
|
||
* Parse `markdown` and return a markdown document as a Markdown.JsonML tree.
|
||
**/
|
||
expose.parse = function( source, dialect ) {
|
||
// dialect will default if undefined
|
||
var md = new Markdown( dialect );
|
||
return md.toTree( source );
|
||
};
|
||
|
||
/**
|
||
* toHTML( markdown, [dialect] ) -> String
|
||
* toHTML( md_tree ) -> String
|
||
* - markdown (String): markdown string to parse
|
||
* - md_tree (Markdown.JsonML): parsed markdown tree
|
||
*
|
||
* Take markdown (either as a string or as a JsonML tree) and run it through
|
||
* [[toHTMLTree]] then turn it into a well-formated HTML fragment.
|
||
**/
|
||
expose.toHTML = function toHTML( source , dialect , options ) {
|
||
var input = expose.toHTMLTree( source , dialect , options );
|
||
|
||
return expose.renderJsonML( input );
|
||
};
|
||
|
||
/**
|
||
* toHTMLTree( markdown, [dialect] ) -> JsonML
|
||
* toHTMLTree( md_tree ) -> JsonML
|
||
* - markdown (String): markdown string to parse
|
||
* - dialect (String | Dialect): the dialect to use, defaults to gruber
|
||
* - md_tree (Markdown.JsonML): parsed markdown tree
|
||
*
|
||
* Turn markdown into HTML, represented as a JsonML tree. If a string is given
|
||
* to this function, it is first parsed into a markdown tree by calling
|
||
* [[parse]].
|
||
**/
|
||
expose.toHTMLTree = function toHTMLTree( input, dialect , options ) {
|
||
// convert string input to an MD tree
|
||
if ( typeof input ==="string" ) input = this.parse( input, dialect );
|
||
|
||
// Now convert the MD tree to an HTML tree
|
||
|
||
// remove references from the tree
|
||
var attrs = extract_attr( input ),
|
||
refs = {};
|
||
|
||
if ( attrs && attrs.references ) {
|
||
refs = attrs.references;
|
||
}
|
||
|
||
var html = convert_tree_to_html( input, refs , options );
|
||
merge_text_nodes( html );
|
||
return html;
|
||
};
|
||
|
||
// For Spidermonkey based engines
|
||
function mk_block_toSource() {
|
||
return "Markdown.mk_block( " +
|
||
uneval(this.toString()) +
|
||
", " +
|
||
uneval(this.trailing) +
|
||
", " +
|
||
uneval(this.lineNumber) +
|
||
" )";
|
||
}
|
||
|
||
// node
|
||
function mk_block_inspect() {
|
||
var util = require('util');
|
||
return "Markdown.mk_block( " +
|
||
util.inspect(this.toString()) +
|
||
", " +
|
||
util.inspect(this.trailing) +
|
||
", " +
|
||
util.inspect(this.lineNumber) +
|
||
" )";
|
||
|
||
}
|
||
|
||
var mk_block = Markdown.mk_block = function(block, trail, line) {
|
||
// Be helpful for default case in tests.
|
||
if ( arguments.length == 1 ) trail = "\n\n";
|
||
|
||
var s = new String(block);
|
||
s.trailing = trail;
|
||
// To make it clear its not just a string
|
||
s.inspect = mk_block_inspect;
|
||
s.toSource = mk_block_toSource;
|
||
|
||
if (line != undefined)
|
||
s.lineNumber = line;
|
||
|
||
return s;
|
||
};
|
||
|
||
function count_lines( str ) {
|
||
var n = 0, i = -1;
|
||
while ( ( i = str.indexOf('\n', i+1) ) !== -1) n++;
|
||
return n;
|
||
}
|
||
|
||
// Internal - split source into rough blocks
|
||
Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) {
|
||
// [\s\S] matches _anything_ (newline or space)
|
||
var re = /([\s\S]+?)($|\n(?:\s*\n|$)+)/g,
|
||
blocks = [],
|
||
m;
|
||
|
||
var line_no = 1;
|
||
|
||
if ( ( m = /^(\s*\n)/.exec(input) ) != null ) {
|
||
// skip (but count) leading blank lines
|
||
line_no += count_lines( m[0] );
|
||
re.lastIndex = m[0].length;
|
||
}
|
||
|
||
while ( ( m = re.exec(input) ) !== null ) {
|
||
blocks.push( mk_block( m[1], m[2], line_no ) );
|
||
line_no += count_lines( m[0] );
|
||
}
|
||
|
||
return blocks;
|
||
};
|
||
|
||
/**
|
||
* Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ]
|
||
* - block (String): the block to process
|
||
* - next (Array): the following blocks
|
||
*
|
||
* Process `block` and return an array of JsonML nodes representing `block`.
|
||
*
|
||
* It does this by asking each block level function in the dialect to process
|
||
* the block until one can. Succesful handling is indicated by returning an
|
||
* array (with zero or more JsonML nodes), failure by a false value.
|
||
*
|
||
* Blocks handlers are responsible for calling [[Markdown#processInline]]
|
||
* themselves as appropriate.
|
||
*
|
||
* If the blocks were split incorrectly or adjacent blocks need collapsing you
|
||
* can adjust `next` in place using shift/splice etc.
|
||
*
|
||
* If any of this default behaviour is not right for the dialect, you can
|
||
* define a `__call__` method on the dialect that will get invoked to handle
|
||
* the block processing.
|
||
*/
|
||
Markdown.prototype.processBlock = function processBlock( block, next ) {
|
||
var cbs = this.dialect.block,
|
||
ord = cbs.__order__;
|
||
|
||
if ( "__call__" in cbs ) {
|
||
return cbs.__call__.call(this, block, next);
|
||
}
|
||
|
||
for ( var i = 0; i < ord.length; i++ ) {
|
||
//D:this.debug( "Testing", ord[i] );
|
||
var res = cbs[ ord[i] ].call( this, block, next );
|
||
if ( res ) {
|
||
//D:this.debug(" matched");
|
||
if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) )
|
||
this.debug(ord[i], "didn't return a proper array");
|
||
//D:this.debug( "" );
|
||
return res;
|
||
}
|
||
}
|
||
|
||
// Uhoh! no match! Should we throw an error?
|
||
return [];
|
||
};
|
||
|
||
Markdown.prototype.processInline = function processInline( block ) {
|
||
return this.dialect.inline.__call__.call( this, String( block ) );
|
||
};
|
||
|
||
/**
|
||
* Markdown#toTree( source ) -> JsonML
|
||
* - source (String): markdown source to parse
|
||
*
|
||
* Parse `source` into a JsonML tree representing the markdown document.
|
||
**/
|
||
// custom_tree means set this.tree to `custom_tree` and restore old value on return
|
||
Markdown.prototype.toTree = function toTree( source, custom_root ) {
|
||
var blocks = source instanceof Array ? source : this.split_blocks( source );
|
||
|
||
// Make tree a member variable so its easier to mess with in extensions
|
||
var old_tree = this.tree;
|
||
try {
|
||
this.tree = custom_root || this.tree || [ "markdown" ];
|
||
|
||
blocks:
|
||
while ( blocks.length ) {
|
||
var b = this.processBlock( blocks.shift(), blocks );
|
||
|
||
// Reference blocks and the like won't return any content
|
||
if ( !b.length ) continue blocks;
|
||
|
||
this.tree.push.apply( this.tree, b );
|
||
}
|
||
return this.tree;
|
||
}
|
||
finally {
|
||
if ( custom_root ) {
|
||
this.tree = old_tree;
|
||
}
|
||
}
|
||
};
|
||
|
||
// Noop by default
|
||
Markdown.prototype.debug = function () {
|
||
var args = Array.prototype.slice.call( arguments);
|
||
args.unshift(this.debug_indent);
|
||
if (typeof print !== "undefined")
|
||
print.apply( print, args );
|
||
if (typeof console !== "undefined" && typeof console.log !== "undefined")
|
||
console.log.apply( null, args );
|
||
}
|
||
|
||
Markdown.prototype.loop_re_over_block = function( re, block, cb ) {
|
||
// Dont use /g regexps with this
|
||
var m,
|
||
b = block.valueOf();
|
||
|
||
while ( b.length && (m = re.exec(b) ) != null) {
|
||
b = b.substr( m[0].length );
|
||
cb.call(this, m);
|
||
}
|
||
return b;
|
||
};
|
||
|
||
/**
|
||
* Markdown.dialects
|
||
*
|
||
* Namespace of built-in dialects.
|
||
**/
|
||
Markdown.dialects = {};
|
||
|
||
/**
|
||
* Markdown.dialects.Gruber
|
||
*
|
||
* The default dialect that follows the rules set out by John Gruber's
|
||
* markdown.pl as closely as possible. Well actually we follow the behaviour of
|
||
* that script which in some places is not exactly what the syntax web page
|
||
* says.
|
||
**/
|
||
Markdown.dialects.Gruber = {
|
||
block: {
|
||
atxHeader: function atxHeader( block, next ) {
|
||
var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ );
|
||
|
||
if ( !m ) return undefined;
|
||
|
||
var header = [ "header", { level: m[ 1 ].length } ];
|
||
Array.prototype.push.apply(header, this.processInline(m[ 2 ]));
|
||
|
||
if ( m[0].length < block.length )
|
||
next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) );
|
||
|
||
return [ header ];
|
||
},
|
||
|
||
setextHeader: function setextHeader( block, next ) {
|
||
var m = block.match( /^(.*)\n([-=])\2\2+(?:\n|$)/ );
|
||
|
||
if ( !m ) return undefined;
|
||
|
||
var level = ( m[ 2 ] === "=" ) ? 1 : 2;
|
||
var header = [ "header", { level : level }, m[ 1 ] ];
|
||
|
||
if ( m[0].length < block.length )
|
||
next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) );
|
||
|
||
return [ header ];
|
||
},
|
||
|
||
code: function code( block, next ) {
|
||
// | Foo
|
||
// |bar
|
||
// should be a code block followed by a paragraph. Fun
|
||
//
|
||
// There might also be adjacent code block to merge.
|
||
|
||
var ret = [],
|
||
re = /^(?: {0,3}\t| {4})(.*)\n?/,
|
||
lines;
|
||
|
||
// 4 spaces + content
|
||
if ( !block.match( re ) ) return undefined;
|
||
|
||
block_search:
|
||
do {
|
||
// Now pull out the rest of the lines
|
||
var b = this.loop_re_over_block(
|
||
re, block.valueOf(), function( m ) { ret.push( m[1] ); } );
|
||
|
||
if (b.length) {
|
||
// Case alluded to in first comment. push it back on as a new block
|
||
next.unshift( mk_block(b, block.trailing) );
|
||
break block_search;
|
||
}
|
||
else if (next.length) {
|
||
// Check the next block - it might be code too
|
||
if ( !next[0].match( re ) ) break block_search;
|
||
|
||
// Pull how how many blanks lines follow - minus two to account for .join
|
||
ret.push ( block.trailing.replace(/[^\n]/g, '').substring(2) );
|
||
|
||
block = next.shift();
|
||
}
|
||
else {
|
||
break block_search;
|
||
}
|
||
} while (true);
|
||
|
||
return [ [ "code_block", ret.join("\n") ] ];
|
||
},
|
||
|
||
horizRule: function horizRule( block, next ) {
|
||
// this needs to find any hr in the block to handle abutting blocks
|
||
var m = block.match( /^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/ );
|
||
|
||
if ( !m ) {
|
||
return undefined;
|
||
}
|
||
|
||
var jsonml = [ [ "hr" ] ];
|
||
|
||
// if there's a leading abutting block, process it
|
||
if ( m[ 1 ] ) {
|
||
jsonml.unshift.apply( jsonml, this.processBlock( m[ 1 ], [] ) );
|
||
}
|
||
|
||
// if there's a trailing abutting block, stick it into next
|
||
if ( m[ 3 ] ) {
|
||
next.unshift( mk_block( m[ 3 ] ) );
|
||
}
|
||
|
||
return jsonml;
|
||
},
|
||
|
||
// There are two types of lists. Tight and loose. Tight lists have no whitespace
|
||
// between the items (and result in text just in the <li>) and loose lists,
|
||
// which have an empty line between list items, resulting in (one or more)
|
||
// paragraphs inside the <li>.
|
||
//
|
||
// There are all sorts weird edge cases about the original markdown.pl's
|
||
// handling of lists:
|
||
//
|
||
// * Nested lists are supposed to be indented by four chars per level. But
|
||
// if they aren't, you can get a nested list by indenting by less than
|
||
// four so long as the indent doesn't match an indent of an existing list
|
||
// item in the 'nest stack'.
|
||
//
|
||
// * The type of the list (bullet or number) is controlled just by the
|
||
// first item at the indent. Subsequent changes are ignored unless they
|
||
// are for nested lists
|
||
//
|
||
lists: (function( ) {
|
||
// Use a closure to hide a few variables.
|
||
var any_list = "[*+-]|\\d+\\.",
|
||
bullet_list = /[*+-]/,
|
||
number_list = /\d+\./,
|
||
// Capture leading indent as it matters for determining nested lists.
|
||
is_list_re = new RegExp( "^( {0,3})(" + any_list + ")[ \t]+" ),
|
||
indent_re = "(?: {0,3}\\t| {4})";
|
||
|
||
// TODO: Cache this regexp for certain depths.
|
||
// Create a regexp suitable for matching an li for a given stack depth
|
||
function regex_for_depth( depth ) {
|
||
|
||
return new RegExp(
|
||
// m[1] = indent, m[2] = list_type
|
||
"(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" +
|
||
// m[3] = cont
|
||
"(^" + indent_re + "{0," + (depth-1) + "}[ ]{0,4})"
|
||
);
|
||
}
|
||
function expand_tab( input ) {
|
||
return input.replace( / {0,3}\t/g, " " );
|
||
}
|
||
|
||
// Add inline content `inline` to `li`. inline comes from processInline
|
||
// so is an array of content
|
||
function add(li, loose, inline, nl) {
|
||
if (loose) {
|
||
li.push( [ "para" ].concat(inline) );
|
||
return;
|
||
}
|
||
// Hmmm, should this be any block level element or just paras?
|
||
var add_to = li[li.length -1] instanceof Array && li[li.length - 1][0] == "para"
|
||
? li[li.length -1]
|
||
: li;
|
||
|
||
// If there is already some content in this list, add the new line in
|
||
if (nl && li.length > 1) inline.unshift(nl);
|
||
|
||
for (var i=0; i < inline.length; i++) {
|
||
var what = inline[i],
|
||
is_str = typeof what == "string";
|
||
if (is_str && add_to.length > 1 && typeof add_to[add_to.length-1] == "string" ) {
|
||
add_to[ add_to.length-1 ] += what;
|
||
}
|
||
else {
|
||
add_to.push( what );
|
||
}
|
||
}
|
||
}
|
||
|
||
// contained means have an indent greater than the current one. On
|
||
// *every* line in the block
|
||
function get_contained_blocks( depth, blocks ) {
|
||
|
||
var re = new RegExp( "^(" + indent_re + "{" + depth + "}.*?\\n?)*$" ),
|
||
replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"),
|
||
ret = [];
|
||
|
||
while ( blocks.length > 0 ) {
|
||
if ( re.exec( blocks[0] ) ) {
|
||
var b = blocks.shift(),
|
||
// Now remove that indent
|
||
x = b.replace( replace, "");
|
||
|
||
ret.push( mk_block( x, b.trailing, b.lineNumber ) );
|
||
}
|
||
break;
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
// passed to stack.forEach to turn list items up the stack into paras
|
||
function paragraphify(s, i, stack) {
|
||
var list = s.list;
|
||
var last_li = list[list.length-1];
|
||
|
||
if (last_li[1] instanceof Array && last_li[1][0] == "para") {
|
||
return;
|
||
}
|
||
if (i+1 == stack.length) {
|
||
// Last stack frame
|
||
// Keep the same array, but replace the contents
|
||
last_li.push( ["para"].concat( last_li.splice(1) ) );
|
||
}
|
||
else {
|
||
var sublist = last_li.pop();
|
||
last_li.push( ["para"].concat( last_li.splice(1) ), sublist );
|
||
}
|
||
}
|
||
|
||
// The matcher function
|
||
return function( block, next ) {
|
||
var m = block.match( is_list_re );
|
||
if ( !m ) return undefined;
|
||
|
||
function make_list( m ) {
|
||
var list = bullet_list.exec( m[2] )
|
||
? ["bulletlist"]
|
||
: ["numberlist"];
|
||
|
||
stack.push( { list: list, indent: m[1] } );
|
||
return list;
|
||
}
|
||
|
||
|
||
var stack = [], // Stack of lists for nesting.
|
||
list = make_list( m ),
|
||
last_li,
|
||
loose = false,
|
||
ret = [ stack[0].list ],
|
||
i;
|
||
|
||
// Loop to search over block looking for inner block elements and loose lists
|
||
loose_search:
|
||
while( true ) {
|
||
// Split into lines preserving new lines at end of line
|
||
var lines = block.split( /(?=\n)/ );
|
||
|
||
// We have to grab all lines for a li and call processInline on them
|
||
// once as there are some inline things that can span lines.
|
||
var li_accumulate = "";
|
||
|
||
// Loop over the lines in this block looking for tight lists.
|
||
tight_search:
|
||
for (var line_no=0; line_no < lines.length; line_no++) {
|
||
var nl = "",
|
||
l = lines[line_no].replace(/^\n/, function(n) { nl = n; return ""; });
|
||
|
||
// TODO: really should cache this
|
||
var line_re = regex_for_depth( stack.length );
|
||
|
||
m = l.match( line_re );
|
||
//print( "line:", uneval(l), "\nline match:", uneval(m) );
|
||
|
||
// We have a list item
|
||
if ( m[1] !== undefined ) {
|
||
// Process the previous list item, if any
|
||
if ( li_accumulate.length ) {
|
||
add( last_li, loose, this.processInline( li_accumulate ), nl );
|
||
// Loose mode will have been dealt with. Reset it
|
||
loose = false;
|
||
li_accumulate = "";
|
||
}
|
||
|
||
m[1] = expand_tab( m[1] );
|
||
var wanted_depth = Math.floor(m[1].length/4)+1;
|
||
//print( "want:", wanted_depth, "stack:", stack.length);
|
||
if ( wanted_depth > stack.length ) {
|
||
// Deep enough for a nested list outright
|
||
//print ( "new nested list" );
|
||
list = make_list( m );
|
||
last_li.push( list );
|
||
last_li = list[1] = [ "listitem" ];
|
||
}
|
||
else {
|
||
// We aren't deep enough to be strictly a new level. This is
|
||
// where Md.pl goes nuts. If the indent matches a level in the
|
||
// stack, put it there, else put it one deeper then the
|
||
// wanted_depth deserves.
|
||
var found = false;
|
||
for (i = 0; i < stack.length; i++) {
|
||
if ( stack[ i ].indent != m[1] ) continue;
|
||
list = stack[ i ].list;
|
||
stack.splice( i+1 );
|
||
found = true;
|
||
break;
|
||
}
|
||
|
||
if (!found) {
|
||
//print("not found. l:", uneval(l));
|
||
wanted_depth++;
|
||
if (wanted_depth <= stack.length) {
|
||
stack.splice(wanted_depth);
|
||
//print("Desired depth now", wanted_depth, "stack:", stack.length);
|
||
list = stack[wanted_depth-1].list;
|
||
//print("list:", uneval(list) );
|
||
}
|
||
else {
|
||
//print ("made new stack for messy indent");
|
||
list = make_list(m);
|
||
last_li.push(list);
|
||
}
|
||
}
|
||
|
||
//print( uneval(list), "last", list === stack[stack.length-1].list );
|
||
last_li = [ "listitem" ];
|
||
list.push(last_li);
|
||
} // end depth of shenegains
|
||
nl = "";
|
||
}
|
||
|
||
// Add content
|
||
if (l.length > m[0].length) {
|
||
li_accumulate += nl + l.substr( m[0].length );
|
||
}
|
||
} // tight_search
|
||
|
||
if ( li_accumulate.length ) {
|
||
add( last_li, loose, this.processInline( li_accumulate ), nl );
|
||
// Loose mode will have been dealt with. Reset it
|
||
loose = false;
|
||
li_accumulate = "";
|
||
}
|
||
|
||
// Look at the next block - we might have a loose list. Or an extra
|
||
// paragraph for the current li
|
||
var contained = get_contained_blocks( stack.length, next );
|
||
|
||
// Deal with code blocks or properly nested lists
|
||
if (contained.length > 0) {
|
||
// Make sure all listitems up the stack are paragraphs
|
||
forEach( stack, paragraphify, this);
|
||
|
||
last_li.push.apply( last_li, this.toTree( contained, [] ) );
|
||
}
|
||
|
||
var next_block = next[0] && next[0].valueOf() || "";
|
||
|
||
if ( next_block.match(is_list_re) || next_block.match( /^ / ) ) {
|
||
block = next.shift();
|
||
|
||
// Check for an HR following a list: features/lists/hr_abutting
|
||
var hr = this.dialect.block.horizRule( block, next );
|
||
|
||
if (hr) {
|
||
ret.push.apply(ret, hr);
|
||
break;
|
||
}
|
||
|
||
// Make sure all listitems up the stack are paragraphs
|
||
forEach( stack, paragraphify, this);
|
||
|
||
loose = true;
|
||
continue loose_search;
|
||
}
|
||
break;
|
||
} // loose_search
|
||
|
||
return ret;
|
||
};
|
||
})(),
|
||
|
||
blockquote: function blockquote( block, next ) {
|
||
if ( !block.match( /^>/m ) )
|
||
return undefined;
|
||
|
||
var jsonml = [];
|
||
|
||
// separate out the leading abutting block, if any
|
||
if ( block[ 0 ] != ">" ) {
|
||
var lines = block.split( /\n/ ),
|
||
prev = [];
|
||
|
||
// keep shifting lines until you find a crotchet
|
||
while ( lines.length && lines[ 0 ][ 0 ] != ">" ) {
|
||
prev.push( lines.shift() );
|
||
}
|
||
|
||
// reassemble!
|
||
block = lines.join( "\n" );
|
||
jsonml.push.apply( jsonml, this.processBlock( prev.join( "\n" ), [] ) );
|
||
}
|
||
|
||
// if the next block is also a blockquote merge it in
|
||
while ( next.length && next[ 0 ][ 0 ] == ">" ) {
|
||
var b = next.shift();
|
||
block = new String(block + block.trailing + b);
|
||
block.trailing = b.trailing;
|
||
}
|
||
|
||
// Strip off the leading "> " and re-process as a block.
|
||
var input = block.replace( /^> ?/gm, '' ),
|
||
old_tree = this.tree;
|
||
jsonml.push( this.toTree( input, [ "blockquote" ] ) );
|
||
|
||
return jsonml;
|
||
},
|
||
|
||
referenceDefn: function referenceDefn( block, next) {
|
||
var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/;
|
||
// interesting matches are [ , ref_id, url, , title, title ]
|
||
|
||
if ( !block.match(re) )
|
||
return undefined;
|
||
|
||
// make an attribute node if it doesn't exist
|
||
if ( !extract_attr( this.tree ) ) {
|
||
this.tree.splice( 1, 0, {} );
|
||
}
|
||
|
||
var attrs = extract_attr( this.tree );
|
||
|
||
// make a references hash if it doesn't exist
|
||
if ( attrs.references === undefined ) {
|
||
attrs.references = {};
|
||
}
|
||
|
||
var b = this.loop_re_over_block(re, block, function( m ) {
|
||
|
||
if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' )
|
||
m[2] = m[2].substring( 1, m[2].length - 1 );
|
||
|
||
var ref = attrs.references[ m[1].toLowerCase() ] = {
|
||
href: m[2]
|
||
};
|
||
|
||
if (m[4] !== undefined)
|
||
ref.title = m[4];
|
||
else if (m[5] !== undefined)
|
||
ref.title = m[5];
|
||
|
||
} );
|
||
|
||
if (b.length)
|
||
next.unshift( mk_block( b, block.trailing ) );
|
||
|
||
return [];
|
||
},
|
||
|
||
para: function para( block, next ) {
|
||
// everything's a para!
|
||
return [ ["para"].concat( this.processInline( block ) ) ];
|
||
}
|
||
}
|
||
};
|
||
|
||
Markdown.dialects.Gruber.inline = {
|
||
|
||
__oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) {
|
||
var m,
|
||
res,
|
||
lastIndex = 0;
|
||
|
||
patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__;
|
||
var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" );
|
||
|
||
m = re.exec( text );
|
||
if (!m) {
|
||
// Just boring text
|
||
return [ text.length, text ];
|
||
}
|
||
else if ( m[1] ) {
|
||
// Some un-interesting text matched. Return that first
|
||
return [ m[1].length, m[1] ];
|
||
}
|
||
|
||
var res;
|
||
if ( m[2] in this.dialect.inline ) {
|
||
res = this.dialect.inline[ m[2] ].call(
|
||
this,
|
||
text.substr( m.index ), m, previous_nodes || [] );
|
||
}
|
||
// Default for now to make dev easier. just slurp special and output it.
|
||
res = res || [ m[2].length, m[2] ];
|
||
return res;
|
||
},
|
||
|
||
__call__: function inline( text, patterns ) {
|
||
|
||
var out = [],
|
||
res;
|
||
|
||
function add(x) {
|
||
//D:self.debug(" adding output", uneval(x));
|
||
if (typeof x == "string" && typeof out[out.length-1] == "string")
|
||
out[ out.length-1 ] += x;
|
||
else
|
||
out.push(x);
|
||
}
|
||
|
||
while ( text.length > 0 ) {
|
||
res = this.dialect.inline.__oneElement__.call(this, text, patterns, out );
|
||
text = text.substr( res.shift() );
|
||
forEach(res, add )
|
||
}
|
||
|
||
return out;
|
||
},
|
||
|
||
// These characters are intersting elsewhere, so have rules for them so that
|
||
// chunks of plain text blocks don't include them
|
||
"]": function () {},
|
||
"}": function () {},
|
||
|
||
"\\": function escaped( text ) {
|
||
// [ length of input processed, node/children to add... ]
|
||
// Only esacape: \ ` * _ { } [ ] ( ) # * + - . !
|
||
if ( text.match( /^\\[\\`\*_{}\[\]()#\+.!\-]/ ) )
|
||
return [ 2, text[1] ];
|
||
else
|
||
// Not an esacpe
|
||
return [ 1, "\\" ];
|
||
},
|
||
|
||
"
|
||
// 1 2 3 4 <--- captures
|
||
var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*(\S*)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ );
|
||
|
||
if ( m ) {
|
||
if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' )
|
||
m[2] = m[2].substring( 1, m[2].length - 1 );
|
||
|
||
m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0];
|
||
|
||
var attrs = { alt: m[1], href: m[2] || "" };
|
||
if ( m[4] !== undefined)
|
||
attrs.title = m[4];
|
||
|
||
return [ m[0].length, [ "img", attrs ] ];
|
||
}
|
||
|
||
// ![Alt text][id]
|
||
m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ );
|
||
|
||
if ( m ) {
|
||
// We can't check if the reference is known here as it likely wont be
|
||
// found till after. Check it in md tree->hmtl tree conversion
|
||
return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ];
|
||
}
|
||
|
||
// Just consume the '!['
|
||
return [ 2, "![" ];
|
||
},
|
||
|
||
"[": function link( text ) {
|
||
|
||
var orig = String(text);
|
||
// Inline content is possible inside `link text`
|
||
var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), ']' );
|
||
|
||
// No closing ']' found. Just consume the [
|
||
if ( !res ) return [ 1, '[' ];
|
||
|
||
var consumed = 1 + res[ 0 ],
|
||
children = res[ 1 ],
|
||
link,
|
||
attrs;
|
||
|
||
// At this point the first [...] has been parsed. See what follows to find
|
||
// out which kind of link we are (reference or direct url)
|
||
text = text.substr( consumed );
|
||
|
||
// [link text](/path/to/img.jpg "Optional title")
|
||
// 1 2 3 <--- captures
|
||
// This will capture up to the last paren in the block. We then pull
|
||
// back based on if there a matching ones in the url
|
||
// ([here](/url/(test))
|
||
// The parens have to be balanced
|
||
var m = text.match( /^\s*\([ \t]*(\S+)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ );
|
||
if ( m ) {
|
||
var url = m[1];
|
||
consumed += m[0].length;
|
||
|
||
if ( url && url[0] == '<' && url[url.length-1] == '>' )
|
||
url = url.substring( 1, url.length - 1 );
|
||
|
||
// If there is a title we don't have to worry about parens in the url
|
||
if ( !m[3] ) {
|
||
var open_parens = 1; // One open that isn't in the capture
|
||
for (var len = 0; len < url.length; len++) {
|
||
switch ( url[len] ) {
|
||
case '(':
|
||
open_parens++;
|
||
break;
|
||
case ')':
|
||
if ( --open_parens == 0) {
|
||
consumed -= url.length - len;
|
||
url = url.substring(0, len);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Process escapes only
|
||
url = this.dialect.inline.__call__.call( this, url, /\\/ )[0];
|
||
|
||
attrs = { href: url || "" };
|
||
if ( m[3] !== undefined)
|
||
attrs.title = m[3];
|
||
|
||
link = [ "link", attrs ].concat( children );
|
||
return [ consumed, link ];
|
||
}
|
||
|
||
// [Alt text][id]
|
||
// [Alt text] [id]
|
||
m = text.match( /^\s*\[(.*?)\]/ );
|
||
|
||
if ( m ) {
|
||
|
||
consumed += m[ 0 ].length;
|
||
|
||
// [links][] uses links as its reference
|
||
attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) };
|
||
|
||
link = [ "link_ref", attrs ].concat( children );
|
||
|
||
// We can't check if the reference is known here as it likely wont be
|
||
// found till after. Check it in md tree->hmtl tree conversion.
|
||
// Store the original so that conversion can revert if the ref isn't found.
|
||
return [ consumed, link ];
|
||
}
|
||
|
||
// [id]
|
||
// Only if id is plain (no formatting.)
|
||
if ( children.length == 1 && typeof children[0] == "string" ) {
|
||
|
||
attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) };
|
||
link = [ "link_ref", attrs, children[0] ];
|
||
return [ consumed, link ];
|
||
}
|
||
|
||
// Just consume the '['
|
||
return [ 1, "[" ];
|
||
},
|
||
|
||
|
||
"<": function autoLink( text ) {
|
||
var m;
|
||
|
||
if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) != null ) {
|
||
if ( m[3] ) {
|
||
return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ];
|
||
|
||
}
|
||
else if ( m[2] == "mailto" ) {
|
||
return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ];
|
||
}
|
||
else
|
||
return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ];
|
||
}
|
||
|
||
return [ 1, "<" ];
|
||
},
|
||
|
||
"`": function inlineCode( text ) {
|
||
// Inline code block. as many backticks as you like to start it
|
||
// Always skip over the opening ticks.
|
||
var m = text.match( /(`+)(([\s\S]*?)\1)/ );
|
||
|
||
if ( m && m[2] )
|
||
return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ];
|
||
else {
|
||
// TODO: No matching end code found - warn!
|
||
return [ 1, "`" ];
|
||
}
|
||
},
|
||
|
||
" \n": function lineBreak( text ) {
|
||
return [ 3, [ "linebreak" ] ];
|
||
}
|
||
|
||
};
|
||
|
||
// Meta Helper/generator method for em and strong handling
|
||
function strong_em( tag, md ) {
|
||
|
||
var state_slot = tag + "_state",
|
||
other_slot = tag == "strong" ? "em_state" : "strong_state";
|
||
|
||
function CloseTag(len) {
|
||
this.len_after = len;
|
||
this.name = "close_" + md;
|
||
}
|
||
|
||
return function ( text, orig_match ) {
|
||
|
||
if (this[state_slot][0] == md) {
|
||
// Most recent em is of this type
|
||
//D:this.debug("closing", md);
|
||
this[state_slot].shift();
|
||
|
||
// "Consume" everything to go back to the recrusion in the else-block below
|
||
return[ text.length, new CloseTag(text.length-md.length) ];
|
||
}
|
||
else {
|
||
// Store a clone of the em/strong states
|
||
var other = this[other_slot].slice(),
|
||
state = this[state_slot].slice();
|
||
|
||
this[state_slot].unshift(md);
|
||
|
||
//D:this.debug_indent += " ";
|
||
|
||
// Recurse
|
||
var res = this.processInline( text.substr( md.length ) );
|
||
//D:this.debug_indent = this.debug_indent.substr(2);
|
||
|
||
var last = res[res.length - 1];
|
||
|
||
//D:this.debug("processInline from", tag + ": ", uneval( res ) );
|
||
|
||
var check = this[state_slot].shift();
|
||
if (last instanceof CloseTag) {
|
||
res.pop();
|
||
// We matched! Huzzah.
|
||
var consumed = text.length - last.len_after;
|
||
return [ consumed, [ tag ].concat(res) ];
|
||
}
|
||
else {
|
||
// Restore the state of the other kind. We might have mistakenly closed it.
|
||
this[other_slot] = other;
|
||
this[state_slot] = state;
|
||
|
||
// We can't reuse the processed result as it could have wrong parsing contexts in it.
|
||
return [ md.length, md ];
|
||
}
|
||
}
|
||
}; // End returned function
|
||
}
|
||
|
||
Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**");
|
||
Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__");
|
||
Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*");
|
||
Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_");
|
||
|
||
|
||
// Build default order from insertion order.
|
||
Markdown.buildBlockOrder = function(d) {
|
||
var ord = [];
|
||
for ( var i in d ) {
|
||
if ( i == "__order__" || i == "__call__" ) continue;
|
||
ord.push( i );
|
||
}
|
||
d.__order__ = ord;
|
||
};
|
||
|
||
// Build patterns for inline matcher
|
||
Markdown.buildInlinePatterns = function(d) {
|
||
var patterns = [];
|
||
|
||
for ( var i in d ) {
|
||
// __foo__ is reserved and not a pattern
|
||
if ( i.match( /^__.*__$/) ) continue;
|
||
var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" )
|
||
.replace( /\n/, "\\n" );
|
||
patterns.push( i.length == 1 ? l : "(?:" + l + ")" );
|
||
}
|
||
|
||
patterns = patterns.join("|");
|
||
d.__patterns__ = patterns;
|
||
//print("patterns:", uneval( patterns ) );
|
||
|
||
var fn = d.__call__;
|
||
d.__call__ = function(text, pattern) {
|
||
if (pattern != undefined) {
|
||
return fn.call(this, text, pattern);
|
||
}
|
||
else
|
||
{
|
||
return fn.call(this, text, patterns);
|
||
}
|
||
};
|
||
};
|
||
|
||
Markdown.DialectHelpers = {};
|
||
Markdown.DialectHelpers.inline_until_char = function( text, want ) {
|
||
var consumed = 0,
|
||
nodes = [];
|
||
|
||
while ( true ) {
|
||
if ( text[ consumed ] == want ) {
|
||
// Found the character we were looking for
|
||
consumed++;
|
||
return [ consumed, nodes ];
|
||
}
|
||
|
||
if ( consumed >= text.length ) {
|
||
// No closing char found. Abort.
|
||
return null;
|
||
}
|
||
|
||
res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) );
|
||
consumed += res[ 0 ];
|
||
// Add any returned nodes.
|
||
nodes.push.apply( nodes, res.slice( 1 ) );
|
||
}
|
||
}
|
||
|
||
// Helper function to make sub-classing a dialect easier
|
||
Markdown.subclassDialect = function( d ) {
|
||
function Block() {}
|
||
Block.prototype = d.block;
|
||
function Inline() {}
|
||
Inline.prototype = d.inline;
|
||
|
||
return { block: new Block(), inline: new Inline() };
|
||
};
|
||
|
||
Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block );
|
||
Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline );
|
||
|
||
Markdown.dialects.Maruku = Markdown.subclassDialect( Markdown.dialects.Gruber );
|
||
|
||
Markdown.dialects.Maruku.processMetaHash = function processMetaHash( meta_string ) {
|
||
var meta = split_meta_hash( meta_string ),
|
||
attr = {};
|
||
|
||
for ( var i = 0; i < meta.length; ++i ) {
|
||
// id: #foo
|
||
if ( /^#/.test( meta[ i ] ) ) {
|
||
attr.id = meta[ i ].substring( 1 );
|
||
}
|
||
// class: .foo
|
||
else if ( /^\./.test( meta[ i ] ) ) {
|
||
// if class already exists, append the new one
|
||
if ( attr['class'] ) {
|
||
attr['class'] = attr['class'] + meta[ i ].replace( /./, " " );
|
||
}
|
||
else {
|
||
attr['class'] = meta[ i ].substring( 1 );
|
||
}
|
||
}
|
||
// attribute: foo=bar
|
||
else if ( /\=/.test( meta[ i ] ) ) {
|
||
var s = meta[ i ].split( /\=/ );
|
||
attr[ s[ 0 ] ] = s[ 1 ];
|
||
}
|
||
}
|
||
|
||
return attr;
|
||
}
|
||
|
||
function split_meta_hash( meta_string ) {
|
||
var meta = meta_string.split( "" ),
|
||
parts = [ "" ],
|
||
in_quotes = false;
|
||
|
||
while ( meta.length ) {
|
||
var letter = meta.shift();
|
||
switch ( letter ) {
|
||
case " " :
|
||
// if we're in a quoted section, keep it
|
||
if ( in_quotes ) {
|
||
parts[ parts.length - 1 ] += letter;
|
||
}
|
||
// otherwise make a new part
|
||
else {
|
||
parts.push( "" );
|
||
}
|
||
break;
|
||
case "'" :
|
||
case '"' :
|
||
// reverse the quotes and move straight on
|
||
in_quotes = !in_quotes;
|
||
break;
|
||
case "\\" :
|
||
// shift off the next letter to be used straight away.
|
||
// it was escaped so we'll keep it whatever it is
|
||
letter = meta.shift();
|
||
default :
|
||
parts[ parts.length - 1 ] += letter;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return parts;
|
||
}
|
||
|
||
Markdown.dialects.Maruku.block.document_meta = function document_meta( block, next ) {
|
||
// we're only interested in the first block
|
||
if ( block.lineNumber > 1 ) return undefined;
|
||
|
||
// document_meta blocks consist of one or more lines of `Key: Value\n`
|
||
if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) ) return undefined;
|
||
|
||
// make an attribute node if it doesn't exist
|
||
if ( !extract_attr( this.tree ) ) {
|
||
this.tree.splice( 1, 0, {} );
|
||
}
|
||
|
||
var pairs = block.split( /\n/ );
|
||
for ( p in pairs ) {
|
||
var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ),
|
||
key = m[ 1 ].toLowerCase(),
|
||
value = m[ 2 ];
|
||
|
||
this.tree[ 1 ][ key ] = value;
|
||
}
|
||
|
||
// document_meta produces no content!
|
||
return [];
|
||
};
|
||
|
||
Markdown.dialects.Maruku.block.block_meta = function block_meta( block, next ) {
|
||
// check if the last line of the block is an meta hash
|
||
var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ );
|
||
if ( !m ) return undefined;
|
||
|
||
// process the meta hash
|
||
var attr = this.dialect.processMetaHash( m[ 2 ] );
|
||
|
||
var hash;
|
||
|
||
// if we matched ^ then we need to apply meta to the previous block
|
||
if ( m[ 1 ] === "" ) {
|
||
var node = this.tree[ this.tree.length - 1 ];
|
||
hash = extract_attr( node );
|
||
|
||
// if the node is a string (rather than JsonML), bail
|
||
if ( typeof node === "string" ) return undefined;
|
||
|
||
// create the attribute hash if it doesn't exist
|
||
if ( !hash ) {
|
||
hash = {};
|
||
node.splice( 1, 0, hash );
|
||
}
|
||
|
||
// add the attributes in
|
||
for ( a in attr ) {
|
||
hash[ a ] = attr[ a ];
|
||
}
|
||
|
||
// return nothing so the meta hash is removed
|
||
return [];
|
||
}
|
||
|
||
// pull the meta hash off the block and process what's left
|
||
var b = block.replace( /\n.*$/, "" ),
|
||
result = this.processBlock( b, [] );
|
||
|
||
// get or make the attributes hash
|
||
hash = extract_attr( result[ 0 ] );
|
||
if ( !hash ) {
|
||
hash = {};
|
||
result[ 0 ].splice( 1, 0, hash );
|
||
}
|
||
|
||
// attach the attributes to the block
|
||
for ( a in attr ) {
|
||
hash[ a ] = attr[ a ];
|
||
}
|
||
|
||
return result;
|
||
};
|
||
|
||
Markdown.dialects.Maruku.block.definition_list = function definition_list( block, next ) {
|
||
// one or more terms followed by one or more definitions, in a single block
|
||
var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/,
|
||
list = [ "dl" ],
|
||
i;
|
||
|
||
// see if we're dealing with a tight or loose block
|
||
if ( ( m = block.match( tight ) ) ) {
|
||
// pull subsequent tight DL blocks out of `next`
|
||
var blocks = [ block ];
|
||
while ( next.length && tight.exec( next[ 0 ] ) ) {
|
||
blocks.push( next.shift() );
|
||
}
|
||
|
||
for ( var b = 0; b < blocks.length; ++b ) {
|
||
var m = blocks[ b ].match( tight ),
|
||
terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ),
|
||
defns = m[ 2 ].split( /\n:\s+/ );
|
||
|
||
// print( uneval( m ) );
|
||
|
||
for ( i = 0; i < terms.length; ++i ) {
|
||
list.push( [ "dt", terms[ i ] ] );
|
||
}
|
||
|
||
for ( i = 0; i < defns.length; ++i ) {
|
||
// run inline processing over the definition
|
||
list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) );
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
return undefined;
|
||
}
|
||
|
||
return [ list ];
|
||
};
|
||
|
||
Markdown.dialects.Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) {
|
||
if ( !out.length ) {
|
||
return [ 2, "{:" ];
|
||
}
|
||
|
||
// get the preceeding element
|
||
var before = out[ out.length - 1 ];
|
||
|
||
if ( typeof before === "string" ) {
|
||
return [ 2, "{:" ];
|
||
}
|
||
|
||
// match a meta hash
|
||
var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ );
|
||
|
||
// no match, false alarm
|
||
if ( !m ) {
|
||
return [ 2, "{:" ];
|
||
}
|
||
|
||
// attach the attributes to the preceeding element
|
||
var meta = this.dialect.processMetaHash( m[ 1 ] ),
|
||
attr = extract_attr( before );
|
||
|
||
if ( !attr ) {
|
||
attr = {};
|
||
before.splice( 1, 0, attr );
|
||
}
|
||
|
||
for ( var k in meta ) {
|
||
attr[ k ] = meta[ k ];
|
||
}
|
||
|
||
// cut out the string and replace it with nothing
|
||
return [ m[ 0 ].length, "" ];
|
||
};
|
||
|
||
Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block );
|
||
Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline );
|
||
|
||
var isArray = Array.isArray || function(obj) {
|
||
return Object.prototype.toString.call(obj) == '[object Array]';
|
||
};
|
||
|
||
var forEach;
|
||
// Don't mess with Array.prototype. Its not friendly
|
||
if ( Array.prototype.forEach ) {
|
||
forEach = function( arr, cb, thisp ) {
|
||
return arr.forEach( cb, thisp );
|
||
};
|
||
}
|
||
else {
|
||
forEach = function(arr, cb, thisp) {
|
||
for (var i = 0; i < arr.length; i++) {
|
||
cb.call(thisp || arr, arr[i], i, arr);
|
||
}
|
||
}
|
||
}
|
||
|
||
function extract_attr( jsonml ) {
|
||
return isArray(jsonml)
|
||
&& jsonml.length > 1
|
||
&& typeof jsonml[ 1 ] === "object"
|
||
&& !( isArray(jsonml[ 1 ]) )
|
||
? jsonml[ 1 ]
|
||
: undefined;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* renderJsonML( jsonml[, options] ) -> String
|
||
* - jsonml (Array): JsonML array to render to XML
|
||
* - options (Object): options
|
||
*
|
||
* Converts the given JsonML into well-formed XML.
|
||
*
|
||
* The options currently understood are:
|
||
*
|
||
* - root (Boolean): wether or not the root node should be included in the
|
||
* output, or just its children. The default `false` is to not include the
|
||
* root itself.
|
||
*/
|
||
expose.renderJsonML = function( jsonml, options ) {
|
||
options = options || {};
|
||
// include the root element in the rendered output?
|
||
options.root = options.root || false;
|
||
|
||
var content = [];
|
||
|
||
if ( options.root ) {
|
||
content.push( render_tree( jsonml ) );
|
||
}
|
||
else {
|
||
jsonml.shift(); // get rid of the tag
|
||
if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) {
|
||
jsonml.shift(); // get rid of the attributes
|
||
}
|
||
|
||
while ( jsonml.length ) {
|
||
content.push( render_tree( jsonml.shift() ) );
|
||
}
|
||
}
|
||
|
||
return content.join( "\n\n" );
|
||
};
|
||
|
||
function escapeHTML( text ) {
|
||
return text.replace( /&/g, "&" )
|
||
.replace( /</g, "<" )
|
||
.replace( />/g, ">" )
|
||
.replace( /"/g, """ )
|
||
.replace( /'/g, "'" );
|
||
}
|
||
|
||
function render_tree( jsonml ) {
|
||
// basic case
|
||
if ( typeof jsonml === "string" ) {
|
||
return escapeHTML( jsonml );
|
||
}
|
||
|
||
var tag = jsonml.shift(),
|
||
attributes = {},
|
||
content = [];
|
||
|
||
if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) {
|
||
attributes = jsonml.shift();
|
||
}
|
||
|
||
while ( jsonml.length ) {
|
||
content.push( arguments.callee( jsonml.shift() ) );
|
||
}
|
||
|
||
var tag_attrs = "";
|
||
for ( var a in attributes ) {
|
||
tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"';
|
||
}
|
||
|
||
// be careful about adding whitespace here for inline elements
|
||
if ( tag == "img" || tag == "br" || tag == "hr" ) {
|
||
return "<"+ tag + tag_attrs + "/>";
|
||
}
|
||
else {
|
||
return "<"+ tag + tag_attrs + ">" + content.join( "" ) + "</" + tag + ">";
|
||
}
|
||
}
|
||
|
||
function convert_tree_to_html( tree, references, options ) {
|
||
var i;
|
||
options = options || {};
|
||
|
||
// shallow clone
|
||
var jsonml = tree.slice( 0 );
|
||
|
||
if (typeof options.preprocessTreeNode === "function") {
|
||
jsonml = options.preprocessTreeNode(jsonml, references);
|
||
}
|
||
|
||
// Clone attributes if they exist
|
||
var attrs = extract_attr( jsonml );
|
||
if ( attrs ) {
|
||
jsonml[ 1 ] = {};
|
||
for ( i in attrs ) {
|
||
jsonml[ 1 ][ i ] = attrs[ i ];
|
||
}
|
||
attrs = jsonml[ 1 ];
|
||
}
|
||
|
||
// basic case
|
||
if ( typeof jsonml === "string" ) {
|
||
return jsonml;
|
||
}
|
||
|
||
// convert this node
|
||
switch ( jsonml[ 0 ] ) {
|
||
case "header":
|
||
jsonml[ 0 ] = "h" + jsonml[ 1 ].level;
|
||
delete jsonml[ 1 ].level;
|
||
break;
|
||
case "bulletlist":
|
||
jsonml[ 0 ] = "ul";
|
||
break;
|
||
case "numberlist":
|
||
jsonml[ 0 ] = "ol";
|
||
break;
|
||
case "listitem":
|
||
jsonml[ 0 ] = "li";
|
||
break;
|
||
case "para":
|
||
jsonml[ 0 ] = "p";
|
||
break;
|
||
case "markdown":
|
||
jsonml[ 0 ] = "html";
|
||
if ( attrs ) delete attrs.references;
|
||
break;
|
||
case "code_block":
|
||
jsonml[ 0 ] = "pre";
|
||
i = attrs ? 2 : 1;
|
||
var code = [ "code" ];
|
||
code.push.apply( code, jsonml.splice( i ) );
|
||
jsonml[ i ] = code;
|
||
break;
|
||
case "inlinecode":
|
||
jsonml[ 0 ] = "code";
|
||
break;
|
||
case "img":
|
||
jsonml[ 1 ].src = jsonml[ 1 ].href;
|
||
delete jsonml[ 1 ].href;
|
||
break;
|
||
case "linebreak":
|
||
jsonml[ 0 ] = "br";
|
||
break;
|
||
case "link":
|
||
jsonml[ 0 ] = "a";
|
||
break;
|
||
case "link_ref":
|
||
jsonml[ 0 ] = "a";
|
||
|
||
// grab this ref and clean up the attribute node
|
||
var ref = references[ attrs.ref ];
|
||
|
||
// if the reference exists, make the link
|
||
if ( ref ) {
|
||
delete attrs.ref;
|
||
|
||
// add in the href and title, if present
|
||
attrs.href = ref.href;
|
||
if ( ref.title ) {
|
||
attrs.title = ref.title;
|
||
}
|
||
|
||
// get rid of the unneeded original text
|
||
delete attrs.original;
|
||
}
|
||
// the reference doesn't exist, so revert to plain text
|
||
else {
|
||
return attrs.original;
|
||
}
|
||
break;
|
||
case "img_ref":
|
||
jsonml[ 0 ] = "img";
|
||
|
||
// grab this ref and clean up the attribute node
|
||
var ref = references[ attrs.ref ];
|
||
|
||
// if the reference exists, make the link
|
||
if ( ref ) {
|
||
delete attrs.ref;
|
||
|
||
// add in the href and title, if present
|
||
attrs.src = ref.href;
|
||
if ( ref.title ) {
|
||
attrs.title = ref.title;
|
||
}
|
||
|
||
// get rid of the unneeded original text
|
||
delete attrs.original;
|
||
}
|
||
// the reference doesn't exist, so revert to plain text
|
||
else {
|
||
return attrs.original;
|
||
}
|
||
break;
|
||
}
|
||
|
||
// convert all the children
|
||
i = 1;
|
||
|
||
// deal with the attribute node, if it exists
|
||
if ( attrs ) {
|
||
// if there are keys, skip over it
|
||
for ( var key in jsonml[ 1 ] ) {
|
||
i = 2;
|
||
}
|
||
// if there aren't, remove it
|
||
if ( i === 1 ) {
|
||
jsonml.splice( i, 1 );
|
||
}
|
||
}
|
||
|
||
for ( ; i < jsonml.length; ++i ) {
|
||
jsonml[ i ] = arguments.callee( jsonml[ i ], references, options );
|
||
}
|
||
|
||
return jsonml;
|
||
}
|
||
|
||
|
||
// merges adjacent text nodes into a single node
|
||
function merge_text_nodes( jsonml ) {
|
||
// skip the tag name and attribute hash
|
||
var i = extract_attr( jsonml ) ? 2 : 1;
|
||
|
||
while ( i < jsonml.length ) {
|
||
// if it's a string check the next item too
|
||
if ( typeof jsonml[ i ] === "string" ) {
|
||
if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) {
|
||
// merge the second string into the first and remove it
|
||
jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ];
|
||
}
|
||
else {
|
||
++i;
|
||
}
|
||
}
|
||
// if it's not a string recurse
|
||
else {
|
||
arguments.callee( jsonml[ i ] );
|
||
++i;
|
||
}
|
||
}
|
||
}
|
||
|
||
} )( (function() {
|
||
if ( typeof exports === "undefined" ) {
|
||
window.markdown = {};
|
||
return window.markdown;
|
||
}
|
||
else {
|
||
return exports;
|
||
}
|
||
} )() );
|
||
|
||
});
|
||
|
||
require.define("util",function(require,module,exports,__dirname,__filename,process,global){var events = require('events');
|
||
|
||
exports.isArray = isArray;
|
||
exports.isDate = function(obj){return Object.prototype.toString.call(obj) === '[object Date]'};
|
||
exports.isRegExp = function(obj){return Object.prototype.toString.call(obj) === '[object RegExp]'};
|
||
|
||
|
||
exports.print = function () {};
|
||
exports.puts = function () {};
|
||
exports.debug = function() {};
|
||
|
||
exports.inspect = function(obj, showHidden, depth, colors) {
|
||
var seen = [];
|
||
|
||
var stylize = function(str, styleType) {
|
||
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
|
||
var styles =
|
||
{ 'bold' : [1, 22],
|
||
'italic' : [3, 23],
|
||
'underline' : [4, 24],
|
||
'inverse' : [7, 27],
|
||
'white' : [37, 39],
|
||
'grey' : [90, 39],
|
||
'black' : [30, 39],
|
||
'blue' : [34, 39],
|
||
'cyan' : [36, 39],
|
||
'green' : [32, 39],
|
||
'magenta' : [35, 39],
|
||
'red' : [31, 39],
|
||
'yellow' : [33, 39] };
|
||
|
||
var style =
|
||
{ 'special': 'cyan',
|
||
'number': 'blue',
|
||
'boolean': 'yellow',
|
||
'undefined': 'grey',
|
||
'null': 'bold',
|
||
'string': 'green',
|
||
'date': 'magenta',
|
||
// "name": intentionally not styling
|
||
'regexp': 'red' }[styleType];
|
||
|
||
if (style) {
|
||
return '\033[' + styles[style][0] + 'm' + str +
|
||
'\033[' + styles[style][1] + 'm';
|
||
} else {
|
||
return str;
|
||
}
|
||
};
|
||
if (! colors) {
|
||
stylize = function(str, styleType) { return str; };
|
||
}
|
||
|
||
function format(value, recurseTimes) {
|
||
// Provide a hook for user-specified inspect functions.
|
||
// Check that value is an object with an inspect function on it
|
||
if (value && typeof value.inspect === 'function' &&
|
||
// Filter out the util module, it's inspect function is special
|
||
value !== exports &&
|
||
// Also filter out any prototype objects using the circular check.
|
||
!(value.constructor && value.constructor.prototype === value)) {
|
||
return value.inspect(recurseTimes);
|
||
}
|
||
|
||
// Primitive types cannot have properties
|
||
switch (typeof value) {
|
||
case 'undefined':
|
||
return stylize('undefined', 'undefined');
|
||
|
||
case 'string':
|
||
var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
|
||
.replace(/'/g, "\\'")
|
||
.replace(/\\"/g, '"') + '\'';
|
||
return stylize(simple, 'string');
|
||
|
||
case 'number':
|
||
return stylize('' + value, 'number');
|
||
|
||
case 'boolean':
|
||
return stylize('' + value, 'boolean');
|
||
}
|
||
// For some reason typeof null is "object", so special case here.
|
||
if (value === null) {
|
||
return stylize('null', 'null');
|
||
}
|
||
|
||
// Look up the keys of the object.
|
||
var visible_keys = Object_keys(value);
|
||
var keys = showHidden ? Object_getOwnPropertyNames(value) : visible_keys;
|
||
|
||
// Functions without properties can be shortcutted.
|
||
if (typeof value === 'function' && keys.length === 0) {
|
||
if (isRegExp(value)) {
|
||
return stylize('' + value, 'regexp');
|
||
} else {
|
||
var name = value.name ? ': ' + value.name : '';
|
||
return stylize('[Function' + name + ']', 'special');
|
||
}
|
||
}
|
||
|
||
// Dates without properties can be shortcutted
|
||
if (isDate(value) && keys.length === 0) {
|
||
return stylize(value.toUTCString(), 'date');
|
||
}
|
||
|
||
var base, type, braces;
|
||
// Determine the object type
|
||
if (isArray(value)) {
|
||
type = 'Array';
|
||
braces = ['[', ']'];
|
||
} else {
|
||
type = 'Object';
|
||
braces = ['{', '}'];
|
||
}
|
||
|
||
// Make functions say that they are functions
|
||
if (typeof value === 'function') {
|
||
var n = value.name ? ': ' + value.name : '';
|
||
base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']';
|
||
} else {
|
||
base = '';
|
||
}
|
||
|
||
// Make dates with properties first say the date
|
||
if (isDate(value)) {
|
||
base = ' ' + value.toUTCString();
|
||
}
|
||
|
||
if (keys.length === 0) {
|
||
return braces[0] + base + braces[1];
|
||
}
|
||
|
||
if (recurseTimes < 0) {
|
||
if (isRegExp(value)) {
|
||
return stylize('' + value, 'regexp');
|
||
} else {
|
||
return stylize('[Object]', 'special');
|
||
}
|
||
}
|
||
|
||
seen.push(value);
|
||
|
||
var output = keys.map(function(key) {
|
||
var name, str;
|
||
if (value.__lookupGetter__) {
|
||
if (value.__lookupGetter__(key)) {
|
||
if (value.__lookupSetter__(key)) {
|
||
str = stylize('[Getter/Setter]', 'special');
|
||
} else {
|
||
str = stylize('[Getter]', 'special');
|
||
}
|
||
} else {
|
||
if (value.__lookupSetter__(key)) {
|
||
str = stylize('[Setter]', 'special');
|
||
}
|
||
}
|
||
}
|
||
if (visible_keys.indexOf(key) < 0) {
|
||
name = '[' + key + ']';
|
||
}
|
||
if (!str) {
|
||
if (seen.indexOf(value[key]) < 0) {
|
||
if (recurseTimes === null) {
|
||
str = format(value[key]);
|
||
} else {
|
||
str = format(value[key], recurseTimes - 1);
|
||
}
|
||
if (str.indexOf('\n') > -1) {
|
||
if (isArray(value)) {
|
||
str = str.split('\n').map(function(line) {
|
||
return ' ' + line;
|
||
}).join('\n').substr(2);
|
||
} else {
|
||
str = '\n' + str.split('\n').map(function(line) {
|
||
return ' ' + line;
|
||
}).join('\n');
|
||
}
|
||
}
|
||
} else {
|
||
str = stylize('[Circular]', 'special');
|
||
}
|
||
}
|
||
if (typeof name === 'undefined') {
|
||
if (type === 'Array' && key.match(/^\d+$/)) {
|
||
return str;
|
||
}
|
||
name = JSON.stringify('' + key);
|
||
if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
|
||
name = name.substr(1, name.length - 2);
|
||
name = stylize(name, 'name');
|
||
} else {
|
||
name = name.replace(/'/g, "\\'")
|
||
.replace(/\\"/g, '"')
|
||
.replace(/(^"|"$)/g, "'");
|
||
name = stylize(name, 'string');
|
||
}
|
||
}
|
||
|
||
return name + ': ' + str;
|
||
});
|
||
|
||
seen.pop();
|
||
|
||
var numLinesEst = 0;
|
||
var length = output.reduce(function(prev, cur) {
|
||
numLinesEst++;
|
||
if (cur.indexOf('\n') >= 0) numLinesEst++;
|
||
return prev + cur.length + 1;
|
||
}, 0);
|
||
|
||
if (length > 50) {
|
||
output = braces[0] +
|
||
(base === '' ? '' : base + '\n ') +
|
||
' ' +
|
||
output.join(',\n ') +
|
||
' ' +
|
||
braces[1];
|
||
|
||
} else {
|
||
output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
|
||
}
|
||
|
||
return output;
|
||
}
|
||
return format(obj, (typeof depth === 'undefined' ? 2 : depth));
|
||
};
|
||
|
||
|
||
function isArray(ar) {
|
||
return ar instanceof Array ||
|
||
Array.isArray(ar) ||
|
||
(ar && ar !== Object.prototype && isArray(ar.__proto__));
|
||
}
|
||
|
||
|
||
function isRegExp(re) {
|
||
return re instanceof RegExp ||
|
||
(typeof re === 'object' && Object.prototype.toString.call(re) === '[object RegExp]');
|
||
}
|
||
|
||
|
||
function isDate(d) {
|
||
if (d instanceof Date) return true;
|
||
if (typeof d !== 'object') return false;
|
||
var properties = Date.prototype && Object_getOwnPropertyNames(Date.prototype);
|
||
var proto = d.__proto__ && Object_getOwnPropertyNames(d.__proto__);
|
||
return JSON.stringify(proto) === JSON.stringify(properties);
|
||
}
|
||
|
||
function pad(n) {
|
||
return n < 10 ? '0' + n.toString(10) : n.toString(10);
|
||
}
|
||
|
||
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
|
||
'Oct', 'Nov', 'Dec'];
|
||
|
||
// 26 Feb 16:19:34
|
||
function timestamp() {
|
||
var d = new Date();
|
||
var time = [pad(d.getHours()),
|
||
pad(d.getMinutes()),
|
||
pad(d.getSeconds())].join(':');
|
||
return [d.getDate(), months[d.getMonth()], time].join(' ');
|
||
}
|
||
|
||
exports.log = function (msg) {};
|
||
|
||
exports.pump = null;
|
||
|
||
var Object_keys = Object.keys || function (obj) {
|
||
var res = [];
|
||
for (var key in obj) res.push(key);
|
||
return res;
|
||
};
|
||
|
||
var Object_getOwnPropertyNames = Object.getOwnPropertyNames || function (obj) {
|
||
var res = [];
|
||
for (var key in obj) {
|
||
if (Object.hasOwnProperty.call(obj, key)) res.push(key);
|
||
}
|
||
return res;
|
||
};
|
||
|
||
var Object_create = Object.create || function (prototype, properties) {
|
||
// from es5-shim
|
||
var object;
|
||
if (prototype === null) {
|
||
object = { '__proto__' : null };
|
||
}
|
||
else {
|
||
if (typeof prototype !== 'object') {
|
||
throw new TypeError(
|
||
'typeof prototype[' + (typeof prototype) + '] != \'object\''
|
||
);
|
||
}
|
||
var Type = function () {};
|
||
Type.prototype = prototype;
|
||
object = new Type();
|
||
object.__proto__ = prototype;
|
||
}
|
||
if (typeof properties !== 'undefined' && Object.defineProperties) {
|
||
Object.defineProperties(object, properties);
|
||
}
|
||
return object;
|
||
};
|
||
|
||
exports.inherits = function(ctor, superCtor) {
|
||
ctor.super_ = superCtor;
|
||
ctor.prototype = Object_create(superCtor.prototype, {
|
||
constructor: {
|
||
value: ctor,
|
||
enumerable: false,
|
||
writable: true,
|
||
configurable: true
|
||
}
|
||
});
|
||
};
|
||
|
||
var formatRegExp = /%[sdj%]/g;
|
||
exports.format = function(f) {
|
||
if (typeof f !== 'string') {
|
||
var objects = [];
|
||
for (var i = 0; i < arguments.length; i++) {
|
||
objects.push(exports.inspect(arguments[i]));
|
||
}
|
||
return objects.join(' ');
|
||
}
|
||
|
||
var i = 1;
|
||
var args = arguments;
|
||
var len = args.length;
|
||
var str = String(f).replace(formatRegExp, function(x) {
|
||
if (x === '%%') return '%';
|
||
if (i >= len) return x;
|
||
switch (x) {
|
||
case '%s': return String(args[i++]);
|
||
case '%d': return Number(args[i++]);
|
||
case '%j': return JSON.stringify(args[i++]);
|
||
default:
|
||
return x;
|
||
}
|
||
});
|
||
for(var x = args[i]; i < len; x = args[++i]){
|
||
if (x === null || typeof x !== 'object') {
|
||
str += ' ' + x;
|
||
} else {
|
||
str += ' ' + exports.inspect(x);
|
||
}
|
||
}
|
||
return str;
|
||
};
|
||
|
||
});
|
||
|
||
require.define("events",function(require,module,exports,__dirname,__filename,process,global){if (!process.EventEmitter) process.EventEmitter = function () {};
|
||
|
||
var EventEmitter = exports.EventEmitter = process.EventEmitter;
|
||
var isArray = typeof Array.isArray === 'function'
|
||
? Array.isArray
|
||
: function (xs) {
|
||
return Object.prototype.toString.call(xs) === '[object Array]'
|
||
}
|
||
;
|
||
function indexOf (xs, x) {
|
||
if (xs.indexOf) return xs.indexOf(x);
|
||
for (var i = 0; i < xs.length; i++) {
|
||
if (x === xs[i]) return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
// By default EventEmitters will print a warning if more than
|
||
// 10 listeners are added to it. This is a useful default which
|
||
// helps finding memory leaks.
|
||
//
|
||
// Obviously not all Emitters should be limited to 10. This function allows
|
||
// that to be increased. Set to zero for unlimited.
|
||
var defaultMaxListeners = 10;
|
||
EventEmitter.prototype.setMaxListeners = function(n) {
|
||
if (!this._events) this._events = {};
|
||
this._events.maxListeners = n;
|
||
};
|
||
|
||
|
||
EventEmitter.prototype.emit = function(type) {
|
||
// If there is no 'error' event listener then throw.
|
||
if (type === 'error') {
|
||
if (!this._events || !this._events.error ||
|
||
(isArray(this._events.error) && !this._events.error.length))
|
||
{
|
||
if (arguments[1] instanceof Error) {
|
||
throw arguments[1]; // Unhandled 'error' event
|
||
} else {
|
||
throw new Error("Uncaught, unspecified 'error' event.");
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!this._events) return false;
|
||
var handler = this._events[type];
|
||
if (!handler) return false;
|
||
|
||
if (typeof handler == 'function') {
|
||
switch (arguments.length) {
|
||
// fast cases
|
||
case 1:
|
||
handler.call(this);
|
||
break;
|
||
case 2:
|
||
handler.call(this, arguments[1]);
|
||
break;
|
||
case 3:
|
||
handler.call(this, arguments[1], arguments[2]);
|
||
break;
|
||
// slower
|
||
default:
|
||
var args = Array.prototype.slice.call(arguments, 1);
|
||
handler.apply(this, args);
|
||
}
|
||
return true;
|
||
|
||
} else if (isArray(handler)) {
|
||
var args = Array.prototype.slice.call(arguments, 1);
|
||
|
||
var listeners = handler.slice();
|
||
for (var i = 0, l = listeners.length; i < l; i++) {
|
||
listeners[i].apply(this, args);
|
||
}
|
||
return true;
|
||
|
||
} else {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// EventEmitter is defined in src/node_events.cc
|
||
// EventEmitter.prototype.emit() is also defined there.
|
||
EventEmitter.prototype.addListener = function(type, listener) {
|
||
if ('function' !== typeof listener) {
|
||
throw new Error('addListener only takes instances of Function');
|
||
}
|
||
|
||
if (!this._events) this._events = {};
|
||
|
||
// To avoid recursion in the case that type == "newListeners"! Before
|
||
// adding it to the listeners, first emit "newListeners".
|
||
this.emit('newListener', type, listener);
|
||
|
||
if (!this._events[type]) {
|
||
// Optimize the case of one listener. Don't need the extra array object.
|
||
this._events[type] = listener;
|
||
} else if (isArray(this._events[type])) {
|
||
|
||
// Check for listener leak
|
||
if (!this._events[type].warned) {
|
||
var m;
|
||
if (this._events.maxListeners !== undefined) {
|
||
m = this._events.maxListeners;
|
||
} else {
|
||
m = defaultMaxListeners;
|
||
}
|
||
|
||
if (m && m > 0 && this._events[type].length > m) {
|
||
this._events[type].warned = true;
|
||
console.error('(node) warning: possible EventEmitter memory ' +
|
||
'leak detected. %d listeners added. ' +
|
||
'Use emitter.setMaxListeners() to increase limit.',
|
||
this._events[type].length);
|
||
console.trace();
|
||
}
|
||
}
|
||
|
||
// If we've already got an array, just append.
|
||
this._events[type].push(listener);
|
||
} else {
|
||
// Adding the second element, need to change to array.
|
||
this._events[type] = [this._events[type], listener];
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
||
|
||
EventEmitter.prototype.once = function(type, listener) {
|
||
var self = this;
|
||
self.on(type, function g() {
|
||
self.removeListener(type, g);
|
||
listener.apply(this, arguments);
|
||
});
|
||
|
||
return this;
|
||
};
|
||
|
||
EventEmitter.prototype.removeListener = function(type, listener) {
|
||
if ('function' !== typeof listener) {
|
||
throw new Error('removeListener only takes instances of Function');
|
||
}
|
||
|
||
// does not use listeners(), so no side effect of creating _events[type]
|
||
if (!this._events || !this._events[type]) return this;
|
||
|
||
var list = this._events[type];
|
||
|
||
if (isArray(list)) {
|
||
var i = indexOf(list, listener);
|
||
if (i < 0) return this;
|
||
list.splice(i, 1);
|
||
if (list.length == 0)
|
||
delete this._events[type];
|
||
} else if (this._events[type] === listener) {
|
||
delete this._events[type];
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
EventEmitter.prototype.removeAllListeners = function(type) {
|
||
// does not use listeners(), so no side effect of creating _events[type]
|
||
if (type && this._events && this._events[type]) this._events[type] = null;
|
||
return this;
|
||
};
|
||
|
||
EventEmitter.prototype.listeners = function(type) {
|
||
if (!this._events) this._events = {};
|
||
if (!this._events[type]) this._events[type] = [];
|
||
if (!isArray(this._events[type])) {
|
||
this._events[type] = [this._events[type]];
|
||
}
|
||
return this._events[type];
|
||
};
|
||
|
||
});
|
||
|
||
require.define("/src/js/models/commandModel.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
|
||
var Errors = require('../util/errors');
|
||
var GitCommands = require('../git/commands');
|
||
var GitOptionParser = GitCommands.GitOptionParser;
|
||
|
||
var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
|
||
|
||
var CommandProcessError = Errors.CommandProcessError;
|
||
var GitError = Errors.GitError;
|
||
var Warning = Errors.Warning;
|
||
var CommandResult = Errors.CommandResult;
|
||
|
||
var Command = Backbone.Model.extend({
|
||
defaults: {
|
||
status: 'inqueue',
|
||
rawStr: null,
|
||
result: '',
|
||
createTime: null,
|
||
|
||
error: null,
|
||
warnings: null,
|
||
parseWaterfall: new ParseWaterfall(),
|
||
|
||
generalArgs: null,
|
||
supportedMap: null,
|
||
options: null,
|
||
method: null
|
||
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.initDefaults();
|
||
this.validateAtInit();
|
||
|
||
this.on('change:error', this.errorChanged, this);
|
||
// catch errors on init
|
||
if (this.get('error')) {
|
||
this.errorChanged();
|
||
}
|
||
|
||
this.parseOrCatch();
|
||
},
|
||
|
||
initDefaults: function() {
|
||
// weird things happen with defaults if you dont
|
||
// make new objects
|
||
this.set('generalArgs', []);
|
||
this.set('supportedMap', {});
|
||
this.set('warnings', []);
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (this.get('rawStr') === null) {
|
||
throw new Error('Give me a string!');
|
||
}
|
||
if (!this.get('createTime')) {
|
||
this.set('createTime', new Date().toString());
|
||
}
|
||
},
|
||
|
||
setResult: function(msg) {
|
||
this.set('result', msg);
|
||
},
|
||
|
||
finishWith: function(deferred) {
|
||
this.set('status', 'finished');
|
||
deferred.resolve();
|
||
},
|
||
|
||
addWarning: function(msg) {
|
||
this.get('warnings').push(msg);
|
||
// change numWarnings so the change event fires. This is bizarre -- Backbone can't
|
||
// detect if an array changes, so adding an element does nothing
|
||
this.set('numWarnings', this.get('numWarnings') ? this.get('numWarnings') + 1 : 1);
|
||
},
|
||
|
||
getFormattedWarnings: function() {
|
||
if (!this.get('warnings').length) {
|
||
return '';
|
||
}
|
||
var i = '<i class="icon-exclamation-sign"></i>';
|
||
return '<p>' + i + this.get('warnings').join('</p><p>' + i) + '</p>';
|
||
},
|
||
|
||
parseOrCatch: function() {
|
||
this.expandShortcuts(this.get('rawStr'));
|
||
try {
|
||
this.processInstants();
|
||
} catch (err) {
|
||
Errors.filterError(err);
|
||
// errorChanged() will handle status and all of that
|
||
this.set('error', err);
|
||
return;
|
||
}
|
||
|
||
if (this.parseAll()) {
|
||
// something in our parse waterfall succeeded
|
||
return;
|
||
}
|
||
|
||
// if we reach here, this command is not supported :-/
|
||
this.set('error', new CommandProcessError({
|
||
msg: 'The command "' + this.get('rawStr') + '" isn\'t supported, sorry!'
|
||
})
|
||
);
|
||
},
|
||
|
||
errorChanged: function() {
|
||
var err = this.get('error');
|
||
if (err instanceof CommandProcessError ||
|
||
err instanceof GitError) {
|
||
this.set('status', 'error');
|
||
} else if (err instanceof CommandResult) {
|
||
this.set('status', 'finished');
|
||
} else if (err instanceof Warning) {
|
||
this.set('status', 'warning');
|
||
}
|
||
this.formatError();
|
||
},
|
||
|
||
formatError: function() {
|
||
this.set('result', this.get('error').toResult());
|
||
},
|
||
|
||
expandShortcuts: function(str) {
|
||
str = this.get('parseWaterfall').expandAllShortcuts(str);
|
||
this.set('rawStr', str);
|
||
},
|
||
|
||
processInstants: 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 instant commands that will throw
|
||
this.get('parseWaterfall').processAllInstants(str);
|
||
},
|
||
|
||
parseAll: function() {
|
||
var str = this.get('rawStr');
|
||
var results = this.get('parseWaterfall').parseAll(str);
|
||
|
||
if (!results) {
|
||
// nothing parsed successfully
|
||
return false;
|
||
}
|
||
|
||
_.each(results.toSet, function(obj, key) {
|
||
// data comes back from the parsing functions like
|
||
// options (etc) that need to be set
|
||
this.set(key, obj);
|
||
}, this);
|
||
return true;
|
||
}
|
||
});
|
||
|
||
// command entry is for the commandview
|
||
var CommandEntry = Backbone.Model.extend({
|
||
defaults: {
|
||
text: ''
|
||
},
|
||
// stub out if no plugin available
|
||
localStorage: (Backbone.LocalStorage) ? new Backbone.LocalStorage('CommandEntries') : null
|
||
});
|
||
|
||
exports.CommandEntry = CommandEntry;
|
||
exports.Command = Command;
|
||
|
||
});
|
||
|
||
require.define("/src/js/git/commands.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var Errors = require('../util/errors');
|
||
var CommandProcessError = Errors.CommandProcessError;
|
||
var GitError = Errors.GitError;
|
||
var Warning = Errors.Warning;
|
||
var CommandResult = Errors.CommandResult;
|
||
|
||
var shortcutMap = {
|
||
'git commit': /^gc($|\s)/,
|
||
'git add': /^ga($|\s)/,
|
||
'git checkout': /^go($|\s)/,
|
||
'git rebase': /^gr($|\s)/,
|
||
'git branch': /^gb($|\s)/,
|
||
'git status': /^gs($|\s)/,
|
||
'git help': /^git$/
|
||
};
|
||
|
||
var instantCommands = [
|
||
[/^git help($|\s)/, function() {
|
||
var lines = [
|
||
'Git Version PCOTTLE.1.0',
|
||
'<br/>',
|
||
'Usage:',
|
||
_.escape('\t git <command> [<args>]'),
|
||
'<br/>',
|
||
'Supported commands:',
|
||
'<br/>'
|
||
];
|
||
var commands = GitOptionParser.prototype.getMasterOptionMap();
|
||
|
||
// build up a nice display of what we support
|
||
_.each(commands, function(commandOptions, command) {
|
||
lines.push('git ' + command);
|
||
_.each(commandOptions, function(vals, optionName) {
|
||
lines.push('\t ' + optionName);
|
||
}, this);
|
||
}, this);
|
||
|
||
// format and throw
|
||
var msg = lines.join('\n');
|
||
msg = msg.replace(/\t/g, ' ');
|
||
throw new CommandResult({
|
||
msg: msg
|
||
});
|
||
}]
|
||
];
|
||
|
||
var regexMap = {
|
||
// ($|\s) means that we either have to end the string
|
||
// after the command or there needs to be a space for options
|
||
'git commit': /^git commit($|\s)/,
|
||
'git add': /^git add($|\s)/,
|
||
'git checkout': /^git checkout($|\s)/,
|
||
'git rebase': /^git rebase($|\s)/,
|
||
'git reset': /^git reset($|\s)/,
|
||
'git branch': /^git branch($|\s)/,
|
||
'git revert': /^git revert($|\s)/,
|
||
'git log': /^git log($|\s)/,
|
||
'git merge': /^git merge($|\s)/,
|
||
'git show': /^git show($|\s)/,
|
||
'git status': /^git status($|\s)/,
|
||
'git cherry-pick': /^git cherry-pick($|\s)/
|
||
};
|
||
|
||
var parse = function(str) {
|
||
var method;
|
||
var options;
|
||
|
||
// see if we support this particular command
|
||
_.each(regexMap, function(regex, thisMethod) {
|
||
if (regex.exec(str)) {
|
||
options = str.slice(thisMethod.length + 1);
|
||
method = thisMethod.slice('git '.length);
|
||
}
|
||
});
|
||
|
||
if (!method) {
|
||
return false;
|
||
}
|
||
|
||
// we support this command!
|
||
// parse off the options and assemble the map / general args
|
||
var parsedOptions = new GitOptionParser(method, options);
|
||
return {
|
||
toSet: {
|
||
generalArgs: parsedOptions.generalArgs,
|
||
supportedMap: parsedOptions.supportedMap,
|
||
method: method,
|
||
options: options,
|
||
eventName: 'processGitCommand'
|
||
}
|
||
};
|
||
};
|
||
|
||
/**
|
||
* GitOptionParser
|
||
*/
|
||
function GitOptionParser(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();
|
||
}
|
||
|
||
GitOptionParser.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, // warning
|
||
'-m': false
|
||
},
|
||
status: {},
|
||
log: {},
|
||
add: {},
|
||
'cherry-pick': {},
|
||
branch: {
|
||
'-d': false,
|
||
'-D': false,
|
||
'-f': false,
|
||
'--contains': false
|
||
},
|
||
checkout: {
|
||
'-b': false,
|
||
'-B': false,
|
||
'-': false
|
||
},
|
||
reset: {
|
||
'--hard': false,
|
||
'--soft': false // this will raise an error but we catch it in gitEngine
|
||
},
|
||
merge: {},
|
||
rebase: {
|
||
'-i': false // the mother of all options
|
||
},
|
||
revert: {},
|
||
show: {}
|
||
};
|
||
};
|
||
|
||
GitOptionParser.prototype.explodeAndSet = function() {
|
||
// split on spaces, except when inside quotes
|
||
|
||
var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/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);
|
||
}
|
||
}
|
||
};
|
||
|
||
exports.shortcutMap = shortcutMap;
|
||
exports.instantCommands = instantCommands;
|
||
exports.parse = parse;
|
||
exports.regexMap = regexMap;
|
||
|
||
});
|
||
|
||
require.define("/src/js/level/parseWaterfall.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var GitCommands = require('../git/commands');
|
||
var SandboxCommands = require('../level/SandboxCommands');
|
||
|
||
// more or less a static class
|
||
function ParseWaterfall(options) {
|
||
options = options || {};
|
||
this.shortcutWaterfall = options.shortcutWaterfall || [
|
||
GitCommands.shortcutMap
|
||
];
|
||
|
||
this.instantWaterfall = options.instantWaterfall || [
|
||
GitCommands.instantCommands,
|
||
SandboxCommands.instantCommands
|
||
];
|
||
|
||
this.parseWaterfall = options.parseWaterfall || [
|
||
GitCommands.parse,
|
||
SandboxCommands.parse
|
||
];
|
||
}
|
||
|
||
ParseWaterfall.prototype.clone = function() {
|
||
return new ParseWaterfall({
|
||
shortcutWaterfall: this.shortcutWaterfall.slice(),
|
||
instantWaterfall: this.instantWaterfall.slice(),
|
||
parseWaterfall: this.parseWaterfall.slice()
|
||
});
|
||
};
|
||
|
||
ParseWaterfall.prototype.getWaterfallMap = function() {
|
||
return {
|
||
shortcutWaterfall: this.shortcutWaterfall,
|
||
instantWaterfall: this.instantWaterfall,
|
||
parseWaterfall: this.parseWaterfall
|
||
};
|
||
};
|
||
|
||
ParseWaterfall.prototype.addFirst = function(which, value) {
|
||
this.getWaterfallMap()[which].unshift(value);
|
||
};
|
||
|
||
ParseWaterfall.prototype.addLast = function(which, value) {
|
||
this.getWaterfallMap()[which].push(value);
|
||
};
|
||
|
||
ParseWaterfall.prototype.expandAllShortcuts = function(commandStr) {
|
||
_.each(this.shortcutWaterfall, function(shortcutMap) {
|
||
commandStr = this.expandShortcut(commandStr, shortcutMap);
|
||
}, this);
|
||
return commandStr;
|
||
};
|
||
|
||
ParseWaterfall.prototype.expandShortcut = function(commandStr, shortcutMap) {
|
||
_.each(shortcutMap, function(regex, method) {
|
||
var results = regex.exec(commandStr);
|
||
if (results) {
|
||
commandStr = method + ' ' + commandStr.slice(results[0].length);
|
||
}
|
||
});
|
||
return commandStr;
|
||
};
|
||
|
||
ParseWaterfall.prototype.processAllInstants = function(commandStr) {
|
||
_.each(this.instantWaterfall, function(instantCommands) {
|
||
this.processInstant(commandStr, instantCommands);
|
||
}, this);
|
||
};
|
||
|
||
ParseWaterfall.prototype.processInstant = function(commandStr, instantCommands) {
|
||
_.each(instantCommands, function(tuple) {
|
||
var regex = tuple[0];
|
||
var results = regex.exec(commandStr);
|
||
if (results) {
|
||
// this will throw a result because it's an instant
|
||
tuple[1](results);
|
||
}
|
||
});
|
||
};
|
||
|
||
ParseWaterfall.prototype.parseAll = function(commandStr) {
|
||
var toReturn = false;
|
||
_.each(this.parseWaterfall, function(parseFunc) {
|
||
var results = parseFunc(commandStr);
|
||
if (results) {
|
||
toReturn = results;
|
||
}
|
||
}, this);
|
||
|
||
return toReturn;
|
||
};
|
||
|
||
exports.ParseWaterfall = ParseWaterfall;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/level/SandboxCommands.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var GitCommands = require('../git/commands');
|
||
var GitOptionParser = GitCommands.GitOptionParser;
|
||
|
||
var Errors = require('../util/errors');
|
||
var CommandProcessError = Errors.CommandProcessError;
|
||
var GitError = Errors.GitError;
|
||
var Warning = Errors.Warning;
|
||
var CommandResult = Errors.CommandResult;
|
||
|
||
var instantCommands = [
|
||
[/^ls/, function() {
|
||
throw new CommandResult({
|
||
msg: "DontWorryAboutFilesInThisDemo.txt"
|
||
});
|
||
}],
|
||
[/^cd/, function() {
|
||
throw new CommandResult({
|
||
msg: "Directory Changed to '/directories/dont/matter/in/this/demo'"
|
||
});
|
||
}],
|
||
[/^refresh$/, function() {
|
||
var events = require('../app').getEvents();
|
||
|
||
events.trigger('refreshTree');
|
||
throw new CommandResult({
|
||
msg: "Refreshing tree..."
|
||
});
|
||
}],
|
||
[/^rollup (\d+)$/, function(bits) {
|
||
var events = require('../app').getEvents();
|
||
|
||
// go roll up these commands by joining them with semicolons
|
||
events.trigger('rollupCommands', bits[1]);
|
||
throw new CommandResult({
|
||
msg: 'Commands combined!'
|
||
});
|
||
}]
|
||
];
|
||
|
||
var regexMap = {
|
||
'help': /^help($|\s)|\?/
|
||
};
|
||
|
||
var parse = function(str) {
|
||
var sandboxMethod;
|
||
|
||
_.each(regexMap, function(regex, method) {
|
||
if (regex.test(str)) {
|
||
sandboxMethod = method;
|
||
}
|
||
});
|
||
|
||
return (!sandboxMethod) ? false : {
|
||
toSet: {
|
||
eventName: 'processSandboxCommand',
|
||
method: sandboxMethod
|
||
}
|
||
};
|
||
};
|
||
|
||
exports.instantCommands = instantCommands;
|
||
exports.parse = parse;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/visuals/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Q = require('q');
|
||
var Backbone = require('backbone');
|
||
|
||
var GRAPHICS = require('../util/constants').GRAPHICS;
|
||
var GLOBAL = require('../util/constants').GLOBAL;
|
||
|
||
var Collections = require('../models/collections');
|
||
var CommitCollection = Collections.CommitCollection;
|
||
var BranchCollection = Collections.BranchCollection;
|
||
|
||
var VisNode = require('../visuals/visNode').VisNode;
|
||
var VisBranch = require('../visuals/visBranch').VisBranch;
|
||
var VisBranchCollection = require('../visuals/visBranch').VisBranchCollection;
|
||
var VisEdge = require('../visuals/visEdge').VisEdge;
|
||
var VisEdgeCollection = require('../visuals/visEdge').VisEdgeCollection;
|
||
|
||
function GitVisuals(options) {
|
||
this.commitCollection = options.commitCollection;
|
||
this.branchCollection = options.branchCollection;
|
||
this.visNodeMap = {};
|
||
|
||
this.visEdgeCollection = new VisEdgeCollection();
|
||
this.visBranchCollection = new VisBranchCollection();
|
||
this.commitMap = {};
|
||
|
||
this.rootCommit = null;
|
||
this.branchStackMap = null;
|
||
this.upstreamBranchSet = null;
|
||
this.upstreamHeadSet = null;
|
||
|
||
this.paper = options.paper;
|
||
this.gitReady = false;
|
||
|
||
this.branchCollection.on('add', this.addBranchFromEvent, this);
|
||
this.branchCollection.on('remove', this.removeBranch, this);
|
||
this.deferred = [];
|
||
|
||
var Main = require('../app');
|
||
Main.getEvents().on('refreshTree', this.refreshTree, this);
|
||
}
|
||
|
||
GitVisuals.prototype.defer = function(action) {
|
||
this.deferred.push(action);
|
||
};
|
||
|
||
GitVisuals.prototype.deferFlush = function() {
|
||
_.each(this.deferred, function(action) {
|
||
action();
|
||
}, this);
|
||
this.deferred = [];
|
||
};
|
||
|
||
GitVisuals.prototype.resetAll = function() {
|
||
// make sure to copy these collections because we remove
|
||
// items in place and underscore is too dumb to detect length change
|
||
var edges = this.visEdgeCollection.toArray();
|
||
_.each(edges, function(visEdge) {
|
||
visEdge.remove();
|
||
}, this);
|
||
|
||
var branches = this.visBranchCollection.toArray();
|
||
_.each(branches, function(visBranch) {
|
||
visBranch.remove();
|
||
}, this);
|
||
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.remove();
|
||
}, this);
|
||
|
||
this.visEdgeCollection.reset();
|
||
this.visBranchCollection.reset();
|
||
|
||
this.visNodeMap = {};
|
||
this.rootCommit = null;
|
||
this.commitMap = {};
|
||
};
|
||
|
||
GitVisuals.prototype.assignGitEngine = function(gitEngine) {
|
||
this.gitEngine = gitEngine;
|
||
this.initHeadBranch();
|
||
this.deferFlush();
|
||
};
|
||
|
||
GitVisuals.prototype.initHeadBranch = function() {
|
||
// it's unfortaunte we have to do this, but the head branch
|
||
// is an edge case because it's not part of a collection so
|
||
// we can't use events to load or unload it. thus we have to call
|
||
// this ugly method which will be deleted one day
|
||
|
||
// seed this with the HEAD pseudo-branch
|
||
this.addBranchFromEvent(this.gitEngine.HEAD);
|
||
};
|
||
|
||
GitVisuals.prototype.getScreenPadding = function() {
|
||
// for now we return the node radius subtracted from the walls
|
||
return {
|
||
widthPadding: GRAPHICS.nodeRadius * 1.5,
|
||
heightPadding: GRAPHICS.nodeRadius * 1.5
|
||
};
|
||
};
|
||
|
||
GitVisuals.prototype.toScreenCoords = function(pos) {
|
||
if (!this.paper.width) {
|
||
throw new Error('being called too early for screen coords');
|
||
}
|
||
var padding = this.getScreenPadding();
|
||
|
||
var shrink = function(frac, total, padding) {
|
||
return padding + frac * (total - padding * 2);
|
||
};
|
||
|
||
return {
|
||
x: shrink(pos.x, this.paper.width, padding.widthPadding),
|
||
y: shrink(pos.y, this.paper.height, padding.heightPadding)
|
||
};
|
||
};
|
||
|
||
GitVisuals.prototype.animateAllAttrKeys = function(keys, attr, speed, easing) {
|
||
var deferred = Q.defer();
|
||
|
||
var animate = function(visObj) {
|
||
visObj.animateAttrKeys(keys, attr, speed, easing);
|
||
};
|
||
|
||
this.visBranchCollection.each(animate);
|
||
this.visEdgeCollection.each(animate);
|
||
_.each(this.visNodeMap, animate);
|
||
|
||
var time = (speed !== undefined) ? speed : GRAPHICS.defaultAnimationTime;
|
||
setTimeout(function() {
|
||
deferred.resolve();
|
||
}, time);
|
||
|
||
return deferred.promise;
|
||
};
|
||
|
||
GitVisuals.prototype.finishAnimation = function() {
|
||
var _this = this;
|
||
var deferred = Q.defer();
|
||
var animationDone = Q.defer();
|
||
var defaultTime = GRAPHICS.defaultAnimationTime;
|
||
var nodeRadius = GRAPHICS.nodeRadius;
|
||
|
||
var textString = 'Solved!!\n:D';
|
||
var text = null;
|
||
var makeText = _.bind(function() {
|
||
text = this.paper.text(
|
||
this.paper.width / 2,
|
||
this.paper.height / 2,
|
||
textString
|
||
);
|
||
text.attr({
|
||
opacity: 0,
|
||
'font-weight': 500,
|
||
'font-size': '32pt',
|
||
'font-family': 'Monaco, Courier, font-monospace',
|
||
stroke: '#000',
|
||
'stroke-width': 2,
|
||
fill: '#000'
|
||
});
|
||
text.animate({ opacity: 1 }, defaultTime);
|
||
}, this);
|
||
|
||
// this is a BIG ANIMATION but it ends up just being
|
||
// a sweet chain of promises but is pretty nice. this is
|
||
// after I discovered promises / deferred's. Unfortunately
|
||
// I wrote a lot of the git stuff before promises, so
|
||
// that's somewhat ugly
|
||
|
||
deferred.promise
|
||
// first fade out everything but circles
|
||
.then(_.bind(function() {
|
||
return this.animateAllAttrKeys(
|
||
{ exclude: ['circle'] },
|
||
{ opacity: 0 },
|
||
defaultTime * 1.1
|
||
);
|
||
}, this))
|
||
// then make circle radii bigger
|
||
.then(_.bind(function() {
|
||
return this.animateAllAttrKeys(
|
||
{ exclude: ['arrow', 'rect', 'path', 'text'] },
|
||
{ r: nodeRadius * 2 },
|
||
defaultTime * 1.5
|
||
);
|
||
}, this))
|
||
// then shrink em super fast
|
||
.then(_.bind(function() {
|
||
return this.animateAllAttrKeys(
|
||
{ exclude: ['arrow', 'rect', 'path', 'text'] },
|
||
{ r: nodeRadius * 0.75 },
|
||
defaultTime * 0.5
|
||
);
|
||
}, this))
|
||
// then explode them and display text
|
||
.then(_.bind(function() {
|
||
makeText();
|
||
return this.explodeNodes();
|
||
}, this))
|
||
.then(_.bind(function() {
|
||
return this.explodeNodes();
|
||
}, this))
|
||
// then fade circles (aka everything) in and back
|
||
.then(_.bind(function() {
|
||
return this.animateAllAttrKeys(
|
||
{ exclude: ['arrow', 'rect', 'path', 'text'] },
|
||
{},
|
||
defaultTime * 1.25
|
||
);
|
||
}, this))
|
||
// then fade everything in and remove text
|
||
.then(_.bind(function() {
|
||
text.animate({ opacity: 0 }, defaultTime, undefined, undefined, function() {
|
||
text.remove();
|
||
});
|
||
return this.animateAllAttrKeys(
|
||
{},
|
||
{}
|
||
);
|
||
}, this))
|
||
.then(function() {
|
||
animationDone.resolve();
|
||
})
|
||
.fail(function(reason) {
|
||
console.warn('animation error' + reason);
|
||
})
|
||
.done();
|
||
|
||
// start our animation chain right away
|
||
deferred.resolve();
|
||
return animationDone.promise;
|
||
};
|
||
|
||
GitVisuals.prototype.explodeNodes = function() {
|
||
var deferred = Q.defer();
|
||
var funcs = [];
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
funcs.push(visNode.getExplodeStepFunc());
|
||
});
|
||
|
||
var interval = setInterval(function() {
|
||
// object creation here is a bit ugly inside a loop,
|
||
// but the alternative is to just OR against a bunch
|
||
// of booleans which means the other stepFuncs
|
||
// are called unnecessarily when they have almost
|
||
// zero speed. would be interesting to see performance differences
|
||
var keepGoing = [];
|
||
_.each(funcs, function(func) {
|
||
if (func()) {
|
||
keepGoing.push(func);
|
||
}
|
||
});
|
||
|
||
if (!keepGoing.length) {
|
||
clearInterval(interval);
|
||
// next step :D wow I love promises
|
||
deferred.resolve();
|
||
return;
|
||
}
|
||
|
||
funcs = keepGoing;
|
||
}, 1/40);
|
||
|
||
return deferred.promise;
|
||
};
|
||
|
||
GitVisuals.prototype.animateAllFromAttrToAttr = function(fromSnapshot, toSnapshot, idsToOmit) {
|
||
var animate = function(obj) {
|
||
var id = obj.getID();
|
||
if (_.include(idsToOmit, id)) {
|
||
return;
|
||
}
|
||
|
||
if (!fromSnapshot[id] || !toSnapshot[id]) {
|
||
// its actually ok it doesnt exist yet
|
||
return;
|
||
}
|
||
obj.animateFromAttrToAttr(fromSnapshot[id], toSnapshot[id]);
|
||
};
|
||
|
||
this.visBranchCollection.each(animate);
|
||
this.visEdgeCollection.each(animate);
|
||
_.each(this.visNodeMap, animate);
|
||
};
|
||
|
||
/***************************************
|
||
== BEGIN Tree Calculation Parts ==
|
||
_ __ __ _
|
||
\\/ / \ \//_
|
||
\ \ / __| __
|
||
\ \___/ /_____/ /
|
||
| _______ \
|
||
\ ( ) / \_\
|
||
\ /
|
||
| |
|
||
| |
|
||
____+-_=+-^ ^+-=_=__________
|
||
|
||
^^ I drew that :D
|
||
|
||
**************************************/
|
||
|
||
GitVisuals.prototype.genSnapshot = function() {
|
||
this.fullCalc();
|
||
|
||
var snapshot = {};
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
snapshot[visNode.get('id')] = visNode.getAttributes();
|
||
}, this);
|
||
|
||
this.visBranchCollection.each(function(visBranch) {
|
||
snapshot[visBranch.getID()] = visBranch.getAttributes();
|
||
}, this);
|
||
|
||
this.visEdgeCollection.each(function(visEdge) {
|
||
snapshot[visEdge.getID()] = visEdge.getAttributes();
|
||
}, this);
|
||
|
||
return snapshot;
|
||
};
|
||
|
||
GitVisuals.prototype.refreshTree = function(speed) {
|
||
if (!this.gitReady) {
|
||
return;
|
||
}
|
||
|
||
// this method can only be called after graphics are rendered
|
||
this.fullCalc();
|
||
|
||
this.animateAll(speed);
|
||
};
|
||
|
||
GitVisuals.prototype.refreshTreeHarsh = function() {
|
||
this.fullCalc();
|
||
|
||
this.animateAll(0);
|
||
};
|
||
|
||
GitVisuals.prototype.animateAll = function(speed) {
|
||
this.zIndexReflow();
|
||
|
||
this.animateEdges(speed);
|
||
this.animateNodePositions(speed);
|
||
this.animateRefs(speed);
|
||
};
|
||
|
||
GitVisuals.prototype.fullCalc = function() {
|
||
this.calcTreeCoords();
|
||
this.calcGraphicsCoords();
|
||
};
|
||
|
||
GitVisuals.prototype.calcTreeCoords = function() {
|
||
// this method can only contain things that dont rely on graphics
|
||
if (!this.rootCommit) {
|
||
throw new Error('grr, no root commit!');
|
||
}
|
||
|
||
this.calcUpstreamSets();
|
||
this.calcBranchStacks();
|
||
|
||
this.calcDepth();
|
||
this.calcWidth();
|
||
};
|
||
|
||
GitVisuals.prototype.calcGraphicsCoords = function() {
|
||
this.visBranchCollection.each(function(visBranch) {
|
||
visBranch.updateName();
|
||
});
|
||
};
|
||
|
||
GitVisuals.prototype.calcUpstreamSets = function() {
|
||
this.upstreamBranchSet = this.gitEngine.getUpstreamBranchSet();
|
||
this.upstreamHeadSet = this.gitEngine.getUpstreamHeadSet();
|
||
};
|
||
|
||
GitVisuals.prototype.getCommitUpstreamBranches = function(commit) {
|
||
return this.branchStackMap[commit.get('id')];
|
||
};
|
||
|
||
GitVisuals.prototype.getBlendedHuesForCommit = function(commit) {
|
||
var branches = this.upstreamBranchSet[commit.get('id')];
|
||
if (!branches) {
|
||
throw new Error('that commit doesnt have upstream branches!');
|
||
}
|
||
|
||
return this.blendHuesFromBranchStack(branches);
|
||
};
|
||
|
||
GitVisuals.prototype.blendHuesFromBranchStack = function(branchStackArray) {
|
||
var hueStrings = [];
|
||
_.each(branchStackArray, function(branchWrapper) {
|
||
var fill = branchWrapper.obj.get('visBranch').get('fill');
|
||
|
||
if (fill.slice(0,3) !== 'hsb') {
|
||
// crap! convert
|
||
var color = Raphael.color(fill);
|
||
fill = 'hsb(' + String(color.h) + ',' + String(color.l);
|
||
fill = fill + ',' + String(color.s) + ')';
|
||
}
|
||
|
||
hueStrings.push(fill);
|
||
});
|
||
|
||
return blendHueStrings(hueStrings);
|
||
};
|
||
|
||
GitVisuals.prototype.getCommitUpstreamStatus = function(commit) {
|
||
if (!this.upstreamBranchSet) {
|
||
throw new Error("Can't calculate this yet!");
|
||
}
|
||
|
||
var id = commit.get('id');
|
||
var branch = this.upstreamBranchSet;
|
||
var head = this.upstreamHeadSet;
|
||
|
||
if (branch[id]) {
|
||
return 'branch';
|
||
} else if (head[id]) {
|
||
return 'head';
|
||
} else {
|
||
return 'none';
|
||
}
|
||
};
|
||
|
||
GitVisuals.prototype.calcBranchStacks = function() {
|
||
var branches = this.gitEngine.getBranches();
|
||
var map = {};
|
||
_.each(branches, function(branch) {
|
||
var thisId = branch.target.get('id');
|
||
|
||
map[thisId] = map[thisId] || [];
|
||
map[thisId].push(branch);
|
||
map[thisId].sort(function(a, b) {
|
||
var aId = a.obj.get('id');
|
||
var bId = b.obj.get('id');
|
||
if (aId == 'master' || bId == 'master') {
|
||
return aId == 'master' ? -1 : 1;
|
||
}
|
||
return aId.localeCompare(bId);
|
||
});
|
||
});
|
||
this.branchStackMap = map;
|
||
};
|
||
|
||
GitVisuals.prototype.calcWidth = function() {
|
||
this.maxWidthRecursive(this.rootCommit);
|
||
|
||
this.assignBoundsRecursive(this.rootCommit, 0, 1);
|
||
};
|
||
|
||
GitVisuals.prototype.maxWidthRecursive = function(commit) {
|
||
var childrenTotalWidth = 0;
|
||
_.each(commit.get('children'), function(child) {
|
||
// only include this if we are the "main" parent of
|
||
// this child
|
||
if (child.isMainParent(commit)) {
|
||
var childWidth = this.maxWidthRecursive(child);
|
||
childrenTotalWidth += childWidth;
|
||
}
|
||
}, this);
|
||
|
||
var maxWidth = Math.max(1, childrenTotalWidth);
|
||
commit.get('visNode').set('maxWidth', maxWidth);
|
||
return maxWidth;
|
||
};
|
||
|
||
GitVisuals.prototype.assignBoundsRecursive = function(commit, min, max) {
|
||
// I always center myself within my bounds
|
||
var myWidthPos = (min + max) / 2.0;
|
||
commit.get('visNode').get('pos').x = myWidthPos;
|
||
|
||
if (commit.get('children').length === 0) {
|
||
return;
|
||
}
|
||
|
||
// i have a certain length to divide up
|
||
var myLength = max - min;
|
||
// I will divide up that length based on my children's max width in a
|
||
// basic box-flex model
|
||
var totalFlex = 0;
|
||
var children = commit.get('children');
|
||
_.each(children, function(child) {
|
||
if (child.isMainParent(commit)) {
|
||
totalFlex += child.get('visNode').getMaxWidthScaled();
|
||
}
|
||
}, this);
|
||
|
||
var prevBound = min;
|
||
|
||
// now go through and do everything
|
||
// TODO: order so the max width children are in the middle!!
|
||
_.each(children, function(child) {
|
||
if (!child.isMainParent(commit)) {
|
||
return;
|
||
}
|
||
|
||
var flex = child.get('visNode').getMaxWidthScaled();
|
||
var portion = (flex / totalFlex) * myLength;
|
||
var childMin = prevBound;
|
||
var childMax = childMin + portion;
|
||
this.assignBoundsRecursive(child, childMin, childMax);
|
||
prevBound = childMax;
|
||
}, this);
|
||
};
|
||
|
||
GitVisuals.prototype.calcDepth = function() {
|
||
var maxDepth = this.calcDepthRecursive(this.rootCommit, 0);
|
||
if (maxDepth > 15) {
|
||
// issue warning
|
||
console.warn('graphics are degrading from too many layers');
|
||
}
|
||
|
||
var depthIncrement = this.getDepthIncrement(maxDepth);
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.setDepthBasedOn(depthIncrement);
|
||
}, this);
|
||
};
|
||
|
||
/***************************************
|
||
== END Tree Calculation ==
|
||
_ __ __ _
|
||
\\/ / \ \//_
|
||
\ \ / __| __
|
||
\ \___/ /_____/ /
|
||
| _______ \
|
||
\ ( ) / \_\
|
||
\ /
|
||
| |
|
||
| |
|
||
____+-_=+-^ ^+-=_=__________
|
||
|
||
^^ I drew that :D
|
||
|
||
**************************************/
|
||
|
||
GitVisuals.prototype.animateNodePositions = function(speed) {
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.animateUpdatedPosition(speed);
|
||
}, this);
|
||
};
|
||
|
||
GitVisuals.prototype.turnOnPaper = function() {
|
||
this.gitReady = false;
|
||
};
|
||
|
||
// does making an accessor method make it any less hacky? that is the true question
|
||
GitVisuals.prototype.turnOffPaper = function() {
|
||
this.gitReady = true;
|
||
};
|
||
|
||
GitVisuals.prototype.addBranchFromEvent = function(branch, collection, index) {
|
||
var action = _.bind(function() {
|
||
this.addBranch(branch);
|
||
}, this);
|
||
|
||
if (!this.gitEngine || !this.gitReady) {
|
||
this.defer(action);
|
||
} else {
|
||
action();
|
||
}
|
||
};
|
||
|
||
GitVisuals.prototype.addBranch = function(branch) {
|
||
var visBranch = new VisBranch({
|
||
branch: branch,
|
||
gitVisuals: this,
|
||
gitEngine: this.gitEngine
|
||
});
|
||
|
||
this.visBranchCollection.add(visBranch);
|
||
if (this.gitReady) {
|
||
visBranch.genGraphics(this.paper);
|
||
}
|
||
};
|
||
|
||
GitVisuals.prototype.removeVisBranch = function(visBranch) {
|
||
this.visBranchCollection.remove(visBranch);
|
||
};
|
||
|
||
GitVisuals.prototype.removeVisNode = function(visNode) {
|
||
this.visNodeMap[visNode.getID()] = undefined;
|
||
};
|
||
|
||
GitVisuals.prototype.removeVisEdge = function(visEdge) {
|
||
this.visEdgeCollection.remove(visEdge);
|
||
};
|
||
|
||
GitVisuals.prototype.animateRefs = function(speed) {
|
||
this.visBranchCollection.each(function(visBranch) {
|
||
visBranch.animateUpdatedPos(speed);
|
||
}, this);
|
||
};
|
||
|
||
GitVisuals.prototype.animateEdges = function(speed) {
|
||
this.visEdgeCollection.each(function(edge) {
|
||
edge.animateUpdatedPath(speed);
|
||
}, this);
|
||
};
|
||
|
||
GitVisuals.prototype.getDepthIncrement = function(maxDepth) {
|
||
// assume there are at least 7 layers until later
|
||
maxDepth = Math.max(maxDepth, 7);
|
||
var increment = 1.0 / maxDepth;
|
||
return increment;
|
||
};
|
||
|
||
GitVisuals.prototype.calcDepthRecursive = function(commit, depth) {
|
||
commit.get('visNode').setDepth(depth);
|
||
|
||
var children = commit.get('children');
|
||
var maxDepth = depth;
|
||
_.each(children, function(child) {
|
||
var d = this.calcDepthRecursive(child, depth + 1);
|
||
maxDepth = Math.max(d, maxDepth);
|
||
}, this);
|
||
|
||
return maxDepth;
|
||
};
|
||
|
||
// we debounce here so we aren't firing a resize call on every resize event
|
||
// but only after they stop
|
||
GitVisuals.prototype.canvasResize = _.debounce(function(width, height) {
|
||
// refresh when we are ready
|
||
if (GLOBAL.isAnimating) {
|
||
var Main = require('../app');
|
||
Main.getEventBaton().trigger('commandSubmitted', 'refresh');
|
||
} else {
|
||
this.refreshTree();
|
||
}
|
||
}, 200);
|
||
|
||
GitVisuals.prototype.addNode = function(id, commit) {
|
||
this.commitMap[id] = commit;
|
||
if (commit.get('rootCommit')) {
|
||
this.rootCommit = commit;
|
||
}
|
||
|
||
var visNode = new VisNode({
|
||
id: id,
|
||
commit: commit,
|
||
gitVisuals: this,
|
||
gitEngine: this.gitEngine
|
||
});
|
||
this.visNodeMap[id] = visNode;
|
||
|
||
if (this.gitReady) {
|
||
visNode.genGraphics(this.paper);
|
||
}
|
||
return visNode;
|
||
};
|
||
|
||
GitVisuals.prototype.addEdge = function(idTail, idHead) {
|
||
var visNodeTail = this.visNodeMap[idTail];
|
||
var visNodeHead = this.visNodeMap[idHead];
|
||
|
||
if (!visNodeTail || !visNodeHead) {
|
||
throw new Error('one of the ids in (' + idTail +
|
||
', ' + idHead + ') does not exist');
|
||
}
|
||
|
||
var edge = new VisEdge({
|
||
tail: visNodeTail,
|
||
head: visNodeHead,
|
||
gitVisuals: this,
|
||
gitEngine: this.gitEngine
|
||
});
|
||
this.visEdgeCollection.add(edge);
|
||
|
||
if (this.gitReady) {
|
||
edge.genGraphics(this.paper);
|
||
}
|
||
};
|
||
|
||
GitVisuals.prototype.zIndexReflow = function() {
|
||
this.visNodesFront();
|
||
this.visBranchesFront();
|
||
};
|
||
|
||
GitVisuals.prototype.visNodesFront = function() {
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.toFront();
|
||
});
|
||
};
|
||
|
||
GitVisuals.prototype.visBranchesFront = function() {
|
||
this.visBranchCollection.each(function(vBranch) {
|
||
vBranch.nonTextToFront();
|
||
vBranch.textToFront();
|
||
});
|
||
};
|
||
|
||
GitVisuals.prototype.drawTreeFromReload = function() {
|
||
this.gitReady = true;
|
||
// gen all the graphics we need
|
||
this.deferFlush();
|
||
|
||
this.calcTreeCoords();
|
||
};
|
||
|
||
GitVisuals.prototype.drawTreeFirstTime = function() {
|
||
this.gitReady = true;
|
||
this.calcTreeCoords();
|
||
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.genGraphics(this.paper);
|
||
}, this);
|
||
|
||
this.visEdgeCollection.each(function(edge) {
|
||
edge.genGraphics(this.paper);
|
||
}, this);
|
||
|
||
this.visBranchCollection.each(function(visBranch) {
|
||
visBranch.genGraphics(this.paper);
|
||
}, this);
|
||
|
||
this.zIndexReflow();
|
||
};
|
||
|
||
|
||
/************************
|
||
* Random util functions, some from liquidGraph
|
||
***********************/
|
||
function blendHueStrings(hueStrings) {
|
||
// assumes a sat of 0.7 and brightness of 1
|
||
|
||
var x = 0;
|
||
var y = 0;
|
||
var totalSat = 0;
|
||
var totalBright = 0;
|
||
var length = hueStrings.length;
|
||
|
||
_.each(hueStrings, function(hueString) {
|
||
var exploded = hueString.split('(')[1];
|
||
exploded = exploded.split(')')[0];
|
||
exploded = exploded.split(',');
|
||
|
||
totalSat += parseFloat(exploded[1]);
|
||
totalBright += parseFloat(exploded[2]);
|
||
var hue = parseFloat(exploded[0]);
|
||
|
||
var angle = hue * Math.PI * 2;
|
||
x += Math.cos(angle);
|
||
y += Math.sin(angle);
|
||
});
|
||
|
||
x = x / length;
|
||
y = y / length;
|
||
totalSat = totalSat / length;
|
||
totalBright = totalBright / length;
|
||
|
||
var hue = Math.atan2(y, x) / (Math.PI * 2); // could fail on 0's
|
||
if (hue < 0) {
|
||
hue = hue + 1;
|
||
}
|
||
return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')';
|
||
}
|
||
|
||
exports.GitVisuals = GitVisuals;
|
||
|
||
});
|
||
|
||
require.define("/src/js/visuals/visNode.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var GRAPHICS = require('../util/constants').GRAPHICS;
|
||
|
||
var VisBase = require('../visuals/tree').VisBase;
|
||
|
||
var VisNode = VisBase.extend({
|
||
defaults: {
|
||
depth: undefined,
|
||
maxWidth: null,
|
||
outgoingEdges: null,
|
||
|
||
circle: null,
|
||
text: null,
|
||
|
||
id: null,
|
||
pos: null,
|
||
radius: null,
|
||
|
||
commit: null,
|
||
animationSpeed: GRAPHICS.defaultAnimationTime,
|
||
animationEasing: GRAPHICS.defaultEasing,
|
||
|
||
fill: GRAPHICS.defaultNodeFill,
|
||
'stroke-width': GRAPHICS.defaultNodeStrokeWidth,
|
||
stroke: GRAPHICS.defaultNodeStroke
|
||
},
|
||
|
||
getID: function() {
|
||
return this.get('id');
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (!this.get('id')) {
|
||
throw new Error('need id for mapping');
|
||
}
|
||
if (!this.get('commit')) {
|
||
throw new Error('need commit for linking');
|
||
}
|
||
|
||
if (!this.get('pos')) {
|
||
this.set('pos', {
|
||
x: Math.random(),
|
||
y: Math.random()
|
||
});
|
||
}
|
||
},
|
||
|
||
initialize: function() {
|
||
this.validateAtInit();
|
||
// shorthand for the main objects
|
||
this.gitVisuals = this.get('gitVisuals');
|
||
this.gitEngine = this.get('gitEngine');
|
||
|
||
this.set('outgoingEdges', []);
|
||
},
|
||
|
||
setDepth: function(depth) {
|
||
// for merge commits we need to max the depths across all
|
||
this.set('depth', Math.max(this.get('depth') || 0, depth));
|
||
},
|
||
|
||
setDepthBasedOn: function(depthIncrement) {
|
||
if (this.get('depth') === undefined) {
|
||
debugger;
|
||
throw new Error('no depth yet!');
|
||
}
|
||
var pos = this.get('pos');
|
||
pos.y = this.get('depth') * depthIncrement;
|
||
},
|
||
|
||
getMaxWidthScaled: function() {
|
||
// returns our max width scaled based on if we are visible
|
||
// from a branch or not
|
||
var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit'));
|
||
var map = {
|
||
branch: 1,
|
||
head: 0.3,
|
||
none: 0.1
|
||
};
|
||
if (map[stat] === undefined) { throw new Error('bad stat'); }
|
||
return map[stat] * this.get('maxWidth');
|
||
},
|
||
|
||
toFront: function() {
|
||
this.get('circle').toFront();
|
||
this.get('text').toFront();
|
||
},
|
||
|
||
getOpacity: function() {
|
||
var map = {
|
||
'branch': 1,
|
||
'head': GRAPHICS.upstreamHeadOpacity,
|
||
'none': GRAPHICS.upstreamNoneOpacity
|
||
};
|
||
|
||
var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit'));
|
||
if (map[stat] === undefined) {
|
||
throw new Error('invalid status');
|
||
}
|
||
return map[stat];
|
||
},
|
||
|
||
getTextScreenCoords: function() {
|
||
return this.getScreenCoords();
|
||
},
|
||
|
||
getAttributes: function() {
|
||
var pos = this.getScreenCoords();
|
||
var textPos = this.getTextScreenCoords();
|
||
var opacity = this.getOpacity();
|
||
|
||
return {
|
||
circle: {
|
||
cx: pos.x,
|
||
cy: pos.y,
|
||
opacity: opacity,
|
||
r: this.getRadius(),
|
||
fill: this.getFill(),
|
||
'stroke-width': this.get('stroke-width'),
|
||
stroke: this.get('stroke')
|
||
},
|
||
text: {
|
||
x: textPos.x,
|
||
y: textPos.y,
|
||
opacity: opacity
|
||
}
|
||
};
|
||
},
|
||
|
||
highlightTo: function(visObj, speed, easing) {
|
||
// a small function to highlight the color of a node for demonstration purposes
|
||
var color = visObj.get('fill');
|
||
|
||
var attr = {
|
||
circle: {
|
||
fill: color,
|
||
stroke: color,
|
||
'stroke-width': this.get('stroke-width') * 5
|
||
},
|
||
text: {}
|
||
};
|
||
|
||
this.animateToAttr(attr, speed, easing);
|
||
},
|
||
|
||
animateUpdatedPosition: function(speed, easing) {
|
||
var attr = this.getAttributes();
|
||
this.animateToAttr(attr, speed, easing);
|
||
},
|
||
|
||
animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) {
|
||
// an animation of 0 is essentially setting the attribute directly
|
||
this.animateToAttr(fromAttr, 0);
|
||
this.animateToAttr(toAttr, speed, easing);
|
||
},
|
||
|
||
animateToSnapshot: function(snapShot, speed, easing) {
|
||
if (!snapShot[this.getID()]) {
|
||
return;
|
||
}
|
||
this.animateToAttr(snapShot[this.getID()], speed, easing);
|
||
},
|
||
|
||
animateToAttr: function(attr, speed, easing) {
|
||
if (speed === 0) {
|
||
this.get('circle').attr(attr.circle);
|
||
this.get('text').attr(attr.text);
|
||
return;
|
||
}
|
||
|
||
var s = speed !== undefined ? speed : this.get('animationSpeed');
|
||
var e = easing || this.get('animationEasing');
|
||
|
||
this.get('circle').stop().animate(attr.circle, s, e);
|
||
this.get('text').stop().animate(attr.text, s, e);
|
||
|
||
if (easing == 'bounce' &&
|
||
attr.circle && attr.circle.cx !== undefined &&
|
||
attr.text && attr.text.x !== undefined ) {
|
||
// animate the x attribute without bouncing so it looks like there's
|
||
// gravity in only one direction. Just a small animation polish
|
||
this.get('circle').animate(attr.circle.cx, s, 'easeInOut');
|
||
this.get('text').animate(attr.text.x, s, 'easeInOut');
|
||
}
|
||
},
|
||
|
||
getScreenCoords: function() {
|
||
var pos = this.get('pos');
|
||
return this.gitVisuals.toScreenCoords(pos);
|
||
},
|
||
|
||
getRadius: function() {
|
||
return this.get('radius') || GRAPHICS.nodeRadius;
|
||
},
|
||
|
||
getParentScreenCoords: function() {
|
||
return this.get('commit').get('parents')[0].get('visNode').getScreenCoords();
|
||
},
|
||
|
||
setBirthPosition: function() {
|
||
// utility method for animating it out from underneath a parent
|
||
var parentCoords = this.getParentScreenCoords();
|
||
|
||
this.get('circle').attr({
|
||
cx: parentCoords.x,
|
||
cy: parentCoords.y,
|
||
opacity: 0,
|
||
r: 0
|
||
});
|
||
this.get('text').attr({
|
||
x: parentCoords.x,
|
||
y: parentCoords.y,
|
||
opacity: 0
|
||
});
|
||
},
|
||
|
||
setBirthFromSnapshot: function(beforeSnapshot) {
|
||
// first get parent attribute
|
||
// woof this is pretty bad data access...
|
||
var parentID = this.get('commit').get('parents')[0].get('visNode').getID();
|
||
var parentAttr = beforeSnapshot[parentID];
|
||
|
||
// then set myself faded on top of parent
|
||
this.get('circle').attr({
|
||
opacity: 0,
|
||
r: 0,
|
||
cx: parentAttr.circle.cx,
|
||
cy: parentAttr.circle.cy
|
||
});
|
||
|
||
this.get('text').attr({
|
||
opacity: 0,
|
||
x: parentAttr.text.x,
|
||
y: parentAttr.text.y
|
||
});
|
||
|
||
// then do edges
|
||
var parentCoords = {
|
||
x: parentAttr.circle.cx,
|
||
y: parentAttr.circle.cy
|
||
};
|
||
this.setOutgoingEdgesBirthPosition(parentCoords);
|
||
},
|
||
|
||
setBirth: function() {
|
||
this.setBirthPosition();
|
||
this.setOutgoingEdgesBirthPosition(this.getParentScreenCoords());
|
||
},
|
||
|
||
setOutgoingEdgesOpacity: function(opacity) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
edge.setOpacity(opacity);
|
||
});
|
||
},
|
||
|
||
animateOutgoingEdgesToAttr: function(snapShot, speed, easing) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
var attr = snapShot[edge.getID()];
|
||
edge.animateToAttr(attr);
|
||
}, this);
|
||
},
|
||
|
||
animateOutgoingEdges: function(speed, easing) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
edge.animateUpdatedPath(speed, easing);
|
||
}, this);
|
||
},
|
||
|
||
animateOutgoingEdgesFromSnapshot: function(snapshot, speed, easing) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
var attr = snapshot[edge.getID()];
|
||
edge.animateToAttr(attr, speed, easing);
|
||
}, this);
|
||
},
|
||
|
||
setOutgoingEdgesBirthPosition: function(parentCoords) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
var headPos = edge.get('head').getScreenCoords();
|
||
var path = edge.genSmoothBezierPathStringFromCoords(parentCoords, headPos);
|
||
edge.get('path').stop().attr({
|
||
path: path,
|
||
opacity: 0
|
||
});
|
||
}, this);
|
||
},
|
||
|
||
parentInFront: function() {
|
||
// woof! talk about bad data access
|
||
this.get('commit').get('parents')[0].get('visNode').toFront();
|
||
},
|
||
|
||
getFontSize: function(str) {
|
||
if (str.length < 3) {
|
||
return 12;
|
||
} else if (str.length < 5) {
|
||
return 10;
|
||
} else {
|
||
return 8;
|
||
}
|
||
},
|
||
|
||
getFill: function() {
|
||
// first get our status, might be easy from this
|
||
var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit'));
|
||
if (stat == 'head') {
|
||
return GRAPHICS.headRectFill;
|
||
} else if (stat == 'none') {
|
||
return GRAPHICS.orphanNodeFill;
|
||
}
|
||
|
||
// now we need to get branch hues
|
||
return this.gitVisuals.getBlendedHuesForCommit(this.get('commit'));
|
||
},
|
||
|
||
attachClickHandlers: function() {
|
||
var commandStr = 'git checkout ' + this.get('commit').get('id');
|
||
var Main = require('../app');
|
||
_.each([this.get('circle'), this.get('text')], function(rObj) {
|
||
rObj.click(function() {
|
||
Main.getEventBaton().trigger('commandSubmitted', commandStr);
|
||
});
|
||
$(rObj.node).css('cursor', 'pointer');
|
||
});
|
||
},
|
||
|
||
setOpacity: function(opacity) {
|
||
opacity = (opacity === undefined) ? 1 : opacity;
|
||
|
||
// set the opacity on my stuff
|
||
var keys = ['circle', 'text'];
|
||
_.each(keys, function(key) {
|
||
this.get(key).attr({
|
||
opacity: opacity
|
||
});
|
||
}, this);
|
||
},
|
||
|
||
remove: function() {
|
||
this.removeKeys(['circle'], ['text']);
|
||
// needs a manual removal of text for whatever reason
|
||
this.get('text').remove();
|
||
|
||
this.gitVisuals.removeVisNode(this);
|
||
},
|
||
|
||
removeAll: function() {
|
||
this.remove();
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
edge.remove();
|
||
}, this);
|
||
},
|
||
|
||
getExplodeStepFunc: function() {
|
||
var circle = this.get('circle');
|
||
|
||
// decide on a speed
|
||
var speedMag = 20;
|
||
// aim upwards
|
||
var angle = Math.PI + Math.random() * 1 * Math.PI;
|
||
var gravity = 1 / 5;
|
||
var drag = 1 / 100;
|
||
|
||
var vx = speedMag * Math.cos(angle);
|
||
var vy = speedMag * Math.sin(angle);
|
||
var x = circle.attr('cx');
|
||
var y = circle.attr('cy');
|
||
|
||
var maxWidth = this.gitVisuals.paper.width;
|
||
var maxHeight = this.gitVisuals.paper.height;
|
||
var elasticity = 0.8;
|
||
var dt = 1.0;
|
||
|
||
var stepFunc = function() {
|
||
// lol epic runge kutta here... not
|
||
vy += gravity * dt - drag * vy;
|
||
vx -= drag * vx;
|
||
x += vx * dt;
|
||
y += vy * dt;
|
||
|
||
if (x < 0 || x > maxWidth) {
|
||
vx = elasticity * -vx;
|
||
x = (x < 0) ? 0 : maxWidth;
|
||
}
|
||
if (y < 0 || y > maxHeight) {
|
||
vy = elasticity * -vy;
|
||
y = (y < 0) ? 0 : maxHeight;
|
||
}
|
||
|
||
circle.attr({
|
||
cx: x,
|
||
cy: y
|
||
});
|
||
// continuation calculation
|
||
if ((vx * vx + vy * vy) < 0.01 && Math.abs(y - maxHeight) === 0) {
|
||
// dont need to animate anymore, we are on ground
|
||
return false;
|
||
}
|
||
// keep animating!
|
||
return true;
|
||
};
|
||
return stepFunc;
|
||
},
|
||
|
||
genGraphics: function() {
|
||
var paper = this.gitVisuals.paper;
|
||
|
||
var pos = this.getScreenCoords();
|
||
var textPos = this.getTextScreenCoords();
|
||
|
||
var circle = paper.circle(
|
||
pos.x,
|
||
pos.y,
|
||
this.getRadius()
|
||
).attr(this.getAttributes().circle);
|
||
|
||
var text = paper.text(textPos.x, textPos.y, String(this.get('id')));
|
||
text.attr({
|
||
'font-size': this.getFontSize(this.get('id')),
|
||
'font-weight': 'bold',
|
||
'font-family': 'Monaco, Courier, font-monospace',
|
||
opacity: this.getOpacity()
|
||
});
|
||
|
||
this.set('circle', circle);
|
||
this.set('text', text);
|
||
|
||
this.attachClickHandlers();
|
||
}
|
||
});
|
||
|
||
exports.VisNode = VisNode;
|
||
|
||
});
|
||
|
||
require.define("/src/js/visuals/tree.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var VisBase = Backbone.Model.extend({
|
||
removeKeys: function(keys) {
|
||
_.each(keys, function(key) {
|
||
if (this.get(key)) {
|
||
this.get(key).remove();
|
||
}
|
||
}, this);
|
||
},
|
||
|
||
animateAttrKeys: function(keys, attrObj, speed, easing) {
|
||
// either we animate a specific subset of keys or all
|
||
// possible things we could animate
|
||
keys = _.extend(
|
||
{},
|
||
{
|
||
include: ['circle', 'arrow', 'rect', 'path', 'text'],
|
||
exclude: []
|
||
},
|
||
keys || {}
|
||
);
|
||
|
||
var attr = this.getAttributes();
|
||
|
||
// safely insert this attribute into all the keys we want
|
||
_.each(keys.include, function(key) {
|
||
attr[key] = _.extend(
|
||
{},
|
||
attr[key],
|
||
attrObj
|
||
);
|
||
});
|
||
|
||
_.each(keys.exclude, function(key) {
|
||
delete attr[key];
|
||
});
|
||
|
||
this.animateToAttr(attr, speed, easing);
|
||
}
|
||
});
|
||
|
||
exports.VisBase = VisBase;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/visuals/visBranch.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var GRAPHICS = require('../util/constants').GRAPHICS;
|
||
|
||
var VisBase = require('../visuals/tree').VisBase;
|
||
|
||
var randomHueString = function() {
|
||
var hue = Math.random();
|
||
var str = 'hsb(' + String(hue) + ',0.7,1)';
|
||
return str;
|
||
};
|
||
|
||
var VisBranch = VisBase.extend({
|
||
defaults: {
|
||
pos: null,
|
||
text: null,
|
||
rect: null,
|
||
arrow: null,
|
||
isHead: false,
|
||
flip: 1,
|
||
|
||
fill: GRAPHICS.rectFill,
|
||
stroke: GRAPHICS.rectStroke,
|
||
'stroke-width': GRAPHICS.rectStrokeWidth,
|
||
|
||
offsetX: GRAPHICS.nodeRadius * 4.75,
|
||
offsetY: 0,
|
||
arrowHeight: 14,
|
||
arrowInnerSkew: 0,
|
||
arrowEdgeHeight: 6,
|
||
arrowLength: 14,
|
||
arrowOffsetFromCircleX: 10,
|
||
|
||
vPad: 5,
|
||
hPad: 5,
|
||
|
||
animationSpeed: GRAPHICS.defaultAnimationTime,
|
||
animationEasing: GRAPHICS.defaultEasing
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (!this.get('branch')) {
|
||
throw new Error('need a branch!');
|
||
}
|
||
},
|
||
|
||
getID: function() {
|
||
return this.get('branch').get('id');
|
||
},
|
||
|
||
initialize: function() {
|
||
this.validateAtInit();
|
||
|
||
// shorthand notation for the main objects
|
||
this.gitVisuals = this.get('gitVisuals');
|
||
this.gitEngine = this.get('gitEngine');
|
||
if (!this.gitEngine) {
|
||
console.log('throw damnit');
|
||
throw new Error('asd');
|
||
}
|
||
|
||
this.get('branch').set('visBranch', this);
|
||
var id = this.get('branch').get('id');
|
||
|
||
if (id == 'HEAD') {
|
||
// switch to a head ref
|
||
this.set('isHead', true);
|
||
this.set('flip', -1);
|
||
|
||
this.set('fill', GRAPHICS.headRectFill);
|
||
} else if (id !== 'master') {
|
||
// we need to set our color to something random
|
||
this.set('fill', randomHueString());
|
||
}
|
||
},
|
||
|
||
getCommitPosition: function() {
|
||
var commit = this.gitEngine.getCommitFromRef(this.get('branch'));
|
||
var visNode = commit.get('visNode');
|
||
return visNode.getScreenCoords();
|
||
},
|
||
|
||
getBranchStackIndex: function() {
|
||
if (this.get('isHead')) {
|
||
// head is never stacked with other branches
|
||
return 0;
|
||
}
|
||
|
||
var myArray = this.getBranchStackArray();
|
||
var index = -1;
|
||
_.each(myArray, function(branch, i) {
|
||
if (branch.obj == this.get('branch')) {
|
||
index = i;
|
||
}
|
||
}, this);
|
||
return index;
|
||
},
|
||
|
||
getBranchStackLength: function() {
|
||
if (this.get('isHead')) {
|
||
// head is always by itself
|
||
return 1;
|
||
}
|
||
|
||
return this.getBranchStackArray().length;
|
||
},
|
||
|
||
getBranchStackArray: function() {
|
||
var arr = this.gitVisuals.branchStackMap[this.get('branch').get('target').get('id')];
|
||
if (arr === undefined) {
|
||
// this only occurs when we are generating graphics inside of
|
||
// a new Branch instantiation, so we need to force the update
|
||
this.gitVisuals.calcBranchStacks();
|
||
return this.getBranchStackArray();
|
||
}
|
||
return arr;
|
||
},
|
||
|
||
getTextPosition: function() {
|
||
var pos = this.getCommitPosition();
|
||
|
||
// then order yourself accordingly. we use alphabetical sorting
|
||
// so everything is independent
|
||
var myPos = this.getBranchStackIndex();
|
||
return {
|
||
x: pos.x + this.get('flip') * this.get('offsetX'),
|
||
y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY')
|
||
};
|
||
},
|
||
|
||
getRectPosition: function() {
|
||
var pos = this.getTextPosition();
|
||
var f = this.get('flip');
|
||
|
||
// first get text width and height
|
||
var textSize = this.getTextSize();
|
||
return {
|
||
x: pos.x - 0.5 * textSize.w - this.get('hPad'),
|
||
y: pos.y - 0.5 * textSize.h - this.get('vPad')
|
||
};
|
||
},
|
||
|
||
getArrowPath: function() {
|
||
// should make these util functions...
|
||
var offset2d = function(pos, x, y) {
|
||
return {
|
||
x: pos.x + x,
|
||
y: pos.y + y
|
||
};
|
||
};
|
||
var toStringCoords = function(pos) {
|
||
return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y));
|
||
};
|
||
var f = this.get('flip');
|
||
|
||
var arrowTip = offset2d(this.getCommitPosition(),
|
||
f * this.get('arrowOffsetFromCircleX'),
|
||
0
|
||
);
|
||
var arrowEdgeUp = offset2d(arrowTip, f * this.get('arrowLength'), -this.get('arrowHeight'));
|
||
var arrowEdgeLow = offset2d(arrowTip, f * this.get('arrowLength'), this.get('arrowHeight'));
|
||
|
||
var arrowInnerUp = offset2d(arrowEdgeUp,
|
||
f * this.get('arrowInnerSkew'),
|
||
this.get('arrowEdgeHeight')
|
||
);
|
||
var arrowInnerLow = offset2d(arrowEdgeLow,
|
||
f * this.get('arrowInnerSkew'),
|
||
-this.get('arrowEdgeHeight')
|
||
);
|
||
|
||
var tailLength = 49;
|
||
var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0);
|
||
var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0);
|
||
|
||
var pathStr = '';
|
||
pathStr += 'M' + toStringCoords(arrowStartUp) + ' ';
|
||
var coords = [
|
||
arrowInnerUp,
|
||
arrowEdgeUp,
|
||
arrowTip,
|
||
arrowEdgeLow,
|
||
arrowInnerLow,
|
||
arrowStartLow
|
||
];
|
||
_.each(coords, function(pos) {
|
||
pathStr += 'L' + toStringCoords(pos) + ' ';
|
||
}, this);
|
||
pathStr += 'z';
|
||
return pathStr;
|
||
},
|
||
|
||
getTextSize: function() {
|
||
var getTextWidth = function(visBranch) {
|
||
var textNode = visBranch.get('text').node;
|
||
return (textNode === null) ? 1 : textNode.clientWidth;
|
||
};
|
||
|
||
var textNode = this.get('text').node;
|
||
if (this.get('isHead')) {
|
||
// HEAD is a special case
|
||
return {
|
||
w: textNode.clientWidth,
|
||
h: textNode.clientHeight
|
||
};
|
||
}
|
||
|
||
var maxWidth = 0;
|
||
_.each(this.getBranchStackArray(), function(branch) {
|
||
maxWidth = Math.max(maxWidth, getTextWidth(
|
||
branch.obj.get('visBranch')
|
||
));
|
||
});
|
||
|
||
return {
|
||
w: maxWidth,
|
||
h: textNode.clientHeight
|
||
};
|
||
},
|
||
|
||
getSingleRectSize: function() {
|
||
var textSize = this.getTextSize();
|
||
var vPad = this.get('vPad');
|
||
var hPad = this.get('hPad');
|
||
return {
|
||
w: textSize.w + vPad * 2,
|
||
h: textSize.h + hPad * 2
|
||
};
|
||
},
|
||
|
||
getRectSize: function() {
|
||
var textSize = this.getTextSize();
|
||
// enforce padding
|
||
var vPad = this.get('vPad');
|
||
var hPad = this.get('hPad');
|
||
|
||
// number of other branch names we are housing
|
||
var totalNum = this.getBranchStackLength();
|
||
return {
|
||
w: textSize.w + vPad * 2,
|
||
h: textSize.h * totalNum * 1.1 + hPad * 2
|
||
};
|
||
},
|
||
|
||
getName: function() {
|
||
var name = this.get('branch').get('id');
|
||
var selected = this.gitEngine.HEAD.get('target').get('id');
|
||
|
||
var add = (selected == name) ? '*' : '';
|
||
return name + add;
|
||
},
|
||
|
||
nonTextToFront: function() {
|
||
this.get('arrow').toFront();
|
||
this.get('rect').toFront();
|
||
},
|
||
|
||
textToFront: function() {
|
||
this.get('text').toFront();
|
||
},
|
||
|
||
getFill: function() {
|
||
// in the easy case, just return your own fill if you are:
|
||
// - the HEAD ref
|
||
// - by yourself (length of 1)
|
||
// - part of a multi branch, but your thing is hidden
|
||
if (this.get('isHead') ||
|
||
this.getBranchStackLength() == 1 ||
|
||
this.getBranchStackIndex() !== 0) {
|
||
return this.get('fill');
|
||
}
|
||
|
||
// woof. now it's hard, we need to blend hues...
|
||
return this.gitVisuals.blendHuesFromBranchStack(this.getBranchStackArray());
|
||
},
|
||
|
||
remove: function() {
|
||
this.removeKeys(['text', 'arrow', 'rect']);
|
||
// also need to remove from this.gitVisuals
|
||
this.gitVisuals.removeVisBranch(this);
|
||
},
|
||
|
||
genGraphics: function(paper) {
|
||
var textPos = this.getTextPosition();
|
||
var name = this.getName();
|
||
var text;
|
||
|
||
// when from a reload, we dont need to generate the text
|
||
text = paper.text(textPos.x, textPos.y, String(name));
|
||
text.attr({
|
||
'font-size': 14,
|
||
'font-family': 'Monaco, Courier, font-monospace',
|
||
opacity: this.getTextOpacity()
|
||
});
|
||
this.set('text', text);
|
||
|
||
var rectPos = this.getRectPosition();
|
||
var sizeOfRect = this.getRectSize();
|
||
var rect = paper
|
||
.rect(rectPos.x, rectPos.y, sizeOfRect.w, sizeOfRect.h, 8)
|
||
.attr(this.getAttributes().rect);
|
||
this.set('rect', rect);
|
||
|
||
var arrowPath = this.getArrowPath();
|
||
var arrow = paper
|
||
.path(arrowPath)
|
||
.attr(this.getAttributes().arrow);
|
||
this.set('arrow', arrow);
|
||
|
||
this.attachClickHandlers();
|
||
rect.toFront();
|
||
text.toFront();
|
||
},
|
||
|
||
attachClickHandlers: function() {
|
||
var commandStr = 'git checkout ' + this.get('branch').get('id');
|
||
var Main = require('../app');
|
||
var objs = [this.get('rect'), this.get('text'), this.get('arrow')];
|
||
|
||
_.each(objs, function(rObj) {
|
||
rObj.click(function() {
|
||
Main.getEventBaton().trigger('commandSubmitted', commandStr);
|
||
});
|
||
$(rObj.node).css('cursor', 'pointer');
|
||
});
|
||
},
|
||
|
||
updateName: function() {
|
||
this.get('text').attr({
|
||
text: this.getName()
|
||
});
|
||
},
|
||
|
||
getNonTextOpacity: function() {
|
||
if (this.get('isHead')) {
|
||
return this.gitEngine.getDetachedHead() ? 1 : 0;
|
||
}
|
||
return this.getBranchStackIndex() === 0 ? 1 : 0.0;
|
||
},
|
||
|
||
getTextOpacity: function() {
|
||
if (this.get('isHead')) {
|
||
return this.gitEngine.getDetachedHead() ? 1 : 0;
|
||
}
|
||
return 1;
|
||
},
|
||
|
||
getAttributes: function() {
|
||
var nonTextOpacity = this.getNonTextOpacity();
|
||
var textOpacity = this.getTextOpacity();
|
||
this.updateName();
|
||
|
||
var textPos = this.getTextPosition();
|
||
var rectPos = this.getRectPosition();
|
||
var rectSize = this.getRectSize();
|
||
|
||
var arrowPath = this.getArrowPath();
|
||
|
||
return {
|
||
text: {
|
||
x: textPos.x,
|
||
y: textPos.y,
|
||
opacity: textOpacity
|
||
},
|
||
rect: {
|
||
x: rectPos.x,
|
||
y: rectPos.y,
|
||
width: rectSize.w,
|
||
height: rectSize.h,
|
||
opacity: nonTextOpacity,
|
||
fill: this.getFill(),
|
||
stroke: this.get('stroke'),
|
||
'stroke-width': this.get('stroke-width')
|
||
},
|
||
arrow: {
|
||
path: arrowPath,
|
||
opacity: nonTextOpacity,
|
||
fill: this.getFill(),
|
||
stroke: this.get('stroke'),
|
||
'stroke-width': this.get('stroke-width')
|
||
}
|
||
};
|
||
},
|
||
|
||
animateUpdatedPos: function(speed, easing) {
|
||
var attr = this.getAttributes();
|
||
this.animateToAttr(attr, speed, easing);
|
||
},
|
||
|
||
animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) {
|
||
// an animation of 0 is essentially setting the attribute directly
|
||
this.animateToAttr(fromAttr, 0);
|
||
this.animateToAttr(toAttr, speed, easing);
|
||
},
|
||
|
||
animateToAttr: function(attr, speed, easing) {
|
||
if (speed === 0) {
|
||
this.get('text').attr(attr.text);
|
||
this.get('rect').attr(attr.rect);
|
||
this.get('arrow').attr(attr.arrow);
|
||
return;
|
||
}
|
||
|
||
var s = speed !== undefined ? speed : this.get('animationSpeed');
|
||
var e = easing || this.get('animationEasing');
|
||
|
||
this.get('text').stop().animate(attr.text, s, e);
|
||
this.get('rect').stop().animate(attr.rect, s, e);
|
||
this.get('arrow').stop().animate(attr.arrow, s, e);
|
||
}
|
||
});
|
||
|
||
var VisBranchCollection = Backbone.Collection.extend({
|
||
model: VisBranch
|
||
});
|
||
|
||
exports.VisBranchCollection = VisBranchCollection;
|
||
exports.VisBranch = VisBranch;
|
||
|
||
});
|
||
|
||
require.define("/src/js/visuals/visEdge.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var GRAPHICS = require('../util/constants').GRAPHICS;
|
||
|
||
var VisBase = require('../visuals/tree').VisBase;
|
||
|
||
var VisEdge = VisBase.extend({
|
||
defaults: {
|
||
tail: null,
|
||
head: null,
|
||
animationSpeed: GRAPHICS.defaultAnimationTime,
|
||
animationEasing: GRAPHICS.defaultEasing
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
var required = ['tail', 'head'];
|
||
_.each(required, function(key) {
|
||
if (!this.get(key)) {
|
||
throw new Error(key + ' is required!');
|
||
}
|
||
}, this);
|
||
},
|
||
|
||
getID: function() {
|
||
return this.get('tail').get('id') + '.' + this.get('head').get('id');
|
||
},
|
||
|
||
initialize: function() {
|
||
this.validateAtInit();
|
||
|
||
// shorthand for the main objects
|
||
this.gitVisuals = this.get('gitVisuals');
|
||
this.gitEngine = this.get('gitEngine');
|
||
|
||
this.get('tail').get('outgoingEdges').push(this);
|
||
},
|
||
|
||
remove: function() {
|
||
this.removeKeys(['path']);
|
||
this.gitVisuals.removeVisEdge(this);
|
||
},
|
||
|
||
genSmoothBezierPathString: function(tail, head) {
|
||
var tailPos = tail.getScreenCoords();
|
||
var headPos = head.getScreenCoords();
|
||
return this.genSmoothBezierPathStringFromCoords(tailPos, headPos);
|
||
},
|
||
|
||
genSmoothBezierPathStringFromCoords: function(tailPos, headPos) {
|
||
// we need to generate the path and control points for the bezier. format
|
||
// is M(move abs) C (curve to) (control point 1) (control point 2) (final point)
|
||
// the control points have to be __below__ to get the curve starting off straight.
|
||
|
||
var coords = function(pos) {
|
||
return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y));
|
||
};
|
||
var offset = function(pos, dir, delta) {
|
||
delta = delta || GRAPHICS.curveControlPointOffset;
|
||
return {
|
||
x: pos.x,
|
||
y: pos.y + delta * dir
|
||
};
|
||
};
|
||
var offset2d = function(pos, x, y) {
|
||
return {
|
||
x: pos.x + x,
|
||
y: pos.y + y
|
||
};
|
||
};
|
||
|
||
// first offset tail and head by radii
|
||
tailPos = offset(tailPos, -1, this.get('tail').getRadius());
|
||
headPos = offset(headPos, 1, this.get('head').getRadius());
|
||
|
||
var str = '';
|
||
// first move to bottom of tail
|
||
str += 'M' + coords(tailPos) + ' ';
|
||
// start bezier
|
||
str += 'C';
|
||
// then control points above tail and below head
|
||
str += coords(offset(tailPos, -1)) + ' ';
|
||
str += coords(offset(headPos, 1)) + ' ';
|
||
// now finish
|
||
str += coords(headPos);
|
||
|
||
// arrow head
|
||
var delta = GRAPHICS.arrowHeadSize || 10;
|
||
str += ' L' + coords(offset2d(headPos, -delta, delta));
|
||
str += ' L' + coords(offset2d(headPos, delta, delta));
|
||
str += ' L' + coords(headPos);
|
||
|
||
// then go back, so we can fill correctly
|
||
str += 'C';
|
||
str += coords(offset(headPos, 1)) + ' ';
|
||
str += coords(offset(tailPos, -1)) + ' ';
|
||
str += coords(tailPos);
|
||
|
||
return str;
|
||
},
|
||
|
||
getBezierCurve: function() {
|
||
return this.genSmoothBezierPathString(this.get('tail'), this.get('head'));
|
||
},
|
||
|
||
getStrokeColor: function() {
|
||
return GRAPHICS.visBranchStrokeColorNone;
|
||
},
|
||
|
||
setOpacity: function(opacity) {
|
||
opacity = (opacity === undefined) ? 1 : opacity;
|
||
|
||
this.get('path').attr({opacity: opacity});
|
||
},
|
||
|
||
genGraphics: function(paper) {
|
||
var pathString = this.getBezierCurve();
|
||
|
||
var path = paper.path(pathString).attr({
|
||
'stroke-width': GRAPHICS.visBranchStrokeWidth,
|
||
'stroke': this.getStrokeColor(),
|
||
'stroke-linecap': 'round',
|
||
'stroke-linejoin': 'round',
|
||
'fill': this.getStrokeColor()
|
||
});
|
||
path.toBack();
|
||
this.set('path', path);
|
||
},
|
||
|
||
getOpacity: function() {
|
||
var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('tail'));
|
||
var map = {
|
||
'branch': 1,
|
||
'head': GRAPHICS.edgeUpstreamHeadOpacity,
|
||
'none': GRAPHICS.edgeUpstreamNoneOpacity
|
||
};
|
||
|
||
if (map[stat] === undefined) { throw new Error('bad stat'); }
|
||
return map[stat];
|
||
},
|
||
|
||
getAttributes: function() {
|
||
var newPath = this.getBezierCurve();
|
||
var opacity = this.getOpacity();
|
||
return {
|
||
path: {
|
||
path: newPath,
|
||
opacity: opacity
|
||
}
|
||
};
|
||
},
|
||
|
||
animateUpdatedPath: function(speed, easing) {
|
||
var attr = this.getAttributes();
|
||
this.animateToAttr(attr, speed, easing);
|
||
},
|
||
|
||
animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) {
|
||
// an animation of 0 is essentially setting the attribute directly
|
||
this.animateToAttr(fromAttr, 0);
|
||
this.animateToAttr(toAttr, speed, easing);
|
||
},
|
||
|
||
animateToAttr: function(attr, speed, easing) {
|
||
if (speed === 0) {
|
||
this.get('path').attr(attr.path);
|
||
return;
|
||
}
|
||
|
||
this.get('path').toBack();
|
||
this.get('path').stop().animate(
|
||
attr.path,
|
||
speed !== undefined ? speed : this.get('animationSpeed'),
|
||
easing || this.get('animationEasing')
|
||
);
|
||
}
|
||
});
|
||
|
||
var VisEdgeCollection = Backbone.Collection.extend({
|
||
model: VisEdge
|
||
});
|
||
|
||
exports.VisEdgeCollection = VisEdgeCollection;
|
||
exports.VisEdge = VisEdge;
|
||
|
||
});
|
||
|
||
require.define("/src/js/level/disabledMap.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var GitCommands = require('../git/commands');
|
||
|
||
var Errors = require('../util/errors');
|
||
var GitError = Errors.GitError;
|
||
|
||
function DisabledMap(options) {
|
||
options = options || {};
|
||
this.disabledMap = options.disabledMap || {
|
||
'git cherry-pick': true,
|
||
'git rebase': true
|
||
};
|
||
}
|
||
|
||
DisabledMap.prototype.getInstantCommands = function() {
|
||
// this produces an array of regex / function pairs that can be
|
||
// piped into a parse waterfall to disable certain git commmands
|
||
// :D
|
||
var instants = [];
|
||
var onMatch = function() {
|
||
throw new GitError({
|
||
msg: 'That git command is disabled for this level!'
|
||
});
|
||
};
|
||
|
||
_.each(this.disabledMap, function(val, disabledCommand) {
|
||
var gitRegex = GitCommands.regexMap[disabledCommand];
|
||
if (!gitRegex) {
|
||
throw new Error('wuttttt this disbaled command' + disabledCommand +
|
||
' has no regex matching');
|
||
}
|
||
instants.push([gitRegex, onMatch]);
|
||
});
|
||
return instants;
|
||
};
|
||
|
||
exports.DisabledMap = DisabledMap;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/git/gitShim.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Q = require('q');
|
||
|
||
var Main = require('../app');
|
||
var MultiView = require('../views/multiView').MultiView;
|
||
|
||
function GitShim(options) {
|
||
options = options || {};
|
||
|
||
// these variables are just functions called before / after for
|
||
// simple things (like incrementing a counter)
|
||
this.beforeCB = options.beforeCB || function() {};
|
||
this.afterCB = options.afterCB || function() {};
|
||
|
||
// these guys handle an optional async process before the git
|
||
// command executes or afterwards. If there is none,
|
||
// it just resolves the deferred immediately
|
||
var resolveImmediately = function(deferred) {
|
||
deferred.resolve();
|
||
};
|
||
this.beforeDeferHandler = options.beforeDeferHandler || resolveImmediately;
|
||
this.afterDeferHandler = options.afterDeferHandler || resolveImmediately;
|
||
this.eventBaton = options.eventBaton || Main.getEventBaton();
|
||
}
|
||
|
||
GitShim.prototype.insertShim = function() {
|
||
this.eventBaton.stealBaton('processGitCommand', this.processGitCommand, this);
|
||
};
|
||
|
||
GitShim.prototype.processGitCommand = function(command, deferred) {
|
||
this.beforeCB(command);
|
||
|
||
// ok we make a NEW deferred that will, upon resolution,
|
||
// call our afterGitCommandProcessed. This inserts the 'after' shim
|
||
// functionality. we give this new deferred to the eventBaton handler
|
||
var newDeferred = Q.defer();
|
||
newDeferred.promise
|
||
.then(_.bind(function() {
|
||
// give this method the original defer so it can resolve it
|
||
this.afterGitCommandProcessed(command, deferred);
|
||
}, this))
|
||
.done();
|
||
|
||
// now our shim owner might want to launch some kind of deferred beforehand, like
|
||
// a modal or something. in order to do this, we need to defer the passing
|
||
// of the event baton backwards, and either resolve that promise immediately or
|
||
// give it to our shim owner.
|
||
var passBaton = _.bind(function() {
|
||
// punt to the previous listener
|
||
this.eventBaton.passBatonBack('processGitCommand', this.processGitCommand, this, [command, newDeferred]);
|
||
}, this);
|
||
|
||
var beforeDefer = Q.defer();
|
||
beforeDefer.promise
|
||
.then(passBaton)
|
||
.done();
|
||
|
||
// if we didnt receive a defer handler in the options, this just
|
||
// resolves immediately
|
||
this.beforeDeferHandler(beforeDefer);
|
||
};
|
||
|
||
GitShim.prototype.afterGitCommandProcessed = function(command, deferred) {
|
||
this.afterCB(command);
|
||
|
||
// again we can't just resolve this deferred right away... our shim owner might
|
||
// want to insert some promise functionality before that happens. so again
|
||
// we make a defer
|
||
var afterDefer = Q.defer();
|
||
afterDefer.promise
|
||
.then(function() {
|
||
deferred.resolve();
|
||
})
|
||
.done();
|
||
|
||
this.afterDeferHandler(afterDefer);
|
||
};
|
||
|
||
exports.GitShim = GitShim;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/views/multiView.js",function(require,module,exports,__dirname,__filename,process,global){var GitError = require('../util/errors').GitError;
|
||
var _ = require('underscore');
|
||
var Q = require('q');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;
|
||
|
||
var ModalTerminal = require('../views').ModalTerminal;
|
||
var ContainedBase = require('../views').ContainedBase;
|
||
var ConfirmCancelView = require('../views').ConfirmCancelView;
|
||
var LeftRightView = require('../views').LeftRightView;
|
||
var ModalAlert = require('../views').ModalAlert;
|
||
var KeyboardListener = require('../util/keyboard').KeyboardListener;
|
||
|
||
var MultiView = Backbone.View.extend({
|
||
tagName: 'div',
|
||
className: 'multiView',
|
||
// ms to debounce the nav functions
|
||
navEventDebounce: 750,
|
||
deathTime: 700,
|
||
|
||
// a simple mapping of what childViews we support
|
||
typeToConstructor: {
|
||
ModalAlert: ModalAlert
|
||
},
|
||
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
this.childViewJSONs = options.childViews || [{
|
||
type: 'ModalAlert',
|
||
options: {
|
||
markdown: 'Woah wtf!!'
|
||
}
|
||
}, {
|
||
type: 'ModalAlert',
|
||
options: {
|
||
markdown: 'Im second'
|
||
}
|
||
}];
|
||
this.deferred = options.deferred || Q.defer();
|
||
|
||
this.childViews = [];
|
||
this.currentIndex = 0;
|
||
|
||
this.navEvents = _.clone(Backbone.Events);
|
||
this.navEvents.on('negative', this.getNegFunc(), this);
|
||
this.navEvents.on('positive', this.getPosFunc(), this);
|
||
|
||
this.keyboardListener = new KeyboardListener({
|
||
events: this.navEvents,
|
||
aliasMap: {
|
||
left: 'negative',
|
||
right: 'positive',
|
||
enter: 'positive'
|
||
}
|
||
});
|
||
|
||
this.render();
|
||
if (!options.wait) {
|
||
this.start();
|
||
}
|
||
},
|
||
|
||
onWindowFocus: function() {
|
||
// nothing here for now...
|
||
// TODO -- add a cool glow effect?
|
||
},
|
||
|
||
getAnimationTime: function() {
|
||
return 700;
|
||
},
|
||
|
||
getPromise: function() {
|
||
return this.deferred.promise;
|
||
},
|
||
|
||
getPosFunc: function() {
|
||
return _.debounce(_.bind(function() {
|
||
this.navForward();
|
||
}, this), this.navEventDebounce, true);
|
||
},
|
||
|
||
getNegFunc: function() {
|
||
return _.debounce(_.bind(function() {
|
||
this.navBackward();
|
||
}, this), this.navEventDebounce, true);
|
||
},
|
||
|
||
navForward: function() {
|
||
if (this.currentIndex === this.childViews.length - 1) {
|
||
this.hideViewIndex(this.currentIndex);
|
||
this.finish();
|
||
return;
|
||
}
|
||
|
||
this.navIndexChange(1);
|
||
},
|
||
|
||
navBackward: function() {
|
||
if (this.currentIndex === 0) {
|
||
return;
|
||
}
|
||
|
||
this.navIndexChange(-1);
|
||
},
|
||
|
||
navIndexChange: function(delta) {
|
||
this.hideViewIndex(this.currentIndex);
|
||
this.currentIndex += delta;
|
||
this.showViewIndex(this.currentIndex);
|
||
},
|
||
|
||
hideViewIndex: function(index) {
|
||
this.childViews[index].hide();
|
||
},
|
||
|
||
showViewIndex: function(index) {
|
||
this.childViews[index].show();
|
||
},
|
||
|
||
finish: function() {
|
||
// first we stop listening to keyboard and give that back to UI, which
|
||
// other views will take if they need to
|
||
this.keyboardListener.mute();
|
||
|
||
_.each(this.childViews, function(childView) {
|
||
childView.die();
|
||
});
|
||
|
||
this.deferred.resolve();
|
||
},
|
||
|
||
start: function() {
|
||
// steal the window focus baton
|
||
this.showViewIndex(this.currentIndex);
|
||
},
|
||
|
||
createChildView: function(viewJSON) {
|
||
var type = viewJSON.type;
|
||
if (!this.typeToConstructor[type]) {
|
||
throw new Error('no constructor for type "' + type + '"');
|
||
}
|
||
var view = new this.typeToConstructor[type](viewJSON.options);
|
||
return view;
|
||
},
|
||
|
||
addNavToView: function(view, index) {
|
||
var leftRight = new LeftRightView({
|
||
events: this.navEvents,
|
||
// we want the arrows to be on the same level as the content (not
|
||
// beneath), so we go one level up with getDestination()
|
||
destination: view.getDestination(),
|
||
showLeft: (index !== 0),
|
||
lastNav: (index === this.childViewJSONs.length - 1)
|
||
});
|
||
},
|
||
|
||
render: function() {
|
||
// go through each and render... show the first
|
||
_.each(this.childViewJSONs, function(childViewJSON, index) {
|
||
var childView = this.createChildView(childViewJSON);
|
||
this.childViews.push(childView);
|
||
this.addNavToView(childView, index);
|
||
}, this);
|
||
}
|
||
});
|
||
|
||
exports.MultiView = MultiView;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/util/keyboard.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var mapKeycodeToKey = function(keycode) {
|
||
// HELP WANTED -- internationalize? Dvorak? I have no idea
|
||
var keyMap = {
|
||
37: 'left',
|
||
38: 'up',
|
||
39: 'right',
|
||
40: 'down',
|
||
27: 'esc',
|
||
13: 'enter'
|
||
};
|
||
return keyMap[keycode];
|
||
};
|
||
|
||
function KeyboardListener(options) {
|
||
this.events = options.events || _.clone(Backbone.Events);
|
||
this.aliasMap = options.aliasMap || {};
|
||
|
||
this.keydownListener = _.bind(this.keydown, this);
|
||
this.listen();
|
||
}
|
||
|
||
KeyboardListener.prototype.listen = function() {
|
||
$(document).bind('keydown', this.keydownListener);
|
||
};
|
||
|
||
KeyboardListener.prototype.mute = function() {
|
||
$(document).unbind('keydown', this.keydownListener);
|
||
};
|
||
|
||
KeyboardListener.prototype.keydown = function(e) {
|
||
var which = e.which;
|
||
|
||
var key = mapKeycodeToKey(which);
|
||
if (key === undefined) {
|
||
return;
|
||
}
|
||
|
||
this.fireEvent(key);
|
||
};
|
||
|
||
KeyboardListener.prototype.fireEvent = function(eventName) {
|
||
eventName = this.aliasMap[eventName] || eventName;
|
||
this.events.trigger(eventName);
|
||
};
|
||
|
||
exports.KeyboardListener = KeyboardListener;
|
||
exports.mapKeycodeToKey = mapKeycodeToKey;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/util/eventBaton.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
function EventBaton() {
|
||
this.eventMap = {};
|
||
}
|
||
|
||
// this method steals the "baton" -- aka, only this method will now
|
||
// get called. analogous to events.on
|
||
// EventBaton.prototype.on = function(name, func, context) {
|
||
EventBaton.prototype.stealBaton = function(name, func, context) {
|
||
if (!name) { throw new Error('need name'); }
|
||
if (!func) { throw new Error('need func!'); }
|
||
|
||
var listeners = this.eventMap[name] || [];
|
||
listeners.push({
|
||
func: func,
|
||
context: context
|
||
});
|
||
this.eventMap[name] = listeners;
|
||
};
|
||
|
||
EventBaton.prototype.sliceOffArgs = function(num, args) {
|
||
var newArgs = [];
|
||
for (var i = num; i < args.length; i++) {
|
||
newArgs.push(args[i]);
|
||
}
|
||
return newArgs;
|
||
};
|
||
|
||
EventBaton.prototype.trigger = function(name) {
|
||
// arguments is weird and doesnt do slice right
|
||
var argsToApply = this.sliceOffArgs(1, arguments);
|
||
|
||
var listeners = this.eventMap[name];
|
||
if (!listeners || !listeners.length) {
|
||
console.warn('no listeners for', name);
|
||
return;
|
||
}
|
||
|
||
// call the top most listener with context and such
|
||
var toCall = listeners.slice(-1)[0];
|
||
toCall.func.apply(toCall.context, argsToApply);
|
||
};
|
||
|
||
EventBaton.prototype.getListenersThrow = function(name) {
|
||
var listeners = this.eventMap[name];
|
||
if (!listeners || !listeners.length) {
|
||
throw new Error('no one has that baton!' + name);
|
||
}
|
||
return listeners;
|
||
};
|
||
|
||
EventBaton.prototype.passBatonBack = function(name, func, context, args) {
|
||
// this method will call the listener BEFORE the name/func pair. this
|
||
// basically allows you to put in shims, where you steal batons but pass
|
||
// them back if they don't meet certain conditions
|
||
var listeners = this.getListenersThrow(name);
|
||
|
||
var indexBefore;
|
||
_.each(listeners, function(listenerObj, index) {
|
||
// skip the first
|
||
if (index === 0) { return; }
|
||
if (listenerObj.func === func && listenerObj.context === context) {
|
||
indexBefore = index - 1;
|
||
}
|
||
}, this);
|
||
if (indexBefore === undefined) {
|
||
throw new Error('you are the last baton holder! or i didnt find you');
|
||
}
|
||
var toCallObj = listeners[indexBefore];
|
||
|
||
toCallObj.func.apply(toCallObj.context, args);
|
||
};
|
||
|
||
EventBaton.prototype.releaseBaton = function(name, func, context) {
|
||
// might be in the middle of the stack, so we have to loop instead of
|
||
// just popping blindly
|
||
var listeners = this.getListenersThrow(name);
|
||
|
||
var newListeners = [];
|
||
var found = false;
|
||
_.each(listeners, function(listenerObj) {
|
||
if (listenerObj.func === func && listenerObj.context === context) {
|
||
if (found) { console.warn('woah duplicates!!!'); }
|
||
found = true;
|
||
} else {
|
||
newListeners.push(listenerObj);
|
||
}
|
||
}, this);
|
||
|
||
if (!found) {
|
||
console.log('did not find that function', func, context, name, arguments);
|
||
throw new Error('cant releasebaton if yu dont have it');
|
||
}
|
||
this.eventMap[name] = newListeners;
|
||
};
|
||
|
||
exports.EventBaton = EventBaton;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/util/zoomLevel.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var warnOnce = true;
|
||
|
||
function detectZoom() {
|
||
/**
|
||
* Note: this method has only been tested on Chrome
|
||
* but seems to work. A much more elaborate library is available here:
|
||
* https://github.com/yonran/detect-zoom
|
||
* but seems to return a "2" zoom level for my computer (who knows)
|
||
* so I can't use it. The ecosystem for zoom level detection is a mess
|
||
*/
|
||
if (!window.outerWidth || !window.innerWidth) {
|
||
if (warnOnce) {
|
||
console.warn("Can't detect zoom level correctly :-/");
|
||
warnOnce = false;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
return window.outerWidth / window.innerWidth;
|
||
}
|
||
|
||
var setupZoomPoll = function(callback, context) {
|
||
var currentZoom = 0;
|
||
|
||
setInterval(function() {
|
||
var newZoom = detectZoom();
|
||
if (newZoom !== currentZoom) {
|
||
currentZoom = newZoom;
|
||
callback.apply(context, [newZoom]);
|
||
}
|
||
}, 500);
|
||
};
|
||
|
||
exports.setupZoomPoll = setupZoomPoll;
|
||
exports.detectZoom = detectZoom;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/views/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
|
||
var CommandEntryCollection = require('../models/collections').CommandEntryCollection;
|
||
var Main = require('../app');
|
||
var Command = require('../models/commandModel').Command;
|
||
var CommandEntry = require('../models/commandModel').CommandEntry;
|
||
|
||
var Errors = require('../util/errors');
|
||
var Warning = Errors.Warning;
|
||
|
||
var util = require('../util');
|
||
var keyboard = require('../util/keyboard');
|
||
|
||
var CommandPromptView = Backbone.View.extend({
|
||
initialize: function(options) {
|
||
Main.getEvents().on('commandSubmittedPassive', this.addToCommandHistory, this);
|
||
|
||
// uses local storage
|
||
this.commands = new CommandEntryCollection();
|
||
this.commands.fetch({
|
||
success: _.bind(function() {
|
||
// reverse the commands. this is ugly but needs to be done...
|
||
var commands = [];
|
||
this.commands.each(function(c) {
|
||
commands.push(c);
|
||
});
|
||
|
||
commands.reverse();
|
||
this.commands.reset();
|
||
|
||
_.each(commands, function(c) {
|
||
this.commands.add(c);
|
||
}, this);
|
||
}, this)
|
||
});
|
||
|
||
this.index = -1;
|
||
this.commandSpan = this.$('#prompt span.command')[0];
|
||
this.commandCursor = this.$('#prompt span.cursor')[0];
|
||
this.focus();
|
||
|
||
Main.getEvents().on('rollupCommands', this.rollupCommands, this);
|
||
|
||
Main.getEventBaton().stealBaton('keydown', this.onKeyDown, this);
|
||
Main.getEventBaton().stealBaton('keyup', this.onKeyUp, this);
|
||
},
|
||
|
||
events: {
|
||
'blur #commandTextField': 'hideCursor',
|
||
'focus #commandTextField': 'showCursor'
|
||
},
|
||
|
||
blur: function() {
|
||
this.hideCursor();
|
||
},
|
||
|
||
focus: function() {
|
||
this.$('#commandTextField').focus();
|
||
this.showCursor();
|
||
},
|
||
|
||
hideCursor: function() {
|
||
this.toggleCursor(false);
|
||
},
|
||
|
||
showCursor: function() {
|
||
this.toggleCursor(true);
|
||
},
|
||
|
||
toggleCursor: function(state) {
|
||
$(this.commandCursor).toggleClass('shown', state);
|
||
},
|
||
|
||
onKeyDown: function(e) {
|
||
var el = e.srcElement;
|
||
this.updatePrompt(el);
|
||
},
|
||
|
||
onKeyUp: function(e) {
|
||
this.onKeyDown(e);
|
||
|
||
// we need to capture some of these events.
|
||
var keyToFuncMap = {
|
||
enter: _.bind(function() {
|
||
this.submit();
|
||
}, this),
|
||
up: _.bind(function() {
|
||
this.commandSelectChange(1);
|
||
}, this),
|
||
down: _.bind(function() {
|
||
this.commandSelectChange(-1);
|
||
}, this)
|
||
};
|
||
|
||
var key = keyboard.mapKeycodeToKey(e.which);
|
||
if (keyToFuncMap[key] !== undefined) {
|
||
e.preventDefault();
|
||
keyToFuncMap[key]();
|
||
this.onKeyDown(e);
|
||
}
|
||
},
|
||
|
||
badHtmlEncode: function(text) {
|
||
return text.replace(/&/g,'&')
|
||
.replace(/</g,'<')
|
||
.replace(/</g,'<')
|
||
.replace(/ /g,' ')
|
||
.replace(/\n/g,'');
|
||
},
|
||
|
||
updatePrompt: function(el) {
|
||
// i WEEEPPPPPPpppppppppppp that this reflow takes so long. it adds this
|
||
// super annoying delay to every keystroke... I have tried everything
|
||
// to make this more performant. getting the srcElement from the event,
|
||
// getting the value directly from the dom, etc etc. yet still,
|
||
// there's a very annoying and sightly noticeable command delay.
|
||
// try.github.com also has this, so I'm assuming those engineers gave up as
|
||
// well...
|
||
var val = this.badHtmlEncode(el.value);
|
||
this.commandSpan.innerHTML = val;
|
||
|
||
// now mutate the cursor...
|
||
this.cursorUpdate(el.value.length, el.selectionStart, el.selectionEnd);
|
||
// and scroll down due to some weird bug
|
||
Main.getEvents().trigger('commandScrollDown');
|
||
},
|
||
|
||
cursorUpdate: function(commandLength, selectionStart, selectionEnd) {
|
||
// 10px for monospaced font at "1" zoom
|
||
var zoom = require('../util/zoomLevel').detectZoom();
|
||
var widthPerChar = 10 * zoom;
|
||
|
||
var numCharsSelected = Math.max(1, selectionEnd - selectionStart);
|
||
var width = String(numCharsSelected * widthPerChar) + 'px';
|
||
|
||
// now for positioning
|
||
var numLeft = Math.max(commandLength - selectionStart, 0);
|
||
var left = String(-numLeft * widthPerChar) + 'px';
|
||
// one reflow? :D
|
||
$(this.commandCursor).css({
|
||
width: width,
|
||
left: left
|
||
});
|
||
},
|
||
|
||
commandSelectChange: function(delta) {
|
||
this.index += delta;
|
||
|
||
// if we are over / under, display blank line. yes this eliminates your
|
||
// partially edited 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
|
||
var commandEntry = this.commands.toArray()[this.index].get('text');
|
||
this.setTextField(commandEntry);
|
||
},
|
||
|
||
clearLocalStorage: function() {
|
||
this.commands.each(function(c) {
|
||
Backbone.sync('delete', c, function() { });
|
||
}, this);
|
||
localStorage.setItem('CommandEntries', '');
|
||
},
|
||
|
||
setTextField: function(value) {
|
||
this.$('#commandTextField').val(value);
|
||
},
|
||
|
||
clear: function() {
|
||
this.setTextField('');
|
||
},
|
||
|
||
submit: function() {
|
||
var value = this.$('#commandTextField').val().replace('\n', '');
|
||
this.clear();
|
||
|
||
this.submitCommand(value);
|
||
this.index = -1;
|
||
},
|
||
|
||
rollupCommands: function(numBack) {
|
||
var which = this.commands.toArray().slice(1, Number(numBack) + 1);
|
||
which.reverse();
|
||
|
||
var str = '';
|
||
_.each(which, function(commandEntry) {
|
||
str += commandEntry.get('text') + ';';
|
||
}, this);
|
||
|
||
var rolled = new CommandEntry({text: str});
|
||
this.commands.unshift(rolled);
|
||
Backbone.sync('create', rolled, function() { });
|
||
},
|
||
|
||
addToCommandHistory: function(value) {
|
||
// we should add the command to our local storage history
|
||
// if it's not a blank line and this is a new command...
|
||
// or if we edited the command in place in history
|
||
var shouldAdd = (value.length && this.index === -1) ||
|
||
((value.length && this.index !== -1 &&
|
||
this.commands.toArray()[this.index].get('text') !== value));
|
||
|
||
if (this.index !== -1) {
|
||
console.log(this.commands.toArray()[this.index]);
|
||
}
|
||
|
||
if (!shouldAdd) {
|
||
return;
|
||
}
|
||
|
||
var commandEntry = new CommandEntry({text: value});
|
||
this.commands.unshift(commandEntry);
|
||
|
||
// store to local storage
|
||
Backbone.sync('create', commandEntry, function() { });
|
||
|
||
// if our length is too egregious, reset
|
||
if (this.commands.length > 100) {
|
||
this.clearLocalStorage();
|
||
}
|
||
},
|
||
|
||
submitCommand: function(value) {
|
||
Main.getEventBaton().trigger('commandSubmitted', value);
|
||
}
|
||
});
|
||
|
||
// 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) {
|
||
},
|
||
|
||
initialize: function() {
|
||
this.model.bind('change', this.wasChanged, this);
|
||
this.model.bind('destroy', this.remove, this);
|
||
},
|
||
|
||
wasChanged: function(model, changeEvent) {
|
||
// for changes that are just comestic, we actually only want to toggle classes
|
||
// with jquery rather than brutally delete a html. doing so allows us
|
||
// to nicely fade things
|
||
var changes = changeEvent.changes;
|
||
var changeKeys = _.keys(changes);
|
||
if (_.difference(changeKeys, ['status']).length === 0) {
|
||
this.updateStatus();
|
||
} 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: '',
|
||
formattedWarnings: this.model.getFormattedWarnings()
|
||
},
|
||
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);
|
||
Main.getEvents().on('commandScrollDown', this.scrollDown, this);
|
||
},
|
||
|
||
addWarning: function(msg) {
|
||
var err = new Warning({
|
||
msg: msg
|
||
});
|
||
|
||
var command = new Command({
|
||
error: err,
|
||
rawStr: 'Warning:'
|
||
});
|
||
|
||
this.collection.add(command);
|
||
},
|
||
|
||
scrollDown: function() {
|
||
// if commandDisplay is ever bigger than #terminal, we need to
|
||
// add overflow-y to terminal and scroll down
|
||
var cD = $('#commandDisplay')[0];
|
||
var t = $('#terminal')[0];
|
||
|
||
if ($(t).hasClass('scrolling')) {
|
||
t.scrollTop = t.scrollHeight;
|
||
return;
|
||
}
|
||
if (cD.clientHeight > t.clientHeight) {
|
||
$(t).css('overflow-y', 'scroll');
|
||
$(t).css('overflow-x', 'hidden');
|
||
$(t).addClass('scrolling');
|
||
t.scrollTop = t.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);
|
||
}
|
||
});
|
||
|
||
exports.CommandPromptView = CommandPromptView;
|
||
exports.CommandLineHistoryView = CommandLineHistoryView;
|
||
|
||
});
|
||
|
||
require.define("/src/js/dialogs/sandbox.js",function(require,module,exports,__dirname,__filename,process,global){exports.helpDialog = [{
|
||
type: 'ModalAlert',
|
||
options: {
|
||
markdowns: [
|
||
'## Welcome to LearnGitBranching!',
|
||
'',
|
||
'This application is designed to help beginners grasp ',
|
||
'the powerful concepts behind branching when working ',
|
||
'with git. We hope you enjoy this application and maybe ',
|
||
'even learn something!',
|
||
''
|
||
]
|
||
}
|
||
}, {
|
||
type: 'ModalAlert',
|
||
options: {
|
||
markdowns: [
|
||
'## The LearnGitBranching Interface',
|
||
'',
|
||
'There are features to use within the user interface behind ',
|
||
'this modal dialog. A list:',
|
||
'',
|
||
' * git commands (to interact with git)',
|
||
' * level commands (to get level hints or solutions)',
|
||
' * sandbox commands (like this one)'
|
||
]
|
||
}
|
||
}];
|
||
|
||
});
|
||
|
||
require.define("/src/js/util/mock.js",function(require,module,exports,__dirname,__filename,process,global){exports.mock = function(Constructor) {
|
||
var dummy = {};
|
||
var stub = function() {};
|
||
|
||
for (var key in Constructor.prototype) {
|
||
dummy[key] = stub;
|
||
}
|
||
return dummy;
|
||
};
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/git/headless.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var Q = require('q');
|
||
|
||
var GitEngine = require('../git').GitEngine;
|
||
var AnimationFactory = require('../visuals/animation/animationFactory').AnimationFactory;
|
||
var GitVisuals = require('../visuals').GitVisuals;
|
||
var TreeCompare = require('../git/treeCompare').TreeCompare;
|
||
var EventBaton = require('../util/eventBaton').EventBaton;
|
||
|
||
var Collections = require('../models/collections');
|
||
var CommitCollection = Collections.CommitCollection;
|
||
var BranchCollection = Collections.BranchCollection;
|
||
var Command = require('../models/commandModel').Command;
|
||
|
||
var mock = require('../util/mock').mock;
|
||
var util = require('../util');
|
||
|
||
var HeadlessGit = function() {
|
||
this.init();
|
||
};
|
||
|
||
HeadlessGit.prototype.init = function() {
|
||
this.commitCollection = new CommitCollection();
|
||
this.branchCollection = new BranchCollection();
|
||
this.treeCompare = new TreeCompare();
|
||
|
||
// here we mock visuals and animation factory so the git engine
|
||
// is headless
|
||
var animationFactory = mock(AnimationFactory);
|
||
var gitVisuals = mock(GitVisuals);
|
||
|
||
this.gitEngine = new GitEngine({
|
||
collection: this.commitCollection,
|
||
branches: this.branchCollection,
|
||
gitVisuals: gitVisuals,
|
||
animationFactory: animationFactory,
|
||
eventBaton: new EventBaton()
|
||
});
|
||
this.gitEngine.init();
|
||
};
|
||
|
||
HeadlessGit.prototype.sendCommand = function(value) {
|
||
util.splitTextCommand(value, function(commandStr) {
|
||
var commandObj = new Command({
|
||
rawStr: commandStr
|
||
});
|
||
console.log('dispatching command "', commandStr, '"');
|
||
this.gitEngine.dispatch(commandObj, Q.defer());
|
||
}, this);
|
||
};
|
||
|
||
exports.HeadlessGit = HeadlessGit;
|
||
|
||
|
||
});
|
||
|
||
require.define("/src/js/app/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var Constants = require('../util/constants');
|
||
var util = require('../util');
|
||
|
||
/**
|
||
* Globals
|
||
*/
|
||
var events = _.clone(Backbone.Events);
|
||
var commandUI;
|
||
var sandbox;
|
||
var eventBaton;
|
||
|
||
///////////////////////////////////////////////////////////////////////
|
||
|
||
var init = function() {
|
||
/**
|
||
* There is a decent amount of bootstrapping we need just to hook
|
||
* everything up. The init() method takes on these responsibilities,
|
||
* including but not limited to:
|
||
* - setting up Events and EventBaton
|
||
* - calling the constructor for the main visualization
|
||
* - initializing the command input bar
|
||
* - handling window.focus and zoom events
|
||
**/
|
||
var Sandbox = require('../level/sandbox').Sandbox;
|
||
var Level = require('../level').Level;
|
||
var EventBaton = require('../util/eventBaton').EventBaton;
|
||
|
||
eventBaton = new EventBaton();
|
||
commandUI = new CommandUI();
|
||
sandbox = new Level();
|
||
|
||
// we always want to focus the text area to collect input
|
||
var focusTextArea = function() {
|
||
$('#commandTextField').focus();
|
||
};
|
||
focusTextArea();
|
||
|
||
$(window).focus(function(e) {
|
||
eventBaton.trigger('windowFocus', e);
|
||
});
|
||
$(document).click(function(e) {
|
||
eventBaton.trigger('documentClick', e);
|
||
});
|
||
|
||
// zoom level measure, I wish there was a jquery event for this :/
|
||
require('../util/zoomLevel').setupZoomPoll(function(level) {
|
||
eventBaton.trigger('zoomChange', level);
|
||
}, this);
|
||
|
||
eventBaton.stealBaton('zoomChange', function(level) {
|
||
if (level > Constants.VIEWPORT.maxZoom ||
|
||
level < Constants.VIEWPORT.minZoom) {
|
||
var Views = require('../views');
|
||
var view = new Views.ZoomAlertWindow();
|
||
}
|
||
});
|
||
|
||
// the default action on window focus and document click is to just focus the text area
|
||
eventBaton.stealBaton('windowFocus', focusTextArea);
|
||
eventBaton.stealBaton('documentClick', focusTextArea);
|
||
|
||
// but when the input is fired in the text area, we pipe that to whoever is
|
||
// listenining
|
||
var makeKeyListener = function(name) {
|
||
return function() {
|
||
var args = [name];
|
||
_.each(arguments, function(arg) {
|
||
args.push(arg);
|
||
});
|
||
eventBaton.trigger.apply(eventBaton, args);
|
||
};
|
||
};
|
||
|
||
$('#commandTextField').on('keydown', makeKeyListener('keydown'));
|
||
$('#commandTextField').on('keyup', makeKeyListener('keyup'));
|
||
|
||
/* hacky demo functionality */
|
||
if (/\?demo/.test(window.location.href)) {
|
||
setTimeout(function() {
|
||
eventBaton.trigger('commandSubmitted', "gc; git checkout HEAD~1; git commit; git checkout -b bugFix; gc; gc; git rebase -i HEAD~2; git rebase master; git checkout master; gc; gc; git merge bugFix");
|
||
}, 500);
|
||
}
|
||
};
|
||
|
||
$(document).ready(init);
|
||
|
||
/**
|
||
* the UI method simply bootstraps the command buffer and
|
||
* command prompt views. It only interacts with user input
|
||
* and simply pipes commands to the main events system
|
||
**/
|
||
function CommandUI() {
|
||
var Collections = require('../models/collections');
|
||
var CommandViews = require('../views/commandViews');
|
||
|
||
this.commandCollection = new Collections.CommandCollection();
|
||
this.commandBuffer = new Collections.CommandBuffer({
|
||
collection: this.commandCollection
|
||
});
|
||
|
||
this.commandPromptView = new CommandViews.CommandPromptView({
|
||
el: $('#commandLineBar')
|
||
});
|
||
|
||
this.commandLineHistoryView = new CommandViews.CommandLineHistoryView({
|
||
el: $('#commandLineHistory'),
|
||
collection: this.commandCollection
|
||
});
|
||
}
|
||
|
||
exports.getEvents = function() {
|
||
return events;
|
||
};
|
||
|
||
exports.getSandbox = function() {
|
||
return sandbox;
|
||
};
|
||
|
||
exports.getEventBaton = function() {
|
||
return eventBaton;
|
||
};
|
||
|
||
exports.getCommandUI = function() {
|
||
return commandUI;
|
||
};
|
||
|
||
exports.init = init;
|
||
|
||
|
||
});
|
||
require("/src/js/app/index.js");
|
||
|
||
require.define("/src/js/dialogs/sandbox.js",function(require,module,exports,__dirname,__filename,process,global){exports.helpDialog = [{
|
||
type: 'ModalAlert',
|
||
options: {
|
||
markdowns: [
|
||
'## Welcome to LearnGitBranching!',
|
||
'',
|
||
'This application is designed to help beginners grasp ',
|
||
'the powerful concepts behind branching when working ',
|
||
'with git. We hope you enjoy this application and maybe ',
|
||
'even learn something!',
|
||
''
|
||
]
|
||
}
|
||
}, {
|
||
type: 'ModalAlert',
|
||
options: {
|
||
markdowns: [
|
||
'## The LearnGitBranching Interface',
|
||
'',
|
||
'There are features to use within the user interface behind ',
|
||
'this modal dialog. A list:',
|
||
'',
|
||
' * git commands (to interact with git)',
|
||
' * level commands (to get level hints or solutions)',
|
||
' * sandbox commands (like this one)'
|
||
]
|
||
}
|
||
}];
|
||
|
||
});
|
||
require("/src/js/dialogs/sandbox.js");
|
||
|
||
require.define("/src/js/git/commands.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var Errors = require('../util/errors');
|
||
var CommandProcessError = Errors.CommandProcessError;
|
||
var GitError = Errors.GitError;
|
||
var Warning = Errors.Warning;
|
||
var CommandResult = Errors.CommandResult;
|
||
|
||
var shortcutMap = {
|
||
'git commit': /^gc($|\s)/,
|
||
'git add': /^ga($|\s)/,
|
||
'git checkout': /^go($|\s)/,
|
||
'git rebase': /^gr($|\s)/,
|
||
'git branch': /^gb($|\s)/,
|
||
'git status': /^gs($|\s)/,
|
||
'git help': /^git$/
|
||
};
|
||
|
||
var instantCommands = [
|
||
[/^git help($|\s)/, function() {
|
||
var lines = [
|
||
'Git Version PCOTTLE.1.0',
|
||
'<br/>',
|
||
'Usage:',
|
||
_.escape('\t git <command> [<args>]'),
|
||
'<br/>',
|
||
'Supported commands:',
|
||
'<br/>'
|
||
];
|
||
var commands = GitOptionParser.prototype.getMasterOptionMap();
|
||
|
||
// build up a nice display of what we support
|
||
_.each(commands, function(commandOptions, command) {
|
||
lines.push('git ' + command);
|
||
_.each(commandOptions, function(vals, optionName) {
|
||
lines.push('\t ' + optionName);
|
||
}, this);
|
||
}, this);
|
||
|
||
// format and throw
|
||
var msg = lines.join('\n');
|
||
msg = msg.replace(/\t/g, ' ');
|
||
throw new CommandResult({
|
||
msg: msg
|
||
});
|
||
}]
|
||
];
|
||
|
||
var regexMap = {
|
||
// ($|\s) means that we either have to end the string
|
||
// after the command or there needs to be a space for options
|
||
'git commit': /^git commit($|\s)/,
|
||
'git add': /^git add($|\s)/,
|
||
'git checkout': /^git checkout($|\s)/,
|
||
'git rebase': /^git rebase($|\s)/,
|
||
'git reset': /^git reset($|\s)/,
|
||
'git branch': /^git branch($|\s)/,
|
||
'git revert': /^git revert($|\s)/,
|
||
'git log': /^git log($|\s)/,
|
||
'git merge': /^git merge($|\s)/,
|
||
'git show': /^git show($|\s)/,
|
||
'git status': /^git status($|\s)/,
|
||
'git cherry-pick': /^git cherry-pick($|\s)/
|
||
};
|
||
|
||
var parse = function(str) {
|
||
var method;
|
||
var options;
|
||
|
||
// see if we support this particular command
|
||
_.each(regexMap, function(regex, thisMethod) {
|
||
if (regex.exec(str)) {
|
||
options = str.slice(thisMethod.length + 1);
|
||
method = thisMethod.slice('git '.length);
|
||
}
|
||
});
|
||
|
||
if (!method) {
|
||
return false;
|
||
}
|
||
|
||
// we support this command!
|
||
// parse off the options and assemble the map / general args
|
||
var parsedOptions = new GitOptionParser(method, options);
|
||
return {
|
||
toSet: {
|
||
generalArgs: parsedOptions.generalArgs,
|
||
supportedMap: parsedOptions.supportedMap,
|
||
method: method,
|
||
options: options,
|
||
eventName: 'processGitCommand'
|
||
}
|
||
};
|
||
};
|
||
|
||
/**
|
||
* GitOptionParser
|
||
*/
|
||
function GitOptionParser(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();
|
||
}
|
||
|
||
GitOptionParser.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, // warning
|
||
'-m': false
|
||
},
|
||
status: {},
|
||
log: {},
|
||
add: {},
|
||
'cherry-pick': {},
|
||
branch: {
|
||
'-d': false,
|
||
'-D': false,
|
||
'-f': false,
|
||
'--contains': false
|
||
},
|
||
checkout: {
|
||
'-b': false,
|
||
'-B': false,
|
||
'-': false
|
||
},
|
||
reset: {
|
||
'--hard': false,
|
||
'--soft': false // this will raise an error but we catch it in gitEngine
|
||
},
|
||
merge: {},
|
||
rebase: {
|
||
'-i': false // the mother of all options
|
||
},
|
||
revert: {},
|
||
show: {}
|
||
};
|
||
};
|
||
|
||
GitOptionParser.prototype.explodeAndSet = function() {
|
||
// split on spaces, except when inside quotes
|
||
|
||
var exploded = this.rawOptions.match(/('.*?'|".*?"|\S+)/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);
|
||
}
|
||
}
|
||
};
|
||
|
||
exports.shortcutMap = shortcutMap;
|
||
exports.instantCommands = instantCommands;
|
||
exports.parse = parse;
|
||
exports.regexMap = regexMap;
|
||
|
||
});
|
||
require("/src/js/git/commands.js");
|
||
|
||
require.define("/src/js/git/gitShim.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Q = require('q');
|
||
|
||
var Main = require('../app');
|
||
var MultiView = require('../views/multiView').MultiView;
|
||
|
||
function GitShim(options) {
|
||
options = options || {};
|
||
|
||
// these variables are just functions called before / after for
|
||
// simple things (like incrementing a counter)
|
||
this.beforeCB = options.beforeCB || function() {};
|
||
this.afterCB = options.afterCB || function() {};
|
||
|
||
// these guys handle an optional async process before the git
|
||
// command executes or afterwards. If there is none,
|
||
// it just resolves the deferred immediately
|
||
var resolveImmediately = function(deferred) {
|
||
deferred.resolve();
|
||
};
|
||
this.beforeDeferHandler = options.beforeDeferHandler || resolveImmediately;
|
||
this.afterDeferHandler = options.afterDeferHandler || resolveImmediately;
|
||
this.eventBaton = options.eventBaton || Main.getEventBaton();
|
||
}
|
||
|
||
GitShim.prototype.insertShim = function() {
|
||
this.eventBaton.stealBaton('processGitCommand', this.processGitCommand, this);
|
||
};
|
||
|
||
GitShim.prototype.processGitCommand = function(command, deferred) {
|
||
this.beforeCB(command);
|
||
|
||
// ok we make a NEW deferred that will, upon resolution,
|
||
// call our afterGitCommandProcessed. This inserts the 'after' shim
|
||
// functionality. we give this new deferred to the eventBaton handler
|
||
var newDeferred = Q.defer();
|
||
newDeferred.promise
|
||
.then(_.bind(function() {
|
||
// give this method the original defer so it can resolve it
|
||
this.afterGitCommandProcessed(command, deferred);
|
||
}, this))
|
||
.done();
|
||
|
||
// now our shim owner might want to launch some kind of deferred beforehand, like
|
||
// a modal or something. in order to do this, we need to defer the passing
|
||
// of the event baton backwards, and either resolve that promise immediately or
|
||
// give it to our shim owner.
|
||
var passBaton = _.bind(function() {
|
||
// punt to the previous listener
|
||
this.eventBaton.passBatonBack('processGitCommand', this.processGitCommand, this, [command, newDeferred]);
|
||
}, this);
|
||
|
||
var beforeDefer = Q.defer();
|
||
beforeDefer.promise
|
||
.then(passBaton)
|
||
.done();
|
||
|
||
// if we didnt receive a defer handler in the options, this just
|
||
// resolves immediately
|
||
this.beforeDeferHandler(beforeDefer);
|
||
};
|
||
|
||
GitShim.prototype.afterGitCommandProcessed = function(command, deferred) {
|
||
this.afterCB(command);
|
||
|
||
// again we can't just resolve this deferred right away... our shim owner might
|
||
// want to insert some promise functionality before that happens. so again
|
||
// we make a defer
|
||
var afterDefer = Q.defer();
|
||
afterDefer.promise
|
||
.then(function() {
|
||
deferred.resolve();
|
||
})
|
||
.done();
|
||
|
||
this.afterDeferHandler(afterDefer);
|
||
};
|
||
|
||
exports.GitShim = GitShim;
|
||
|
||
|
||
});
|
||
require("/src/js/git/gitShim.js");
|
||
|
||
require.define("/src/js/git/headless.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var Q = require('q');
|
||
|
||
var GitEngine = require('../git').GitEngine;
|
||
var AnimationFactory = require('../visuals/animation/animationFactory').AnimationFactory;
|
||
var GitVisuals = require('../visuals').GitVisuals;
|
||
var TreeCompare = require('../git/treeCompare').TreeCompare;
|
||
var EventBaton = require('../util/eventBaton').EventBaton;
|
||
|
||
var Collections = require('../models/collections');
|
||
var CommitCollection = Collections.CommitCollection;
|
||
var BranchCollection = Collections.BranchCollection;
|
||
var Command = require('../models/commandModel').Command;
|
||
|
||
var mock = require('../util/mock').mock;
|
||
var util = require('../util');
|
||
|
||
var HeadlessGit = function() {
|
||
this.init();
|
||
};
|
||
|
||
HeadlessGit.prototype.init = function() {
|
||
this.commitCollection = new CommitCollection();
|
||
this.branchCollection = new BranchCollection();
|
||
this.treeCompare = new TreeCompare();
|
||
|
||
// here we mock visuals and animation factory so the git engine
|
||
// is headless
|
||
var animationFactory = mock(AnimationFactory);
|
||
var gitVisuals = mock(GitVisuals);
|
||
|
||
this.gitEngine = new GitEngine({
|
||
collection: this.commitCollection,
|
||
branches: this.branchCollection,
|
||
gitVisuals: gitVisuals,
|
||
animationFactory: animationFactory,
|
||
eventBaton: new EventBaton()
|
||
});
|
||
this.gitEngine.init();
|
||
};
|
||
|
||
HeadlessGit.prototype.sendCommand = function(value) {
|
||
util.splitTextCommand(value, function(commandStr) {
|
||
var commandObj = new Command({
|
||
rawStr: commandStr
|
||
});
|
||
console.log('dispatching command "', commandStr, '"');
|
||
this.gitEngine.dispatch(commandObj, Q.defer());
|
||
}, this);
|
||
};
|
||
|
||
exports.HeadlessGit = HeadlessGit;
|
||
|
||
|
||
});
|
||
require("/src/js/git/headless.js");
|
||
|
||
require.define("/src/js/git/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
var Q = require('q');
|
||
|
||
var AnimationFactoryModule = require('../visuals/animation/animationFactory');
|
||
var AnimationQueue = require('../visuals/animation').AnimationQueue;
|
||
var TreeCompare = require('./treeCompare').TreeCompare;
|
||
|
||
var Errors = require('../util/errors');
|
||
var GitError = Errors.GitError;
|
||
var CommandResult = Errors.CommandResult;
|
||
|
||
function GitEngine(options) {
|
||
this.rootCommit = null;
|
||
this.refs = {};
|
||
this.HEAD = null;
|
||
|
||
this.branchCollection = options.branches;
|
||
this.commitCollection = options.collection;
|
||
this.gitVisuals = options.gitVisuals;
|
||
|
||
this.eventBaton = options.eventBaton;
|
||
this.eventBaton.stealBaton('processGitCommand', this.dispatch, this);
|
||
|
||
this.animationFactory = options.animationFactory ||
|
||
new AnimationFactoryModule.AnimationFactory();
|
||
|
||
// global variable to keep track of the options given
|
||
// along with the command call.
|
||
this.commandOptions = {};
|
||
this.generalArgs = [];
|
||
|
||
|
||
// backbone or something uses _.uniqueId, so we make our own here
|
||
this.uniqueId = (function() {
|
||
var n = 0;
|
||
return function(prepend) {
|
||
return prepend? prepend + n++ : n++;
|
||
};
|
||
})();
|
||
}
|
||
|
||
GitEngine.prototype.defaultInit = function() {
|
||
var defaultTree = JSON.parse(unescape("%7B%22branches%22%3A%7B%22master%22%3A%7B%22target%22%3A%22C1%22%2C%22id%22%3A%22master%22%2C%22type%22%3A%22branch%22%7D%7D%2C%22commits%22%3A%7B%22C0%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C0%22%2C%22rootCommit%22%3Atrue%7D%2C%22C1%22%3A%7B%22type%22%3A%22commit%22%2C%22parents%22%3A%5B%22C0%22%5D%2C%22author%22%3A%22Peter%20Cottle%22%2C%22createTime%22%3A%22Mon%20Nov%2005%202012%2000%3A56%3A47%20GMT-0800%20%28PST%29%22%2C%22commitMessage%22%3A%22Quick%20Commit.%20Go%20Bears%21%22%2C%22id%22%3A%22C1%22%7D%7D%2C%22HEAD%22%3A%7B%22id%22%3A%22HEAD%22%2C%22target%22%3A%22master%22%2C%22type%22%3A%22general%20ref%22%7D%7D"));
|
||
this.loadTree(defaultTree);
|
||
};
|
||
|
||
GitEngine.prototype.init = function() {
|
||
// make an initial commit and a master branch
|
||
this.rootCommit = this.makeCommit(null, null, {rootCommit: true});
|
||
this.commitCollection.add(this.rootCommit);
|
||
|
||
var master = this.makeBranch('master', this.rootCommit);
|
||
this.HEAD = new Ref({
|
||
id: 'HEAD',
|
||
target: master
|
||
});
|
||
this.refs[this.HEAD.get('id')] = this.HEAD;
|
||
|
||
// commit once to get things going
|
||
this.commit();
|
||
};
|
||
|
||
GitEngine.prototype.exportTree = function() {
|
||
// need to export all commits, their connectivity / messages, branches, and state of head.
|
||
// this would be simple if didn't have circular structures.... :P
|
||
// thus, we need to loop through and "flatten" our graph of objects referencing one another
|
||
var totalExport = {
|
||
branches: {},
|
||
commits: {},
|
||
HEAD: null
|
||
};
|
||
|
||
_.each(this.branchCollection.toJSON(), function(branch) {
|
||
branch.target = branch.target.get('id');
|
||
branch.visBranch = undefined;
|
||
|
||
totalExport.branches[branch.id] = branch;
|
||
});
|
||
|
||
_.each(this.commitCollection.toJSON(), function(commit) {
|
||
// clear out the fields that reference objects and create circular structure
|
||
_.each(Commit.prototype.constants.circularFields, function(field) {
|
||
commit[field] = undefined;
|
||
}, this);
|
||
|
||
// convert parents
|
||
var parents = [];
|
||
_.each(commit.parents, function(par) {
|
||
parents.push(par.get('id'));
|
||
});
|
||
commit.parents = parents;
|
||
|
||
totalExport.commits[commit.id] = commit;
|
||
}, this);
|
||
|
||
var HEAD = this.HEAD.toJSON();
|
||
HEAD.visBranch = undefined;
|
||
HEAD.lastTarget = HEAD.lastLastTarget = HEAD.visBranch = undefined;
|
||
HEAD.target = HEAD.target.get('id');
|
||
totalExport.HEAD = HEAD;
|
||
|
||
return totalExport;
|
||
};
|
||
|
||
GitEngine.prototype.printTree = function(tree) {
|
||
tree = tree || this.exportTree();
|
||
TreeCompare.prototype.reduceTreeFields([tree]);
|
||
|
||
var str = JSON.stringify(tree);
|
||
if (/'/.test(str)) {
|
||
// escape it to make it more copy paste friendly
|
||
str = escape(str);
|
||
}
|
||
return str;
|
||
};
|
||
|
||
GitEngine.prototype.printAndCopyTree = function() {
|
||
window.prompt('Copy the tree string below', this.printTree());
|
||
};
|
||
|
||
GitEngine.prototype.loadTree = function(tree) {
|
||
// deep copy in case we use it a bunch
|
||
tree = $.extend(true, {}, tree);
|
||
|
||
// first clear everything
|
||
this.removeAll();
|
||
|
||
this.instantiateFromTree(tree);
|
||
|
||
this.reloadGraphics();
|
||
};
|
||
|
||
GitEngine.prototype.loadTreeFromString = function(treeString) {
|
||
this.loadTree(JSON.parse(unescape(treeString)));
|
||
};
|
||
|
||
GitEngine.prototype.instantiateFromTree = function(tree) {
|
||
// now we do the loading part
|
||
var createdSoFar = {};
|
||
|
||
_.each(tree.commits, function(commitJSON) {
|
||
var commit = this.getOrMakeRecursive(tree, createdSoFar, commitJSON.id);
|
||
this.commitCollection.add(commit);
|
||
}, this);
|
||
|
||
_.each(tree.branches, function(branchJSON) {
|
||
var branch = this.getOrMakeRecursive(tree, createdSoFar, branchJSON.id);
|
||
|
||
this.branchCollection.add(branch, {silent: true});
|
||
}, this);
|
||
|
||
var HEAD = this.getOrMakeRecursive(tree, createdSoFar, tree.HEAD.id);
|
||
this.HEAD = HEAD;
|
||
|
||
this.rootCommit = createdSoFar['C0'];
|
||
if (!this.rootCommit) {
|
||
throw new Error('Need root commit of C0 for calculations');
|
||
}
|
||
this.refs = createdSoFar;
|
||
|
||
this.branchCollection.each(function(branch) {
|
||
this.gitVisuals.addBranch(branch);
|
||
}, this);
|
||
};
|
||
|
||
GitEngine.prototype.reloadGraphics = function() {
|
||
// get the root commit, no better way to do it
|
||
var rootCommit = null;
|
||
this.commitCollection.each(function(commit) {
|
||
if (commit.get('id') == 'C0') {
|
||
rootCommit = commit;
|
||
}
|
||
});
|
||
this.gitVisuals.rootCommit = rootCommit;
|
||
|
||
// this just basically makes the HEAD branch. the head branch really should have been
|
||
// a member of a collection and not this annoying edge case stuff... one day
|
||
this.gitVisuals.initHeadBranch();
|
||
|
||
// when the paper is ready
|
||
this.gitVisuals.drawTreeFromReload();
|
||
|
||
this.gitVisuals.refreshTreeHarsh();
|
||
};
|
||
|
||
GitEngine.prototype.getOrMakeRecursive = function(tree, createdSoFar, objID) {
|
||
if (createdSoFar[objID]) {
|
||
// base case
|
||
return createdSoFar[objID];
|
||
}
|
||
|
||
var getType = function(tree, id) {
|
||
if (tree.commits[id]) {
|
||
return 'commit';
|
||
} else if (tree.branches[id]) {
|
||
return 'branch';
|
||
} else if (id == 'HEAD') {
|
||
return 'HEAD';
|
||
}
|
||
throw new Error("bad type for " + id);
|
||
};
|
||
|
||
// figure out what type
|
||
var type = getType(tree, objID);
|
||
|
||
if (type == 'HEAD') {
|
||
var headJSON = tree.HEAD;
|
||
var HEAD = new Ref(_.extend(
|
||
tree.HEAD,
|
||
{
|
||
target: this.getOrMakeRecursive(tree, createdSoFar, headJSON.target)
|
||
}
|
||
));
|
||
createdSoFar[objID] = HEAD;
|
||
return HEAD;
|
||
}
|
||
|
||
if (type == 'branch') {
|
||
var branchJSON = tree.branches[objID];
|
||
|
||
var branch = new Branch(_.extend(
|
||
tree.branches[objID],
|
||
{
|
||
target: this.getOrMakeRecursive(tree, createdSoFar, branchJSON.target)
|
||
}
|
||
));
|
||
createdSoFar[objID] = branch;
|
||
return branch;
|
||
}
|
||
|
||
if (type == 'commit') {
|
||
// for commits, we need to grab all the parents
|
||
var commitJSON = tree.commits[objID];
|
||
|
||
var parentObjs = [];
|
||
_.each(commitJSON.parents, function(parentID) {
|
||
parentObjs.push(this.getOrMakeRecursive(tree, createdSoFar, parentID));
|
||
}, this);
|
||
|
||
var commit = new Commit(_.extend(
|
||
commitJSON,
|
||
{
|
||
parents: parentObjs,
|
||
gitVisuals: this.gitVisuals
|
||
}
|
||
));
|
||
createdSoFar[objID] = commit;
|
||
return commit;
|
||
}
|
||
|
||
throw new Error('ruh rho!! unsupported tyep for ' + objID);
|
||
};
|
||
|
||
GitEngine.prototype.removeAll = function() {
|
||
this.branchCollection.reset();
|
||
this.commitCollection.reset();
|
||
this.refs = {};
|
||
this.HEAD = null;
|
||
this.rootCommit = null;
|
||
|
||
this.gitVisuals.resetAll();
|
||
};
|
||
|
||
GitEngine.prototype.getDetachedHead = function() {
|
||
// detached head is if HEAD points to a commit instead of a branch...
|
||
var target = this.HEAD.get('target');
|
||
var targetType = target.get('type');
|
||
return targetType !== 'branch';
|
||
};
|
||
|
||
GitEngine.prototype.validateBranchName = function(name) {
|
||
name = name.replace(/\s/g, '');
|
||
if (!/^[a-zA-Z0-9]+$/.test(name)) {
|
||
throw new GitError({
|
||
msg: 'woah bad branch name!! This is not ok: ' + name
|
||
});
|
||
}
|
||
if (/[hH][eE][aA][dD]/.test(name)) {
|
||
throw new GitError({
|
||
msg: 'branch name of "head" is ambiguous, dont name it that'
|
||
});
|
||
}
|
||
if (name.length > 9) {
|
||
name = name.slice(0, 9);
|
||
this.command.addWarning(
|
||
'Sorry, we need to keep branch names short for the visuals. Your branch ' +
|
||
'name was truncated to 9 characters, resulting in ' + name
|
||
);
|
||
}
|
||
return name;
|
||
};
|
||
|
||
GitEngine.prototype.makeBranch = function(id, target) {
|
||
id = this.validateBranchName(id);
|
||
if (this.refs[id]) {
|
||
throw new GitError({
|
||
msg: 'that branch id either matches a commit hash or already exists!'
|
||
});
|
||
}
|
||
|
||
var branch = new Branch({
|
||
target: target,
|
||
id: id
|
||
});
|
||
this.branchCollection.add(branch);
|
||
this.refs[branch.get('id')] = branch;
|
||
return branch;
|
||
};
|
||
|
||
GitEngine.prototype.getHead = function() {
|
||
return _.clone(this.HEAD);
|
||
};
|
||
|
||
GitEngine.prototype.getBranches = function() {
|
||
var toReturn = [];
|
||
this.branchCollection.each(function(branch) {
|
||
toReturn.push({
|
||
id: branch.get('id'),
|
||
selected: this.HEAD.get('target') === branch,
|
||
target: branch.get('target'),
|
||
obj: branch
|
||
});
|
||
}, this);
|
||
return toReturn;
|
||
};
|
||
|
||
GitEngine.prototype.printBranchesWithout = function(without) {
|
||
var commitToBranches = this.getUpstreamBranchSet();
|
||
var commitID = this.getCommitFromRef(without).get('id');
|
||
|
||
var toPrint = [];
|
||
_.each(commitToBranches[commitID], function(branchJSON) {
|
||
branchJSON.selected = this.HEAD.get('target').get('id') == branchJSON.id;
|
||
toPrint.push(branchJSON);
|
||
}, this);
|
||
this.printBranches(toPrint);
|
||
};
|
||
|
||
GitEngine.prototype.printBranches = function(branches) {
|
||
var result = '';
|
||
_.each(branches, function(branch) {
|
||
result += (branch.selected ? '* ' : '') + branch.id + '\n';
|
||
});
|
||
throw new CommandResult({
|
||
msg: result
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.makeCommit = function(parents, id, options) {
|
||
// ok we need to actually manually create commit IDs now because
|
||
// people like nikita (thanks for finding this!) could
|
||
// make branches named C2 before creating the commit C2
|
||
if (!id) {
|
||
id = this.uniqueId('C');
|
||
while (this.refs[id]) {
|
||
id = this.uniqueId('C');
|
||
}
|
||
}
|
||
|
||
var commit = new Commit(_.extend({
|
||
parents: parents,
|
||
id: id,
|
||
gitVisuals: this.gitVisuals
|
||
},
|
||
options || {}
|
||
));
|
||
|
||
this.refs[commit.get('id')] = commit;
|
||
this.commitCollection.add(commit);
|
||
return commit;
|
||
};
|
||
|
||
GitEngine.prototype.acceptNoGeneralArgs = function() {
|
||
if (this.generalArgs.length) {
|
||
throw new GitError({
|
||
msg: "That command accepts no general arguments"
|
||
});
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.validateArgBounds = function(args, lower, upper, option) {
|
||
// this is a little utility class to help arg validation that happens over and over again
|
||
var what = (option === undefined) ?
|
||
'git ' + this.command.get('method') :
|
||
this.command.get('method') + ' ' + option + ' ';
|
||
what = 'with ' + what;
|
||
|
||
if (args.length < lower) {
|
||
throw new GitError({
|
||
msg: 'I expect at least ' + String(lower) + ' argument(s) ' + what
|
||
});
|
||
}
|
||
if (args.length > upper) {
|
||
throw new GitError({
|
||
msg: 'I expect at most ' + String(upper) + ' argument(s) ' + what
|
||
});
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.oneArgImpliedHead = function(args, option) {
|
||
// for log, show, etc
|
||
this.validateArgBounds(args, 0, 1, option);
|
||
if (args.length === 0) {
|
||
args.push('HEAD');
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.twoArgsImpliedHead = function(args, option) {
|
||
// our args we expect to be between 1 and 2
|
||
this.validateArgBounds(args, 1, 2, option);
|
||
// and if it's one, add a HEAD to the back
|
||
if (args.length == 1) {
|
||
args.push('HEAD');
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.revertStarter = function() {
|
||
this.validateArgBounds(this.generalArgs, 1, NaN);
|
||
|
||
var response = this.revert(this.generalArgs);
|
||
|
||
if (response) {
|
||
this.animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.revert = function(whichCommits) {
|
||
// for each commit, we want to revert it
|
||
var toRebase = [];
|
||
_.each(whichCommits, function(stringRef) {
|
||
toRebase.push(this.getCommitFromRef(stringRef));
|
||
}, this);
|
||
|
||
// we animate reverts now!! we use the rebase animation though so that's
|
||
// why the terminology is like it is
|
||
var animationResponse = {};
|
||
animationResponse.destinationBranch = this.resolveID(toRebase[0]);
|
||
animationResponse.toRebaseArray = toRebase.slice(0);
|
||
animationResponse.rebaseSteps = [];
|
||
|
||
var beforeSnapshot = this.gitVisuals.genSnapshot();
|
||
var afterSnapshot;
|
||
|
||
// now make a bunch of commits on top of where we are
|
||
var base = this.getCommitFromRef('HEAD');
|
||
_.each(toRebase, function(oldCommit) {
|
||
var newId = this.rebaseAltID(oldCommit.get('id'));
|
||
|
||
var newCommit = this.makeCommit([base], newId, {
|
||
commitMessage: 'Reverting ' + this.resolveName(oldCommit) +
|
||
': "' + oldCommit.get('commitMessage') + '"'
|
||
});
|
||
|
||
base = newCommit;
|
||
|
||
// animation stuff
|
||
afterSnapshot = this.gitVisuals.genSnapshot();
|
||
animationResponse.rebaseSteps.push({
|
||
oldCommit: oldCommit,
|
||
newCommit: newCommit,
|
||
beforeSnapshot: beforeSnapshot,
|
||
afterSnapshot: afterSnapshot
|
||
});
|
||
beforeSnapshot = afterSnapshot;
|
||
}, this);
|
||
// done! update our location
|
||
this.setTargetLocation('HEAD', base);
|
||
|
||
// animation
|
||
return animationResponse;
|
||
};
|
||
|
||
GitEngine.prototype.resetStarter = function() {
|
||
if (this.commandOptions['--soft']) {
|
||
throw new GitError({
|
||
msg: "You can't use --soft because there is no concept of stashing" +
|
||
" changes or staging files, so you will lose your progress." +
|
||
" Try using interactive rebasing (or just rebasing) to move commits."
|
||
});
|
||
}
|
||
if (this.commandOptions['--hard']) {
|
||
this.command.addWarning(
|
||
'Nice! You are using --hard. The default behavior is a hard reset in ' +
|
||
"this demo, so don't worry about specifying the option explicity"
|
||
);
|
||
// dont absorb the arg off of --hard
|
||
this.generalArgs = this.generalArgs.concat(this.commandOptions['--hard']);
|
||
}
|
||
|
||
this.validateArgBounds(this.generalArgs, 1, 1);
|
||
|
||
if (this.getDetachedHead()) {
|
||
throw new GitError({
|
||
msg: "Cant reset in detached head! Use checkout if you want to move"
|
||
});
|
||
}
|
||
|
||
this.reset(this.generalArgs[0]);
|
||
};
|
||
|
||
GitEngine.prototype.reset = function(target) {
|
||
this.setTargetLocation('HEAD', this.getCommitFromRef(target));
|
||
};
|
||
|
||
GitEngine.prototype.cherrypickStarter = function() {
|
||
this.validateArgBounds(this.generalArgs, 1, 1);
|
||
var newCommit = this.cherrypick(this.generalArgs[0]);
|
||
|
||
this.animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
|
||
};
|
||
|
||
GitEngine.prototype.cherrypick = function(ref) {
|
||
var commit = this.getCommitFromRef(ref);
|
||
// check if we already have that
|
||
var set = this.getUpstreamSet('HEAD');
|
||
if (set[commit.get('id')]) {
|
||
throw new GitError({
|
||
msg: "We already have that commit in our changes history! You can't cherry-pick it " +
|
||
"if it shows up in git log."
|
||
});
|
||
}
|
||
|
||
// alter the ID slightly
|
||
var id = this.rebaseAltID(commit.get('id'));
|
||
|
||
// now commit with that id onto HEAD
|
||
var newCommit = this.makeCommit([this.getCommitFromRef('HEAD')], id);
|
||
this.setTargetLocation(this.HEAD, newCommit);
|
||
return newCommit;
|
||
};
|
||
|
||
GitEngine.prototype.commitStarter = function() {
|
||
this.acceptNoGeneralArgs();
|
||
if (this.commandOptions['-am'] && (
|
||
this.commandOptions['-a'] || this.commandOptions['-m'])) {
|
||
throw new GitError({
|
||
msg: "You can't have -am with another -m or -a!"
|
||
});
|
||
}
|
||
|
||
var msg = null;
|
||
var args = null;
|
||
if (this.commandOptions['-a']) {
|
||
this.command.addWarning('No need to add files in this demo');
|
||
}
|
||
|
||
if (this.commandOptions['-am']) {
|
||
args = this.commandOptions['-am'];
|
||
this.validateArgBounds(args, 1, 1, '-am');
|
||
|
||
this.command.addWarning("Don't worry about adding files in this demo. I'll take " +
|
||
"down your commit message anyways, but you can commit without a message " +
|
||
"in this demo as well");
|
||
msg = args[0];
|
||
}
|
||
|
||
if (this.commandOptions['-m']) {
|
||
args = this.commandOptions['-m'];
|
||
this.validateArgBounds(args, 1, 1, '-m');
|
||
msg = args[0];
|
||
}
|
||
|
||
var newCommit = this.commit();
|
||
if (msg) {
|
||
msg = msg
|
||
.replace(/"/g, '"')
|
||
.replace(/^"/g, '')
|
||
.replace(/"$/g, '');
|
||
|
||
newCommit.set('commitMessage', msg);
|
||
}
|
||
this.animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
|
||
};
|
||
|
||
GitEngine.prototype.commit = function() {
|
||
var targetCommit = this.getCommitFromRef(this.HEAD);
|
||
var id = null;
|
||
|
||
// if we want to ammend, go one above
|
||
if (this.commandOptions['--amend']) {
|
||
targetCommit = this.resolveID('HEAD~1');
|
||
id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id'));
|
||
}
|
||
|
||
var newCommit = this.makeCommit([targetCommit], id);
|
||
if (this.getDetachedHead()) {
|
||
this.command.addWarning('Warning!! Detached HEAD state');
|
||
}
|
||
|
||
this.setTargetLocation(this.HEAD, newCommit);
|
||
return newCommit;
|
||
};
|
||
|
||
GitEngine.prototype.resolveName = function(someRef) {
|
||
// first get the obj
|
||
var obj = this.resolveID(someRef);
|
||
if (obj.get('type') == 'commit') {
|
||
return 'commit ' + obj.get('id');
|
||
}
|
||
if (obj.get('type') == 'branch') {
|
||
return 'branch "' + obj.get('id') + '"';
|
||
}
|
||
// we are dealing with HEAD
|
||
return this.resolveName(obj.get('target'));
|
||
};
|
||
|
||
GitEngine.prototype.resolveID = function(idOrTarget) {
|
||
if (idOrTarget === null || idOrTarget === undefined) {
|
||
throw new Error('Dont call this with null / undefined');
|
||
}
|
||
|
||
if (typeof idOrTarget !== 'string') {
|
||
return idOrTarget;
|
||
}
|
||
return this.resolveStringRef(idOrTarget);
|
||
};
|
||
|
||
GitEngine.prototype.resolveStringRef = function(ref) {
|
||
if (this.refs[ref]) {
|
||
return this.refs[ref];
|
||
}
|
||
|
||
// may be something like HEAD~2 or master^^
|
||
var relativeRefs = [
|
||
[/^([a-zA-Z0-9]+)~(\d+)\s*$/, function(matches) {
|
||
return parseInt(matches[2], 10);
|
||
}],
|
||
[/^([a-zA-Z0-9]+)(\^+)\s*$/, function(matches) {
|
||
return matches[2].length;
|
||
}]
|
||
];
|
||
|
||
var startRef = null;
|
||
var numBack = null;
|
||
_.each(relativeRefs, function(config) {
|
||
var regex = config[0];
|
||
var parse = config[1];
|
||
if (regex.test(ref)) {
|
||
var matches = regex.exec(ref);
|
||
numBack = parse(matches);
|
||
startRef = matches[1];
|
||
}
|
||
}, this);
|
||
|
||
if (!startRef) {
|
||
throw new GitError({
|
||
msg: 'unknown ref ' + ref
|
||
});
|
||
}
|
||
if (!this.refs[startRef]) {
|
||
throw new GitError({
|
||
msg: 'the ref ' + startRef +' does not exist.'
|
||
});
|
||
}
|
||
var commit = this.getCommitFromRef(startRef);
|
||
|
||
return this.numBackFrom(commit, numBack);
|
||
};
|
||
|
||
GitEngine.prototype.getCommitFromRef = function(ref) {
|
||
var start = this.resolveID(ref);
|
||
|
||
// works for both HEAD and just a single layer. aka branch
|
||
while (start.get('type') !== 'commit') {
|
||
start = start.get('target');
|
||
}
|
||
return start;
|
||
};
|
||
|
||
GitEngine.prototype.getType = function(ref) {
|
||
return this.resolveID(ref).get('type');
|
||
};
|
||
|
||
GitEngine.prototype.setTargetLocation = function(ref, target) {
|
||
if (this.getType(ref) == 'commit') {
|
||
// nothing to do
|
||
return;
|
||
}
|
||
|
||
// sets whatever ref is (branch, HEAD, etc) to a target. so if
|
||
// you pass in HEAD, and HEAD is pointing to a branch, it will update
|
||
// the branch to that commit, not the HEAD
|
||
ref = this.getOneBeforeCommit(ref);
|
||
ref.set('target', target);
|
||
};
|
||
|
||
GitEngine.prototype.getUpstreamBranchSet = function() {
|
||
// this is expensive!! so only call once in a while
|
||
var commitToSet = {};
|
||
|
||
var inArray = function(arr, id) {
|
||
var found = false;
|
||
_.each(arr, function(wrapper) {
|
||
if (wrapper.id == id) {
|
||
found = true;
|
||
}
|
||
});
|
||
|
||
return found;
|
||
};
|
||
|
||
var bfsSearch = function(commit) {
|
||
var set = [];
|
||
var pQueue = [commit];
|
||
while (pQueue.length) {
|
||
var popped = pQueue.pop();
|
||
set.push(popped.get('id'));
|
||
|
||
if (popped.get('parents') && popped.get('parents').length) {
|
||
pQueue = pQueue.concat(popped.get('parents'));
|
||
}
|
||
}
|
||
return set;
|
||
};
|
||
|
||
this.branchCollection.each(function(branch) {
|
||
var set = bfsSearch(branch.get('target'));
|
||
_.each(set, function(id) {
|
||
commitToSet[id] = commitToSet[id] || [];
|
||
|
||
// only add it if it's not there, so hue blending is ok
|
||
if (!inArray(commitToSet[id], branch.get('id'))) {
|
||
commitToSet[id].push({
|
||
obj: branch,
|
||
id: branch.get('id')
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
return commitToSet;
|
||
};
|
||
|
||
GitEngine.prototype.getUpstreamHeadSet = function() {
|
||
var set = this.getUpstreamSet('HEAD');
|
||
var including = this.getCommitFromRef('HEAD').get('id');
|
||
|
||
set[including] = true;
|
||
return set;
|
||
};
|
||
|
||
GitEngine.prototype.getOneBeforeCommit = function(ref) {
|
||
// you can call this command on HEAD in detached, HEAD, or on a branch
|
||
// and it will return the ref that is one above a commit. aka
|
||
// it resolves HEAD to something that we can move the ref with
|
||
var start = this.resolveID(ref);
|
||
if (start === this.HEAD && !this.getDetachedHead()) {
|
||
start = start.get('target');
|
||
}
|
||
return start;
|
||
};
|
||
|
||
GitEngine.prototype.numBackFrom = function(commit, numBack) {
|
||
// going back '3' from a given ref is not trivial, for you might have
|
||
// a bunch of merge commits and such. like this situation:
|
||
//
|
||
// * merge master into new
|
||
// |\
|
||
// | \* commit here
|
||
// |* \ commit there
|
||
// | |* commit here
|
||
// \ /
|
||
// | * root
|
||
//
|
||
//
|
||
// hence we need to do a BFS search, with the commit date being the
|
||
// value to sort off of (rather than just purely the level)
|
||
if (numBack === 0) {
|
||
return commit;
|
||
}
|
||
|
||
// we use a special sorting function here that
|
||
// prefers the later commits over the earlier ones
|
||
var sortQueue = _.bind(function(queue) {
|
||
queue.sort(this.dateSortFunc);
|
||
}, this);
|
||
|
||
var pQueue = [].concat(commit.get('parents') || []);
|
||
sortQueue(pQueue);
|
||
numBack--;
|
||
|
||
while (pQueue.length && numBack !== 0) {
|
||
var popped = pQueue.shift(0);
|
||
var parents = popped.get('parents');
|
||
|
||
if (parents && parents.length) {
|
||
pQueue = pQueue.concat(parents);
|
||
}
|
||
|
||
sortQueue(pQueue);
|
||
numBack--;
|
||
}
|
||
|
||
if (numBack !== 0 || pQueue.length === 0) {
|
||
throw new GitError({
|
||
msg: "Sorry, I can't go that many commits back"
|
||
});
|
||
}
|
||
return pQueue.shift(0);
|
||
};
|
||
|
||
GitEngine.prototype.scrapeBaseID = function(id) {
|
||
var results = /^C(\d+)/.exec(id);
|
||
|
||
if (!results) {
|
||
throw new Error('regex failed on ' + id);
|
||
}
|
||
|
||
return 'C' + results[1];
|
||
};
|
||
|
||
GitEngine.prototype.rebaseAltID = function(id) {
|
||
// this function alters an ID to add a quote to the end,
|
||
// indicating that it was rebased. it also checks existence
|
||
var regexMap = [
|
||
[/^C(\d+)[']{0,2}$/, function(bits) {
|
||
// this id can use another quote, so just add it
|
||
return bits[0] + "'";
|
||
}],
|
||
[/^C(\d+)[']{3}$/, function(bits) {
|
||
// here we switch from C''' to C'^4
|
||
return bits[0].slice(0, -3) + "'^4";
|
||
}],
|
||
[/^C(\d+)['][\^](\d+)$/, function(bits) {
|
||
return 'C' + String(bits[1]) + "'^" + String(Number(bits[2]) + 1);
|
||
}]
|
||
];
|
||
|
||
for (var i = 0; i < regexMap.length; i++) {
|
||
var regex = regexMap[i][0];
|
||
var func = regexMap[i][1];
|
||
var results = regex.exec(id);
|
||
if (results) {
|
||
var newId = func(results);
|
||
// if this id exists, continue down the rabbit hole
|
||
if (this.refs[newId]) {
|
||
return this.rebaseAltID(newId);
|
||
} else {
|
||
return newId;
|
||
}
|
||
}
|
||
}
|
||
throw new Error('could not modify the id ' + id);
|
||
};
|
||
|
||
GitEngine.prototype.idSortFunc = function(cA, cB) {
|
||
// commit IDs can come in many forms:
|
||
// C4
|
||
// C4' (from a rebase)
|
||
// C4'' (from multiple rebases)
|
||
// C4'^3 (from a BUNCH of rebases)
|
||
|
||
var scale = 1000;
|
||
|
||
var regexMap = [
|
||
[/^C(\d+)$/, function(bits) {
|
||
// return the 4 from C4
|
||
return scale * bits[1];
|
||
}],
|
||
[/^C(\d+)([']+)$/, function(bits) {
|
||
// return the 4 from C4, plus the length of the quotes
|
||
return scale * bits[1] + bits[2].length;
|
||
}],
|
||
[/^C(\d+)['][\^](\d+)$/, function(bits) {
|
||
return scale * bits[1] + Number(bits[2]);
|
||
}]
|
||
];
|
||
|
||
var getNumToSort = function(id) {
|
||
for (var i = 0; i < regexMap.length; i++) {
|
||
var regex = regexMap[i][0];
|
||
var func = regexMap[i][1];
|
||
var results = regex.exec(id);
|
||
if (results) {
|
||
return func(results);
|
||
}
|
||
}
|
||
throw new Error('Could not parse commit ID ' + id);
|
||
};
|
||
|
||
return getNumToSort(cA.get('id')) - getNumToSort(cB.get('id'));
|
||
};
|
||
|
||
GitEngine.prototype.dateSortFunc = function(cA, cB) {
|
||
var dateA = new Date(cA.get('createTime'));
|
||
var dateB = new Date(cB.get('createTime'));
|
||
return dateA - dateB;
|
||
};
|
||
|
||
GitEngine.prototype.rebaseInteractiveStarter = function() {
|
||
var args = this.commandOptions['-i'];
|
||
this.twoArgsImpliedHead(args, ' -i');
|
||
|
||
this.rebaseInteractive(args[0], args[1]);
|
||
};
|
||
|
||
GitEngine.prototype.rebaseStarter = function() {
|
||
if (this.commandOptions['-i']) {
|
||
this.rebaseInteractiveStarter();
|
||
return;
|
||
}
|
||
|
||
this.twoArgsImpliedHead(this.generalArgs);
|
||
|
||
var response = this.rebase(this.generalArgs[0], this.generalArgs[1]);
|
||
|
||
if (response === undefined) {
|
||
// was a fastforward or already up to date. returning now
|
||
// will trigger the refresh animation by not adding anything to
|
||
// the animation queue
|
||
return;
|
||
}
|
||
|
||
this.animationFactory.rebaseAnimation(this.animationQueue, response, this, this.gitVisuals);
|
||
};
|
||
|
||
GitEngine.prototype.rebase = function(targetSource, currentLocation) {
|
||
// first some conditions
|
||
if (this.isUpstreamOf(targetSource, currentLocation)) {
|
||
this.command.setResult('Branch already up-to-date');
|
||
|
||
// git for some reason always checks out the branch you are rebasing,
|
||
// no matter the result of the rebase
|
||
this.checkout(currentLocation);
|
||
|
||
// returning instead of throwing makes a tree refresh
|
||
return;
|
||
}
|
||
|
||
if (this.isUpstreamOf(currentLocation, targetSource)) {
|
||
// just set the target of this current location to the source
|
||
this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
|
||
// we need the refresh tree animation to happen, so set the result directly
|
||
// instead of throwing
|
||
this.command.setResult('Fast-forwarding...');
|
||
|
||
this.checkout(currentLocation);
|
||
return;
|
||
}
|
||
|
||
// now the part of actually rebasing.
|
||
// We need to get the downstream set of targetSource first.
|
||
// then we BFS from currentLocation, using the downstream set as our stopping point.
|
||
// we need to BFS because we need to include all commits below
|
||
// pop these commits on top of targetSource and modify their ids with quotes
|
||
var stopSet = this.getUpstreamSet(targetSource);
|
||
|
||
// now BFS from here on out
|
||
var toRebaseRough = [];
|
||
var pQueue = [this.getCommitFromRef(currentLocation)];
|
||
|
||
while (pQueue.length) {
|
||
var popped = pQueue.pop();
|
||
|
||
// if its in the set, dont add it
|
||
if (stopSet[popped.get('id')]) {
|
||
continue;
|
||
}
|
||
|
||
// it's not in the set, so we need to rebase this commit
|
||
toRebaseRough.push(popped);
|
||
toRebaseRough.sort(this.dateSortFunc);
|
||
|
||
// keep searching
|
||
pQueue = pQueue.concat(popped.get('parents'));
|
||
}
|
||
|
||
return this.rebaseFinish(toRebaseRough, stopSet, targetSource, currentLocation);
|
||
};
|
||
|
||
GitEngine.prototype.rebaseInteractive = function(targetSource, currentLocation) {
|
||
// there are a reduced set of checks now, so we can't exactly use parts of the rebase function
|
||
// but it will look similar.
|
||
|
||
// first if we are upstream of the target
|
||
if (this.isUpstreamOf(currentLocation, targetSource)) {
|
||
throw new GitError({
|
||
msg: 'Nothing to do... (git throws a "noop" status here); ' +
|
||
'Your source is upstream of your rebase target'
|
||
});
|
||
}
|
||
|
||
// now get the stop set
|
||
var stopSet = this.getUpstreamSet(targetSource);
|
||
|
||
var toRebaseRough = [];
|
||
// standard BFS
|
||
var pQueue = [this.getCommitFromRef(currentLocation)];
|
||
|
||
while (pQueue.length) {
|
||
var popped = pQueue.pop();
|
||
|
||
if (stopSet[popped.get('id')]) {
|
||
continue;
|
||
}
|
||
|
||
toRebaseRough.push(popped);
|
||
pQueue = pQueue.concat(popped.get('parents'));
|
||
pQueue.sort(this.dateSortFunc);
|
||
}
|
||
|
||
// throw out merge's real fast and see if we have anything to do
|
||
var toRebase = [];
|
||
_.each(toRebaseRough, function(commit) {
|
||
if (commit.get('parents').length == 1) {
|
||
toRebase.push(commit);
|
||
}
|
||
});
|
||
|
||
if (!toRebase.length) {
|
||
throw new GitError({
|
||
msg: 'No commits to rebase! Everything is a merge commit'
|
||
});
|
||
}
|
||
|
||
// now do stuff :D since all our validation checks have passed, we are going to defer animation
|
||
// and actually launch the dialog
|
||
this.animationQueue.set('defer', true);
|
||
|
||
var deferred = Q.defer();
|
||
deferred.promise
|
||
.then(_.bind(function(userSpecifiedRebase) {
|
||
// first, they might have dropped everything (annoying)
|
||
if (!userSpecifiedRebase.length) {
|
||
throw new CommandResult({
|
||
msg: 'Nothing to do...'
|
||
});
|
||
}
|
||
|
||
// finish the rebase crap and animate!
|
||
var animationData = this.rebaseFinish(userSpecifiedRebase, {}, targetSource, currentLocation);
|
||
this.animationFactory.rebaseAnimation(this.animationQueue, animationData, this, this.gitVisuals);
|
||
this.animationQueue.start();
|
||
}, this))
|
||
.fail(_.bind(function(err) {
|
||
this.filterError(err);
|
||
this.command.set('error', err);
|
||
this.animationQueue.start();
|
||
}, this))
|
||
.done();
|
||
|
||
var InteractiveRebaseView = require('../views/rebaseView').InteractiveRebaseView;
|
||
// interactive rebase view will reject or resolve our promise
|
||
new InteractiveRebaseView({
|
||
deferred: deferred,
|
||
toRebase: toRebase
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.rebaseFinish = function(toRebaseRough, stopSet, targetSource, currentLocation) {
|
||
// now we have the all the commits between currentLocation and the set of target to rebase.
|
||
var animationResponse = {};
|
||
animationResponse.destinationBranch = this.resolveID(targetSource);
|
||
|
||
// we need to throw out merge commits
|
||
var toRebase = [];
|
||
_.each(toRebaseRough, function(commit) {
|
||
if (commit.get('parents').length == 1) {
|
||
toRebase.push(commit);
|
||
}
|
||
});
|
||
|
||
// we ALSO need to throw out commits that will do the same changes. like
|
||
// if the upstream set has a commit C4 and we have C4', we dont rebase the C4' again.
|
||
// get this by doing ID scraping
|
||
var changesAlreadyMade = {};
|
||
_.each(stopSet, function(val, key) {
|
||
changesAlreadyMade[this.scrapeBaseID(key)] = val; // val == true
|
||
}, this);
|
||
|
||
// now get rid of the commits that will redo same changes
|
||
toRebaseRough = toRebase;
|
||
toRebase = [];
|
||
_.each(toRebaseRough, function(commit) {
|
||
var baseID = this.scrapeBaseID(commit.get('id'));
|
||
if (!changesAlreadyMade[baseID]) {
|
||
toRebase.push(commit);
|
||
}
|
||
}, this);
|
||
|
||
if (!toRebase.length) {
|
||
throw new GitError({
|
||
msg: 'No Commits to Rebase! Everything else is merge commits or changes already have been applied'
|
||
});
|
||
}
|
||
|
||
// now reverse it once more to get it in the right order
|
||
toRebase.reverse();
|
||
animationResponse.toRebaseArray = toRebase.slice(0);
|
||
|
||
// now pop all of these commits onto targetLocation
|
||
var base = this.getCommitFromRef(targetSource);
|
||
|
||
// do the rebase, and also maintain all our animation info during this
|
||
animationResponse.rebaseSteps = [];
|
||
var beforeSnapshot = this.gitVisuals.genSnapshot();
|
||
var afterSnapshot;
|
||
_.each(toRebase, function(old) {
|
||
var newId = this.rebaseAltID(old.get('id'));
|
||
|
||
var newCommit = this.makeCommit([base], newId);
|
||
base = newCommit;
|
||
|
||
// animation info
|
||
afterSnapshot = this.gitVisuals.genSnapshot();
|
||
animationResponse.rebaseSteps.push({
|
||
oldCommit: old,
|
||
newCommit: newCommit,
|
||
beforeSnapshot: beforeSnapshot,
|
||
afterSnapshot: afterSnapshot
|
||
});
|
||
beforeSnapshot = afterSnapshot;
|
||
}, this);
|
||
|
||
if (this.resolveID(currentLocation).get('type') == 'commit') {
|
||
// we referenced a commit like git rebase C2 C1, so we have
|
||
// to manually check out C1'
|
||
|
||
var steps = animationResponse.rebaseSteps;
|
||
var newestCommit = steps[steps.length - 1].newCommit;
|
||
|
||
this.checkout(newestCommit);
|
||
} else {
|
||
// now we just need to update the rebased branch is
|
||
this.setTargetLocation(currentLocation, base);
|
||
this.checkout(currentLocation);
|
||
}
|
||
|
||
// for animation
|
||
return animationResponse;
|
||
};
|
||
|
||
GitEngine.prototype.mergeStarter = function() {
|
||
this.twoArgsImpliedHead(this.generalArgs);
|
||
|
||
var newCommit = this.merge(this.generalArgs[0], this.generalArgs[1]);
|
||
|
||
if (newCommit === undefined) {
|
||
// its just a fast forwrard
|
||
this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
|
||
return;
|
||
}
|
||
|
||
this.animationFactory.genCommitBirthAnimation(this.animationQueue, newCommit, this.gitVisuals);
|
||
};
|
||
|
||
GitEngine.prototype.merge = function(targetSource, currentLocation) {
|
||
// first some conditions
|
||
if (this.isUpstreamOf(targetSource, currentLocation) ||
|
||
this.getCommitFromRef(targetSource) === this.getCommitFromRef(currentLocation)) {
|
||
throw new CommandResult({
|
||
msg: 'Branch already up-to-date'
|
||
});
|
||
}
|
||
|
||
if (this.isUpstreamOf(currentLocation, targetSource)) {
|
||
// just set the target of this current location to the source
|
||
this.setTargetLocation(currentLocation, this.getCommitFromRef(targetSource));
|
||
// get fresh animation to happen
|
||
this.command.setResult('Fast-forwarding...');
|
||
return;
|
||
}
|
||
|
||
// now the part of making a merge commit
|
||
var parent1 = this.getCommitFromRef(currentLocation);
|
||
var parent2 = this.getCommitFromRef(targetSource);
|
||
|
||
// we need a fancy commit message
|
||
var msg = 'Merge ' + this.resolveName(targetSource) +
|
||
' into ' + this.resolveName(currentLocation);
|
||
|
||
// since we specify parent 1 as the first parent, it is the "main" parent
|
||
// and the node will be displayed below that branch / commit / whatever
|
||
var mergeCommit = this.makeCommit(
|
||
[parent1, parent2],
|
||
null,
|
||
{
|
||
commitMessage: msg
|
||
}
|
||
);
|
||
|
||
this.setTargetLocation(currentLocation, mergeCommit);
|
||
return mergeCommit;
|
||
};
|
||
|
||
GitEngine.prototype.checkoutStarter = function() {
|
||
var args = null;
|
||
if (this.commandOptions['-b']) {
|
||
// the user is really trying to just make a branch and then switch to it. so first:
|
||
args = this.commandOptions['-b'];
|
||
this.twoArgsImpliedHead(args, '-b');
|
||
|
||
var validId = this.validateBranchName(args[0]);
|
||
this.branch(validId, args[1]);
|
||
this.checkout(validId);
|
||
return;
|
||
}
|
||
|
||
if (this.commandOptions['-']) {
|
||
// get the heads last location
|
||
var lastPlace = this.HEAD.get('lastLastTarget');
|
||
if (!lastPlace) {
|
||
throw new GitError({
|
||
msg: 'Need a previous location to do - switching'
|
||
});
|
||
}
|
||
this.HEAD.set('target', lastPlace);
|
||
return;
|
||
}
|
||
|
||
if (this.commandOptions['-B']) {
|
||
args = this.commandOptions['-B'];
|
||
this.twoArgsImpliedHead(args, '-B');
|
||
|
||
this.forceBranch(args[0], args[1]);
|
||
this.checkout(args[0]);
|
||
return;
|
||
}
|
||
|
||
this.validateArgBounds(this.generalArgs, 1, 1);
|
||
|
||
this.checkout(this.unescapeQuotes(this.generalArgs[0]));
|
||
};
|
||
|
||
GitEngine.prototype.checkout = function(idOrTarget) {
|
||
var target = this.resolveID(idOrTarget);
|
||
if (target.get('id') === 'HEAD') {
|
||
// git checkout HEAD is a
|
||
// meaningless command but i used to do this back in the day
|
||
return;
|
||
}
|
||
|
||
var type = target.get('type');
|
||
if (type !== 'branch' && type !== 'commit') {
|
||
throw new GitError({
|
||
msg: 'can only checkout branches and commits!'
|
||
});
|
||
}
|
||
|
||
this.HEAD.set('target', target);
|
||
};
|
||
|
||
GitEngine.prototype.branchStarter = function() {
|
||
var args = null;
|
||
// handle deletion first
|
||
if (this.commandOptions['-d'] || this.commandOptions['-D']) {
|
||
var names = this.commandOptions['-d'] || this.commandOptions['-D'];
|
||
this.validateArgBounds(names, 1, NaN, '-d');
|
||
|
||
_.each(names, function(name) {
|
||
this.deleteBranch(name);
|
||
}, this);
|
||
return;
|
||
}
|
||
|
||
if (this.commandOptions['--contains']) {
|
||
args = this.commandOptions['--contains'];
|
||
this.validateArgBounds(args, 1, 1, '--contains');
|
||
this.printBranchesWithout(args[0]);
|
||
return;
|
||
}
|
||
|
||
if (this.commandOptions['-f']) {
|
||
args = this.commandOptions['-f'];
|
||
this.twoArgsImpliedHead(args, '-f');
|
||
|
||
// we want to force a branch somewhere
|
||
this.forceBranch(args[0], args[1]);
|
||
return;
|
||
}
|
||
|
||
|
||
if (this.generalArgs.length === 0) {
|
||
this.printBranches(this.getBranches());
|
||
return;
|
||
}
|
||
|
||
this.twoArgsImpliedHead(this.generalArgs);
|
||
this.branch(this.generalArgs[0], this.generalArgs[1]);
|
||
};
|
||
|
||
GitEngine.prototype.forceBranch = function(branchName, where) {
|
||
// if branchname doesn't exist...
|
||
if (!this.refs[branchName]) {
|
||
this.branch(branchName, where);
|
||
}
|
||
|
||
var branch = this.resolveID(branchName);
|
||
if (branch.get('type') !== 'branch') {
|
||
throw new GitError({
|
||
msg: "Can't force move anything but a branch!!"
|
||
});
|
||
}
|
||
|
||
var whereCommit = this.getCommitFromRef(where);
|
||
|
||
this.setTargetLocation(branch, whereCommit);
|
||
};
|
||
|
||
GitEngine.prototype.branch = function(name, ref) {
|
||
var target = this.getCommitFromRef(ref);
|
||
this.makeBranch(name, target);
|
||
};
|
||
|
||
GitEngine.prototype.deleteBranch = function(name) {
|
||
// trying to delete, lets check our refs
|
||
var target = this.resolveID(name);
|
||
if (target.get('type') !== 'branch') {
|
||
throw new GitError({
|
||
msg: "You can't delete things that arent branches with branch command"
|
||
});
|
||
}
|
||
if (target.get('id') == 'master') {
|
||
throw new GitError({
|
||
msg: "You can't delete the master branch!"
|
||
});
|
||
}
|
||
if (this.HEAD.get('target') === target) {
|
||
throw new GitError({
|
||
msg: "Cannot delete the branch you are currently on"
|
||
});
|
||
}
|
||
|
||
// now we know it's a branch
|
||
var branch = target;
|
||
|
||
this.branchCollection.remove(branch);
|
||
this.refs[branch.get('id')] = undefined;
|
||
delete this.refs[branch.get('id')];
|
||
|
||
if (branch.get('visBranch')) {
|
||
branch.get('visBranch').remove();
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.unescapeQuotes = function(str) {
|
||
return str.replace(/'/g, "'");
|
||
};
|
||
|
||
GitEngine.prototype.filterError = function(err) {
|
||
if (!(err instanceof GitError ||
|
||
err instanceof CommandResult)) {
|
||
throw err;
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.dispatch = function(command, deferred) {
|
||
// current command, options, and args are stored in the gitEngine
|
||
// for easy reference during processing.
|
||
this.command = command;
|
||
this.commandOptions = command.get('supportedMap');
|
||
this.generalArgs = command.get('generalArgs');
|
||
|
||
// set up the animation queue
|
||
var whenDone = _.bind(function() {
|
||
command.finishWith(deferred);
|
||
}, this);
|
||
this.animationQueue = new AnimationQueue({
|
||
callback: whenDone
|
||
});
|
||
|
||
try {
|
||
var methodName = command.get('method').replace(/-/g, '') + 'Starter';
|
||
this[methodName]();
|
||
} catch (err) {
|
||
this.filterError(err);
|
||
// short circuit animation by just setting error and returning
|
||
command.set('error', err);
|
||
deferred.resolve();
|
||
return;
|
||
}
|
||
|
||
// only add the refresh if we didn't do manual animations
|
||
if (!this.animationQueue.get('animations').length && !this.animationQueue.get('defer')) {
|
||
this.animationFactory.refreshTree(this.animationQueue, this.gitVisuals);
|
||
}
|
||
|
||
// animation queue will call the callback when its done
|
||
if (!this.animationQueue.get('defer')) {
|
||
this.animationQueue.start();
|
||
}
|
||
};
|
||
|
||
GitEngine.prototype.showStarter = function() {
|
||
this.oneArgImpliedHead(this.generalArgs);
|
||
|
||
this.show(this.generalArgs[0]);
|
||
};
|
||
|
||
GitEngine.prototype.show = function(ref) {
|
||
var commit = this.getCommitFromRef(ref);
|
||
|
||
throw new CommandResult({
|
||
msg: commit.getShowEntry()
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.statusStarter = function() {
|
||
var lines = [];
|
||
if (this.getDetachedHead()) {
|
||
lines.push('Detached Head!');
|
||
} else {
|
||
var branchName = this.HEAD.get('target').get('id');
|
||
lines.push('On branch ' + branchName);
|
||
}
|
||
lines.push('Changes to be committed:');
|
||
lines.push('');
|
||
lines.push(' modified: cal/OskiCostume.stl');
|
||
lines.push('');
|
||
lines.push('Ready to commit! (as always in this demo)');
|
||
|
||
var msg = '';
|
||
_.each(lines, function(line) {
|
||
msg += '# ' + line + '\n';
|
||
});
|
||
|
||
throw new CommandResult({
|
||
msg: msg
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.logStarter = function() {
|
||
if (this.generalArgs.length == 2) {
|
||
// do fancy git log branchA ^branchB
|
||
if (this.generalArgs[1][0] == '^') {
|
||
this.logWithout(this.generalArgs[0], this.generalArgs[1]);
|
||
} else {
|
||
throw new GitError({
|
||
msg: 'I need a not branch (^branchName) when getting two arguments!'
|
||
});
|
||
}
|
||
}
|
||
|
||
this.oneArgImpliedHead(this.generalArgs);
|
||
this.log(this.generalArgs[0]);
|
||
};
|
||
|
||
GitEngine.prototype.logWithout = function(ref, omitBranch) {
|
||
// slice off the ^branch
|
||
omitBranch = omitBranch.slice(1);
|
||
this.log(ref, this.getUpstreamSet(omitBranch));
|
||
};
|
||
|
||
GitEngine.prototype.log = function(ref, omitSet) {
|
||
// omit set is for doing stuff like git log branchA ^branchB
|
||
omitSet = omitSet || {};
|
||
// first get the commit we referenced
|
||
var commit = this.getCommitFromRef(ref);
|
||
|
||
// then get as many far back as we can from here, order by commit date
|
||
var toDump = [];
|
||
var pQueue = [commit];
|
||
|
||
var seen = {};
|
||
|
||
while (pQueue.length) {
|
||
var popped = pQueue.shift(0);
|
||
if (seen[popped.get('id')] || omitSet[popped.get('id')]) {
|
||
continue;
|
||
}
|
||
seen[popped.get('id')] = true;
|
||
|
||
toDump.push(popped);
|
||
|
||
if (popped.get('parents') && popped.get('parents').length) {
|
||
pQueue = pQueue.concat(popped.get('parents'));
|
||
}
|
||
}
|
||
|
||
// now go through and collect logs
|
||
var bigLogStr = '';
|
||
_.each(toDump, function(c) {
|
||
bigLogStr += c.getLogEntry();
|
||
}, this);
|
||
|
||
throw new CommandResult({
|
||
msg: bigLogStr
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.addStarter = function() {
|
||
throw new CommandResult({
|
||
msg: "This demo is meant to demonstrate git branching, so don't worry about " +
|
||
"adding / staging files. Just go ahead and commit away!"
|
||
});
|
||
};
|
||
|
||
GitEngine.prototype.getCommonAncestor = function(ancestor, cousin) {
|
||
if (this.isUpstreamOf(cousin, ancestor)) {
|
||
throw new Error('Dont use common ancestor if we are upstream!');
|
||
}
|
||
|
||
var upstreamSet = this.getUpstreamSet(ancestor);
|
||
// now BFS off of cousin until you find something
|
||
|
||
var queue = [this.getCommitFromRef(cousin)];
|
||
while (queue.length) {
|
||
var here = queue.pop();
|
||
if (upstreamSet[here.get('id')]) {
|
||
return here;
|
||
}
|
||
queue = queue.concat(here.get('parents'));
|
||
}
|
||
throw new Error('something has gone very wrong... two nodes arent connected!');
|
||
};
|
||
|
||
GitEngine.prototype.isUpstreamOf = function(child, ancestor) {
|
||
child = this.getCommitFromRef(child);
|
||
|
||
// basically just do a completely BFS search on ancestor to the root, then
|
||
// check for membership of child in that set of explored nodes
|
||
var upstream = this.getUpstreamSet(ancestor);
|
||
return upstream[child.get('id')] !== undefined;
|
||
};
|
||
|
||
GitEngine.prototype.getUpstreamSet = function(ancestor) {
|
||
var commit = this.getCommitFromRef(ancestor);
|
||
var ancestorID = commit.get('id');
|
||
var queue = [commit];
|
||
|
||
var exploredSet = {};
|
||
exploredSet[ancestorID] = true;
|
||
|
||
var addToExplored = function(rent) {
|
||
exploredSet[rent.get('id')] = true;
|
||
queue.push(rent);
|
||
};
|
||
|
||
while (queue.length) {
|
||
var here = queue.pop();
|
||
var rents = here.get('parents');
|
||
|
||
_.each(rents, addToExplored);
|
||
}
|
||
return exploredSet;
|
||
};
|
||
|
||
|
||
var Ref = Backbone.Model.extend({
|
||
initialize: function() {
|
||
if (!this.get('target')) {
|
||
throw new Error('must be initialized with target');
|
||
}
|
||
if (!this.get('id')) {
|
||
throw new Error('must be given an id');
|
||
}
|
||
this.set('type', 'general ref');
|
||
|
||
if (this.get('id') == 'HEAD') {
|
||
this.set('lastLastTarget', null);
|
||
this.set('lastTarget', this.get('target'));
|
||
// have HEAD remember where it is for checkout -
|
||
this.on('change:target', this.targetChanged, this);
|
||
}
|
||
},
|
||
|
||
targetChanged: function(model, targetValue, ev) {
|
||
// push our little 3 stack back. we need to do this because
|
||
// backbone doesn't give you what the value WAS, only what it was changed
|
||
// TO
|
||
this.set('lastLastTarget', this.get('lastTarget'));
|
||
this.set('lastTarget', targetValue);
|
||
},
|
||
|
||
toString: function() {
|
||
return 'a ' + this.get('type') + 'pointing to ' + String(this.get('target'));
|
||
}
|
||
});
|
||
|
||
var Branch = Ref.extend({
|
||
defaults: {
|
||
visBranch: null
|
||
},
|
||
|
||
initialize: function() {
|
||
Ref.prototype.initialize.call(this);
|
||
this.set('type', 'branch');
|
||
}
|
||
});
|
||
|
||
var Commit = Backbone.Model.extend({
|
||
defaults: {
|
||
type: 'commit',
|
||
children: null,
|
||
parents: null,
|
||
author: 'Peter Cottle',
|
||
createTime: null,
|
||
commitMessage: null,
|
||
visNode: null,
|
||
gitVisuals: null
|
||
},
|
||
|
||
constants: {
|
||
circularFields: ['gitVisuals', 'visNode', 'children']
|
||
},
|
||
|
||
getLogEntry: function() {
|
||
// for now we are just joining all these things with newlines which
|
||
// will get placed by paragraph tags. Not really a fan of this, but
|
||
// it's better than making an entire template and all that jazz
|
||
return [
|
||
'Author: ' + this.get('author'),
|
||
'Date: ' + this.get('createTime'),
|
||
'<br/>',
|
||
this.get('commitMessage'),
|
||
'<br/>',
|
||
'Commit: ' + this.get('id')
|
||
].join('\n' ) + '\n';
|
||
},
|
||
|
||
getShowEntry: function() {
|
||
// same deal as above, show log entry and some fake changes
|
||
return [
|
||
this.getLogEntry(),
|
||
'diff --git a/bigGameResults.html b/bigGameResults.html',
|
||
'--- bigGameResults.html',
|
||
'+++ bigGameResults.html',
|
||
'@@ 13,27 @@ Winner, Score',
|
||
'- Stanfurd, 14-7',
|
||
'+ Cal, 21-14'
|
||
].join('\n') + '\n';
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (!this.get('id')) {
|
||
throw new Error('Need ID!!');
|
||
}
|
||
|
||
if (!this.get('createTime')) {
|
||
this.set('createTime', new Date().toString());
|
||
}
|
||
if (!this.get('commitMessage')) {
|
||
this.set('commitMessage', 'Quick Commit. Go Bears!');
|
||
}
|
||
|
||
this.set('children', []);
|
||
|
||
// root commits have no parents
|
||
if (!this.get('rootCommit')) {
|
||
if (!this.get('parents') || !this.get('parents').length) {
|
||
throw new Error('needs parents');
|
||
}
|
||
}
|
||
},
|
||
|
||
addNodeToVisuals: function() {
|
||
var visNode = this.get('gitVisuals').addNode(this.get('id'), this);
|
||
this.set('visNode', visNode);
|
||
},
|
||
|
||
addEdgeToVisuals: function(parent) {
|
||
this.get('gitVisuals').addEdge(this.get('id'), parent.get('id'));
|
||
},
|
||
|
||
isMainParent: function(parent) {
|
||
var index = this.get('parents').indexOf(parent);
|
||
return index === 0;
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.validateAtInit();
|
||
this.addNodeToVisuals();
|
||
|
||
_.each(this.get('parents'), function(parent) {
|
||
parent.get('children').push(this);
|
||
this.addEdgeToVisuals(parent);
|
||
}, this);
|
||
}
|
||
});
|
||
|
||
exports.GitEngine = GitEngine;
|
||
exports.Commit = Commit;
|
||
exports.Branch = Branch;
|
||
exports.Ref = Ref;
|
||
|
||
|
||
});
|
||
require("/src/js/git/index.js");
|
||
|
||
require.define("/src/js/git/treeCompare.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
// static class...
|
||
function TreeCompare() {
|
||
|
||
}
|
||
|
||
TreeCompare.prototype.compareBranchesWithinTrees = function(treeA, treeB, branches) {
|
||
var result = true;
|
||
_.each(branches, function(branchName) {
|
||
result = result && this.compareBranchWithinTrees(treeA, treeB, branchName);
|
||
}, this);
|
||
|
||
return result;
|
||
};
|
||
|
||
TreeCompare.prototype.compareBranchWithinTrees = function(treeA, treeB, branchName) {
|
||
treeA = this.convertTreeSafe(treeA);
|
||
treeB = this.convertTreeSafe(treeB);
|
||
|
||
this.reduceTreeFields([treeA, treeB]);
|
||
|
||
// we need a recursive comparison function to bubble up the branch
|
||
var recurseCompare = function(commitA, commitB) {
|
||
// this is the short-circuit base case
|
||
var result = _.isEqual(commitA, commitB);
|
||
if (!result) {
|
||
return false;
|
||
}
|
||
|
||
// we loop through each parent ID. we sort the parent ID's beforehand
|
||
// so the index lookup is valid
|
||
_.each(commitA.parents, function(pAid, index) {
|
||
var pBid = commitB.parents[index];
|
||
|
||
var childA = treeA.commits[pAid];
|
||
var childB = treeB.commits[pBid];
|
||
|
||
result = result && recurseCompare(childA, childB);
|
||
}, this);
|
||
// if each of our children recursively are equal, we are good
|
||
return result;
|
||
};
|
||
|
||
var branchA = treeA.branches[branchName];
|
||
var branchB = treeB.branches[branchName];
|
||
|
||
return _.isEqual(branchA, branchB) &&
|
||
recurseCompare(treeA.commits[branchA.target], treeB.commits[branchB.target]);
|
||
};
|
||
|
||
TreeCompare.prototype.convertTreeSafe = function(tree) {
|
||
if (typeof tree == 'string') {
|
||
return JSON.parse(unescape(tree));
|
||
}
|
||
return tree;
|
||
};
|
||
|
||
TreeCompare.prototype.reduceTreeFields = function(trees) {
|
||
var commitSaveFields = [
|
||
'parents',
|
||
'id',
|
||
'rootCommit'
|
||
];
|
||
var commitSortFields = ['children', 'parents'];
|
||
var branchSaveFields = [
|
||
'target',
|
||
'id'
|
||
];
|
||
|
||
// this function saves only the specified fields of a tree
|
||
var saveOnly = function(tree, treeKey, saveFields, sortFields) {
|
||
var objects = tree[treeKey];
|
||
_.each(objects, function(obj, objKey) {
|
||
// our blank slate to copy over
|
||
var blank = {};
|
||
_.each(saveFields, function(field) {
|
||
if (obj[field] !== undefined) {
|
||
blank[field] = obj[field];
|
||
}
|
||
});
|
||
|
||
_.each(sortFields, function(field) {
|
||
// also sort some fields
|
||
if (obj[field]) {
|
||
obj[field].sort();
|
||
blank[field] = obj[field];
|
||
}
|
||
});
|
||
tree[treeKey][objKey] = blank;
|
||
});
|
||
};
|
||
|
||
_.each(trees, function(tree) {
|
||
saveOnly(tree, 'commits', commitSaveFields, commitSortFields);
|
||
saveOnly(tree, 'branches', branchSaveFields);
|
||
|
||
tree.HEAD = {
|
||
target: tree.HEAD.target,
|
||
id: tree.HEAD.id
|
||
};
|
||
});
|
||
};
|
||
|
||
TreeCompare.prototype.compareTrees = function(treeA, treeB) {
|
||
treeA = this.convertTreeSafe(treeA);
|
||
treeB = this.convertTreeSafe(treeB);
|
||
|
||
// now we need to strip out the fields we don't care about, aka things
|
||
// like createTime, message, author
|
||
this.reduceTreeFields([treeA, treeB]);
|
||
|
||
console.log('comparing tree A', treeA, 'to', treeB);
|
||
|
||
return _.isEqual(treeA, treeB);
|
||
};
|
||
|
||
exports.TreeCompare = TreeCompare;
|
||
|
||
|
||
});
|
||
require("/src/js/git/treeCompare.js");
|
||
|
||
require.define("/src/js/level/disabledMap.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var GitCommands = require('../git/commands');
|
||
|
||
var Errors = require('../util/errors');
|
||
var GitError = Errors.GitError;
|
||
|
||
function DisabledMap(options) {
|
||
options = options || {};
|
||
this.disabledMap = options.disabledMap || {
|
||
'git cherry-pick': true,
|
||
'git rebase': true
|
||
};
|
||
}
|
||
|
||
DisabledMap.prototype.getInstantCommands = function() {
|
||
// this produces an array of regex / function pairs that can be
|
||
// piped into a parse waterfall to disable certain git commmands
|
||
// :D
|
||
var instants = [];
|
||
var onMatch = function() {
|
||
throw new GitError({
|
||
msg: 'That git command is disabled for this level!'
|
||
});
|
||
};
|
||
|
||
_.each(this.disabledMap, function(val, disabledCommand) {
|
||
var gitRegex = GitCommands.regexMap[disabledCommand];
|
||
if (!gitRegex) {
|
||
throw new Error('wuttttt this disbaled command' + disabledCommand +
|
||
' has no regex matching');
|
||
}
|
||
instants.push([gitRegex, onMatch]);
|
||
});
|
||
return instants;
|
||
};
|
||
|
||
exports.DisabledMap = DisabledMap;
|
||
|
||
|
||
});
|
||
require("/src/js/level/disabledMap.js");
|
||
|
||
require.define("/src/js/level/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var Q = require('q');
|
||
|
||
var util = require('../util');
|
||
var Main = require('../app');
|
||
|
||
var Sandbox = require('../level/sandbox').Sandbox;
|
||
|
||
var Visualization = require('../visuals/visualization').Visualization;
|
||
var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
|
||
var DisabledMap = require('../level/disabledMap').DisabledMap;
|
||
var Command = require('../models/commandModel').Command;
|
||
var GitShim = require('../git/gitShim').GitShim;
|
||
|
||
var ModalTerminal = require('../views').ModalTerminal;
|
||
var ModalAlert = require('../views').ModalAlert;
|
||
var MultiView = require('../views/multiView').MultiView;
|
||
|
||
var TreeCompare = require('../git/treeCompare').TreeCompare;
|
||
|
||
var Level = Sandbox.extend({
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
options.level = options.level || {};
|
||
|
||
this.gitCommandsIssued = 0;
|
||
this.solved = false;
|
||
// possible options on how stringent to be go here
|
||
this.treeCompare = new TreeCompare();
|
||
|
||
this.goalTreeString = options.level.goalTree;
|
||
if (!this.goalTreeString) {
|
||
console.warn('woah no goal, using random other one');
|
||
this.goalTreeString = '{"branches":{"master":{"target":"C2","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C30"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"}}';
|
||
//this.goalTreeString = '{"branches":{"master":{"target":"C2","id":"master"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"master","id":"HEAD"}}';
|
||
}
|
||
|
||
Sandbox.prototype.initialize.apply(this, [options]);
|
||
},
|
||
|
||
initVisualization: function(options) {
|
||
if (!options.level.startTree) {
|
||
console.warn('No start tree specified for this level!!! using default...');
|
||
}
|
||
this.mainVis = new Visualization({
|
||
el: options.el || this.getDefaultVisEl(),
|
||
treeString: options.level.startTree
|
||
});
|
||
},
|
||
|
||
initParseWaterfall: function(options) {
|
||
this.parseWaterfall = new ParseWaterfall();
|
||
|
||
// if we want to disable certain commands...
|
||
if (options.level.disabledMap) {
|
||
// disable these other commands
|
||
this.parseWaterfall.addFirst(
|
||
new DisabledMap({
|
||
disabledMap: options.level.disabledMap
|
||
}).getInstantCommands()
|
||
);
|
||
}
|
||
},
|
||
|
||
initGitShim: function(options) {
|
||
// ok we definitely want a shim here
|
||
this.gitShim = new GitShim({
|
||
afterCB: _.bind(this.afterCommandCB, this),
|
||
afterDeferHandler: _.bind(this.afterCommandDefer, this)
|
||
});
|
||
},
|
||
|
||
afterCommandCB: function(command) {
|
||
// TODO check if error, but not warning
|
||
this.gitCommandsIssued++;
|
||
},
|
||
|
||
afterCommandDefer: function(defer) {
|
||
if (this.solved) { return; }
|
||
// ok so lets see if they solved it...
|
||
var current = this.mainVis.gitEngine.exportTree();
|
||
var solved = this.treeCompare.compareTrees(current, this.goalTreeString);
|
||
|
||
if (!solved) {
|
||
defer.resolve();
|
||
return;
|
||
}
|
||
|
||
// woohoo!!! they solved the level, lets animate and such
|
||
this.levelSolved(defer);
|
||
},
|
||
|
||
levelSolved: function(defer) {
|
||
this.mainVis.gitVisuals.finishAnimation()
|
||
.then(function() {
|
||
defer.resolve();
|
||
});
|
||
}
|
||
});
|
||
|
||
exports.Level = Level;
|
||
|
||
|
||
});
|
||
require("/src/js/level/index.js");
|
||
|
||
require.define("/src/js/level/parseWaterfall.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var GitCommands = require('../git/commands');
|
||
var SandboxCommands = require('../level/SandboxCommands');
|
||
|
||
// more or less a static class
|
||
function ParseWaterfall(options) {
|
||
options = options || {};
|
||
this.shortcutWaterfall = options.shortcutWaterfall || [
|
||
GitCommands.shortcutMap
|
||
];
|
||
|
||
this.instantWaterfall = options.instantWaterfall || [
|
||
GitCommands.instantCommands,
|
||
SandboxCommands.instantCommands
|
||
];
|
||
|
||
this.parseWaterfall = options.parseWaterfall || [
|
||
GitCommands.parse,
|
||
SandboxCommands.parse
|
||
];
|
||
}
|
||
|
||
ParseWaterfall.prototype.clone = function() {
|
||
return new ParseWaterfall({
|
||
shortcutWaterfall: this.shortcutWaterfall.slice(),
|
||
instantWaterfall: this.instantWaterfall.slice(),
|
||
parseWaterfall: this.parseWaterfall.slice()
|
||
});
|
||
};
|
||
|
||
ParseWaterfall.prototype.getWaterfallMap = function() {
|
||
return {
|
||
shortcutWaterfall: this.shortcutWaterfall,
|
||
instantWaterfall: this.instantWaterfall,
|
||
parseWaterfall: this.parseWaterfall
|
||
};
|
||
};
|
||
|
||
ParseWaterfall.prototype.addFirst = function(which, value) {
|
||
this.getWaterfallMap()[which].unshift(value);
|
||
};
|
||
|
||
ParseWaterfall.prototype.addLast = function(which, value) {
|
||
this.getWaterfallMap()[which].push(value);
|
||
};
|
||
|
||
ParseWaterfall.prototype.expandAllShortcuts = function(commandStr) {
|
||
_.each(this.shortcutWaterfall, function(shortcutMap) {
|
||
commandStr = this.expandShortcut(commandStr, shortcutMap);
|
||
}, this);
|
||
return commandStr;
|
||
};
|
||
|
||
ParseWaterfall.prototype.expandShortcut = function(commandStr, shortcutMap) {
|
||
_.each(shortcutMap, function(regex, method) {
|
||
var results = regex.exec(commandStr);
|
||
if (results) {
|
||
commandStr = method + ' ' + commandStr.slice(results[0].length);
|
||
}
|
||
});
|
||
return commandStr;
|
||
};
|
||
|
||
ParseWaterfall.prototype.processAllInstants = function(commandStr) {
|
||
_.each(this.instantWaterfall, function(instantCommands) {
|
||
this.processInstant(commandStr, instantCommands);
|
||
}, this);
|
||
};
|
||
|
||
ParseWaterfall.prototype.processInstant = function(commandStr, instantCommands) {
|
||
_.each(instantCommands, function(tuple) {
|
||
var regex = tuple[0];
|
||
var results = regex.exec(commandStr);
|
||
if (results) {
|
||
// this will throw a result because it's an instant
|
||
tuple[1](results);
|
||
}
|
||
});
|
||
};
|
||
|
||
ParseWaterfall.prototype.parseAll = function(commandStr) {
|
||
var toReturn = false;
|
||
_.each(this.parseWaterfall, function(parseFunc) {
|
||
var results = parseFunc(commandStr);
|
||
if (results) {
|
||
toReturn = results;
|
||
}
|
||
}, this);
|
||
|
||
return toReturn;
|
||
};
|
||
|
||
exports.ParseWaterfall = ParseWaterfall;
|
||
|
||
|
||
});
|
||
require("/src/js/level/parseWaterfall.js");
|
||
|
||
require.define("/src/js/level/sandbox.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;
|
||
|
||
var util = require('../util');
|
||
var Main = require('../app');
|
||
|
||
var Visualization = require('../visuals/visualization').Visualization;
|
||
var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
|
||
var DisabledMap = require('../level/disabledMap').DisabledMap;
|
||
var Command = require('../models/commandModel').Command;
|
||
var GitShim = require('../git/gitShim').GitShim;
|
||
|
||
var ModalTerminal = require('../views').ModalTerminal;
|
||
var ModalAlert = require('../views').ModalAlert;
|
||
|
||
var MultiView = require('../views/multiView').MultiView;
|
||
|
||
var Sandbox = Backbone.View.extend({
|
||
// tag name here is purely vestigial. I made this a view
|
||
// simply to use inheritance and have a nice event system in place
|
||
tagName: 'div',
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
|
||
this.initVisualization(options);
|
||
this.initCommandCollection(options);
|
||
this.initParseWaterfall(options);
|
||
this.initGitShim(options);
|
||
|
||
if (!options.wait) {
|
||
this.takeControl();
|
||
}
|
||
},
|
||
|
||
getDefaultVisEl: function() {
|
||
return $('#canvasWrapper')[0];
|
||
},
|
||
|
||
initVisualization: function(options) {
|
||
this.mainVis = new Visualization({
|
||
el: options.el || this.getDefaultVisEl()
|
||
});
|
||
},
|
||
|
||
initCommandCollection: function(options) {
|
||
// don't add it to just any collection -- adding to the
|
||
// CommandUI collection will put in history
|
||
this.commandCollection = Main.getCommandUI().commandCollection;
|
||
},
|
||
|
||
initParseWaterfall: function(options) {
|
||
this.parseWaterfall = new ParseWaterfall();
|
||
},
|
||
|
||
initGitShim: function(options) {
|
||
/*
|
||
this.gitShim = new GitShim({
|
||
beforeCB: function() { console.log('before'); },
|
||
afterCB: function() { console.log('after'); },
|
||
afterDeferHandler: function(deferred) { deferred.resolve(); },
|
||
});*/
|
||
},
|
||
|
||
takeControl: function() {
|
||
// we will be handling commands that are submitted, mainly to add the sanadbox
|
||
// functionality (which is included by default in ParseWaterfall())
|
||
Main.getEventBaton().stealBaton('commandSubmitted', this.commandSubmitted, this);
|
||
// we obviously take care of sandbox commands
|
||
Main.getEventBaton().stealBaton('processSandboxCommand', this.processSandboxCommand, this);
|
||
|
||
this.insertGitShim();
|
||
},
|
||
|
||
insertGitShim: function() {
|
||
// and our git shim goes in after the git engine is ready so it doesn't steal the baton
|
||
// too early
|
||
if (this.gitShim) {
|
||
this.mainVis.customEvents.on('gitEngineReady', function() {
|
||
this.gitShim.insertShim();
|
||
},this);
|
||
}
|
||
},
|
||
|
||
commandSubmitted: function(value) {
|
||
// allow other things to see this command (aka command history on terminal)
|
||
Main.getEvents().trigger('commandSubmittedPassive', value);
|
||
|
||
util.splitTextCommand(value, function(command) {
|
||
this.commandCollection.add(new Command({
|
||
rawStr: command,
|
||
parseWaterfall: this.parseWaterfall
|
||
}));
|
||
}, this);
|
||
},
|
||
|
||
processSandboxCommand: function(command, deferred) {
|
||
var commandMap = {
|
||
help: this.helpDialog
|
||
};
|
||
var method = commandMap[command.get('method')];
|
||
if (!method) { throw new Error('no method for that wut'); }
|
||
|
||
method.apply(this, [command, deferred]);
|
||
},
|
||
|
||
helpDialog: function(command, deferred) {
|
||
var helpDialog = new MultiView({
|
||
childViews: require('../dialogs/sandbox').helpDialog
|
||
});
|
||
helpDialog.getPromise().then(_.bind(function() {
|
||
// the view has been closed, lets go ahead and resolve our command
|
||
command.finishWith(deferred);
|
||
}, this))
|
||
.done();
|
||
}
|
||
});
|
||
|
||
exports.Sandbox = Sandbox;
|
||
|
||
|
||
});
|
||
require("/src/js/level/sandbox.js");
|
||
|
||
require.define("/src/js/level/sandboxCommands.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var GitCommands = require('../git/commands');
|
||
var GitOptionParser = GitCommands.GitOptionParser;
|
||
|
||
var Errors = require('../util/errors');
|
||
var CommandProcessError = Errors.CommandProcessError;
|
||
var GitError = Errors.GitError;
|
||
var Warning = Errors.Warning;
|
||
var CommandResult = Errors.CommandResult;
|
||
|
||
var instantCommands = [
|
||
[/^ls/, function() {
|
||
throw new CommandResult({
|
||
msg: "DontWorryAboutFilesInThisDemo.txt"
|
||
});
|
||
}],
|
||
[/^cd/, function() {
|
||
throw new CommandResult({
|
||
msg: "Directory Changed to '/directories/dont/matter/in/this/demo'"
|
||
});
|
||
}],
|
||
[/^refresh$/, function() {
|
||
var events = require('../app').getEvents();
|
||
|
||
events.trigger('refreshTree');
|
||
throw new CommandResult({
|
||
msg: "Refreshing tree..."
|
||
});
|
||
}],
|
||
[/^rollup (\d+)$/, function(bits) {
|
||
var events = require('../app').getEvents();
|
||
|
||
// go roll up these commands by joining them with semicolons
|
||
events.trigger('rollupCommands', bits[1]);
|
||
throw new CommandResult({
|
||
msg: 'Commands combined!'
|
||
});
|
||
}]
|
||
];
|
||
|
||
var regexMap = {
|
||
'help': /^help($|\s)|\?/
|
||
};
|
||
|
||
var parse = function(str) {
|
||
var sandboxMethod;
|
||
|
||
_.each(regexMap, function(regex, method) {
|
||
if (regex.test(str)) {
|
||
sandboxMethod = method;
|
||
}
|
||
});
|
||
|
||
return (!sandboxMethod) ? false : {
|
||
toSet: {
|
||
eventName: 'processSandboxCommand',
|
||
method: sandboxMethod
|
||
}
|
||
};
|
||
};
|
||
|
||
exports.instantCommands = instantCommands;
|
||
exports.parse = parse;
|
||
|
||
|
||
});
|
||
require("/src/js/level/sandboxCommands.js");
|
||
|
||
require.define("/src/js/models/collections.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Q = require('q');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
|
||
var Commit = require('../git').Commit;
|
||
var Branch = require('../git').Branch;
|
||
|
||
var Command = require('../models/commandModel').Command;
|
||
var CommandEntry = require('../models/commandModel').CommandEntry;
|
||
var TIME = require('../util/constants').TIME;
|
||
|
||
var CommitCollection = Backbone.Collection.extend({
|
||
model: Commit
|
||
});
|
||
|
||
var CommandCollection = Backbone.Collection.extend({
|
||
model: Command
|
||
});
|
||
|
||
var BranchCollection = Backbone.Collection.extend({
|
||
model: Branch
|
||
});
|
||
|
||
var CommandEntryCollection = Backbone.Collection.extend({
|
||
model: CommandEntry,
|
||
localStorage: (Backbone.LocalStorage) ? new Backbone.LocalStorage('CommandEntries') : null
|
||
});
|
||
|
||
var CommandBuffer = Backbone.Model.extend({
|
||
defaults: {
|
||
collection: null
|
||
},
|
||
|
||
initialize: function(options) {
|
||
options.collection.bind('add', this.addCommand, this);
|
||
|
||
this.buffer = [];
|
||
this.timeout = null;
|
||
},
|
||
|
||
addCommand: function(command) {
|
||
this.buffer.push(command);
|
||
this.touchBuffer();
|
||
},
|
||
|
||
touchBuffer: function() {
|
||
// touch buffer just essentially means we just check if our buffer is being
|
||
// processed. if it's not, we immediately process the first item
|
||
// and then set the timeout.
|
||
if (this.timeout) {
|
||
// timeout existence implies its being processed
|
||
return;
|
||
}
|
||
this.setTimeout();
|
||
},
|
||
|
||
|
||
setTimeout: function() {
|
||
this.timeout = setTimeout(_.bind(function() {
|
||
this.sipFromBuffer();
|
||
}, this), TIME.betweenCommandsDelay);
|
||
},
|
||
|
||
popAndProcess: function() {
|
||
var popped = this.buffer.shift(0);
|
||
|
||
// find a command with no error (aka unprocessed)
|
||
while (popped.get('error') && this.buffer.length) {
|
||
popped = this.buffer.pop();
|
||
}
|
||
if (!popped.get('error')) {
|
||
this.processCommand(popped);
|
||
} else {
|
||
// no more commands to process
|
||
this.clear();
|
||
}
|
||
},
|
||
|
||
processCommand: function(command) {
|
||
command.set('status', 'processing');
|
||
|
||
var deferred = Q.defer();
|
||
deferred.promise.then(_.bind(function() {
|
||
this.setTimeout();
|
||
}, this));
|
||
|
||
var eventName = command.get('eventName');
|
||
if (!eventName) {
|
||
throw new Error('I need an event to trigger when this guy is parsed and ready');
|
||
}
|
||
|
||
var Main = require('../app');
|
||
Main.getEventBaton().trigger(eventName, command, deferred);
|
||
},
|
||
|
||
clear: function() {
|
||
clearTimeout(this.timeout);
|
||
this.timeout = null;
|
||
},
|
||
|
||
sipFromBuffer: function() {
|
||
if (!this.buffer.length) {
|
||
this.clear();
|
||
return;
|
||
}
|
||
|
||
this.popAndProcess();
|
||
}
|
||
});
|
||
|
||
exports.CommitCollection = CommitCollection;
|
||
exports.CommandCollection = CommandCollection;
|
||
exports.BranchCollection = BranchCollection;
|
||
exports.CommandEntryCollection = CommandEntryCollection;
|
||
exports.CommandBuffer = CommandBuffer;
|
||
|
||
|
||
});
|
||
require("/src/js/models/collections.js");
|
||
|
||
require.define("/src/js/models/commandModel.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
|
||
var Errors = require('../util/errors');
|
||
var GitCommands = require('../git/commands');
|
||
var GitOptionParser = GitCommands.GitOptionParser;
|
||
|
||
var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
|
||
|
||
var CommandProcessError = Errors.CommandProcessError;
|
||
var GitError = Errors.GitError;
|
||
var Warning = Errors.Warning;
|
||
var CommandResult = Errors.CommandResult;
|
||
|
||
var Command = Backbone.Model.extend({
|
||
defaults: {
|
||
status: 'inqueue',
|
||
rawStr: null,
|
||
result: '',
|
||
createTime: null,
|
||
|
||
error: null,
|
||
warnings: null,
|
||
parseWaterfall: new ParseWaterfall(),
|
||
|
||
generalArgs: null,
|
||
supportedMap: null,
|
||
options: null,
|
||
method: null
|
||
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.initDefaults();
|
||
this.validateAtInit();
|
||
|
||
this.on('change:error', this.errorChanged, this);
|
||
// catch errors on init
|
||
if (this.get('error')) {
|
||
this.errorChanged();
|
||
}
|
||
|
||
this.parseOrCatch();
|
||
},
|
||
|
||
initDefaults: function() {
|
||
// weird things happen with defaults if you dont
|
||
// make new objects
|
||
this.set('generalArgs', []);
|
||
this.set('supportedMap', {});
|
||
this.set('warnings', []);
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (this.get('rawStr') === null) {
|
||
throw new Error('Give me a string!');
|
||
}
|
||
if (!this.get('createTime')) {
|
||
this.set('createTime', new Date().toString());
|
||
}
|
||
},
|
||
|
||
setResult: function(msg) {
|
||
this.set('result', msg);
|
||
},
|
||
|
||
finishWith: function(deferred) {
|
||
this.set('status', 'finished');
|
||
deferred.resolve();
|
||
},
|
||
|
||
addWarning: function(msg) {
|
||
this.get('warnings').push(msg);
|
||
// change numWarnings so the change event fires. This is bizarre -- Backbone can't
|
||
// detect if an array changes, so adding an element does nothing
|
||
this.set('numWarnings', this.get('numWarnings') ? this.get('numWarnings') + 1 : 1);
|
||
},
|
||
|
||
getFormattedWarnings: function() {
|
||
if (!this.get('warnings').length) {
|
||
return '';
|
||
}
|
||
var i = '<i class="icon-exclamation-sign"></i>';
|
||
return '<p>' + i + this.get('warnings').join('</p><p>' + i) + '</p>';
|
||
},
|
||
|
||
parseOrCatch: function() {
|
||
this.expandShortcuts(this.get('rawStr'));
|
||
try {
|
||
this.processInstants();
|
||
} catch (err) {
|
||
Errors.filterError(err);
|
||
// errorChanged() will handle status and all of that
|
||
this.set('error', err);
|
||
return;
|
||
}
|
||
|
||
if (this.parseAll()) {
|
||
// something in our parse waterfall succeeded
|
||
return;
|
||
}
|
||
|
||
// if we reach here, this command is not supported :-/
|
||
this.set('error', new CommandProcessError({
|
||
msg: 'The command "' + this.get('rawStr') + '" isn\'t supported, sorry!'
|
||
})
|
||
);
|
||
},
|
||
|
||
errorChanged: function() {
|
||
var err = this.get('error');
|
||
if (err instanceof CommandProcessError ||
|
||
err instanceof GitError) {
|
||
this.set('status', 'error');
|
||
} else if (err instanceof CommandResult) {
|
||
this.set('status', 'finished');
|
||
} else if (err instanceof Warning) {
|
||
this.set('status', 'warning');
|
||
}
|
||
this.formatError();
|
||
},
|
||
|
||
formatError: function() {
|
||
this.set('result', this.get('error').toResult());
|
||
},
|
||
|
||
expandShortcuts: function(str) {
|
||
str = this.get('parseWaterfall').expandAllShortcuts(str);
|
||
this.set('rawStr', str);
|
||
},
|
||
|
||
processInstants: 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 instant commands that will throw
|
||
this.get('parseWaterfall').processAllInstants(str);
|
||
},
|
||
|
||
parseAll: function() {
|
||
var str = this.get('rawStr');
|
||
var results = this.get('parseWaterfall').parseAll(str);
|
||
|
||
if (!results) {
|
||
// nothing parsed successfully
|
||
return false;
|
||
}
|
||
|
||
_.each(results.toSet, function(obj, key) {
|
||
// data comes back from the parsing functions like
|
||
// options (etc) that need to be set
|
||
this.set(key, obj);
|
||
}, this);
|
||
return true;
|
||
}
|
||
});
|
||
|
||
// command entry is for the commandview
|
||
var CommandEntry = Backbone.Model.extend({
|
||
defaults: {
|
||
text: ''
|
||
},
|
||
// stub out if no plugin available
|
||
localStorage: (Backbone.LocalStorage) ? new Backbone.LocalStorage('CommandEntries') : null
|
||
});
|
||
|
||
exports.CommandEntry = CommandEntry;
|
||
exports.Command = Command;
|
||
|
||
});
|
||
require("/src/js/models/commandModel.js");
|
||
|
||
require.define("/src/js/util/constants.js",function(require,module,exports,__dirname,__filename,process,global){/**
|
||
* Constants....!!!
|
||
*/
|
||
var TIME = {
|
||
betweenCommandsDelay: 400
|
||
};
|
||
|
||
// useful for locks, etc
|
||
var GLOBAL = {
|
||
isAnimating: false
|
||
};
|
||
|
||
var VIEWPORT = {
|
||
minZoom: 0.66,
|
||
maxZoom: 1.25
|
||
};
|
||
|
||
var GRAPHICS = {
|
||
arrowHeadSize: 8,
|
||
|
||
nodeRadius: 17,
|
||
curveControlPointOffset: 50,
|
||
defaultEasing: 'easeInOut',
|
||
defaultAnimationTime: 400,
|
||
|
||
//rectFill: '#FF3A3A',
|
||
rectFill: 'hsb(0.8816909813322127,0.7,1)',
|
||
headRectFill: '#2831FF',
|
||
rectStroke: '#FFF',
|
||
rectStrokeWidth: '3',
|
||
|
||
multiBranchY: 20,
|
||
upstreamHeadOpacity: 0.5,
|
||
upstreamNoneOpacity: 0.2,
|
||
edgeUpstreamHeadOpacity: 0.4,
|
||
edgeUpstreamNoneOpacity: 0.15,
|
||
|
||
visBranchStrokeWidth: 2,
|
||
visBranchStrokeColorNone: '#333',
|
||
|
||
defaultNodeFill: 'hsba(0.5,0.8,0.7,1)',
|
||
defaultNodeStrokeWidth: 2,
|
||
defaultNodeStroke: '#FFF',
|
||
|
||
orphanNodeFill: 'hsb(0.5,0.8,0.7)'
|
||
};
|
||
|
||
exports.GLOBAL = GLOBAL;
|
||
exports.TIME = TIME;
|
||
exports.GRAPHICS = GRAPHICS;
|
||
exports.VIEWPORT = VIEWPORT;
|
||
|
||
|
||
});
|
||
require("/src/js/util/constants.js");
|
||
|
||
require.define("/src/js/util/debug.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var toGlobalize = {
|
||
Tree: require('../visuals/tree'),
|
||
Visuals: require('../visuals'),
|
||
Git: require('../git'),
|
||
CommandModel: require('../models/commandModel'),
|
||
Levels: require('../git/treeCompare'),
|
||
Constants: require('../util/constants'),
|
||
Collections: require('../models/collections'),
|
||
Async: require('../visuals/animation'),
|
||
AnimationFactory: require('../visuals/animation/animationFactory'),
|
||
Main: require('../app'),
|
||
HeadLess: require('../git/headless'),
|
||
Q: { Q: require('q') },
|
||
RebaseView: require('../views/rebaseView'),
|
||
Views: require('../views'),
|
||
MultiView: require('../views/multiView'),
|
||
ZoomLevel: require('../util/zoomLevel')
|
||
};
|
||
|
||
_.each(toGlobalize, function(module) {
|
||
_.extend(window, module);
|
||
});
|
||
|
||
$(document).ready(function() {
|
||
window.events = toGlobalize.Main.getEvents();
|
||
window.eventBaton = toGlobalize.Main.getEventBaton();
|
||
window.sandbox = toGlobalize.Main.getSandbox();
|
||
});
|
||
|
||
|
||
});
|
||
require("/src/js/util/debug.js");
|
||
|
||
require.define("/src/js/util/errors.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var MyError = Backbone.Model.extend({
|
||
defaults: {
|
||
type: 'MyError',
|
||
msg: 'Unknown Error'
|
||
},
|
||
toString: function() {
|
||
return this.get('type') + ': ' + this.get('msg');
|
||
},
|
||
|
||
getMsg: function() {
|
||
return this.get('msg') || 'Unknown Error';
|
||
},
|
||
|
||
toResult: function() {
|
||
if (!this.get('msg').length) {
|
||
return '';
|
||
}
|
||
return '<p>' + this.get('msg').replace(/\n/g, '</p><p>') + '</p>';
|
||
}
|
||
});
|
||
|
||
var CommandProcessError = exports.CommandProcessError = MyError.extend({
|
||
defaults: {
|
||
type: 'Command Process Error'
|
||
}
|
||
});
|
||
|
||
var CommandResult = exports.CommandResult = MyError.extend({
|
||
defaults: {
|
||
type: 'Command Result'
|
||
}
|
||
});
|
||
|
||
var Warning = exports.Warning = MyError.extend({
|
||
defaults: {
|
||
type: 'Warning'
|
||
}
|
||
});
|
||
|
||
var GitError = exports.GitError = MyError.extend({
|
||
defaults: {
|
||
type: 'Git Error'
|
||
}
|
||
});
|
||
|
||
var filterError = function(err) {
|
||
if (err instanceof CommandProcessError ||
|
||
err instanceof GitError ||
|
||
err instanceof CommandResult ||
|
||
err instanceof Warning) {
|
||
// yay! one of ours
|
||
return;
|
||
} else {
|
||
throw err;
|
||
}
|
||
};
|
||
|
||
exports.filterError = filterError;
|
||
|
||
});
|
||
require("/src/js/util/errors.js");
|
||
|
||
require.define("/src/js/util/eventBaton.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
function EventBaton() {
|
||
this.eventMap = {};
|
||
}
|
||
|
||
// this method steals the "baton" -- aka, only this method will now
|
||
// get called. analogous to events.on
|
||
// EventBaton.prototype.on = function(name, func, context) {
|
||
EventBaton.prototype.stealBaton = function(name, func, context) {
|
||
if (!name) { throw new Error('need name'); }
|
||
if (!func) { throw new Error('need func!'); }
|
||
|
||
var listeners = this.eventMap[name] || [];
|
||
listeners.push({
|
||
func: func,
|
||
context: context
|
||
});
|
||
this.eventMap[name] = listeners;
|
||
};
|
||
|
||
EventBaton.prototype.sliceOffArgs = function(num, args) {
|
||
var newArgs = [];
|
||
for (var i = num; i < args.length; i++) {
|
||
newArgs.push(args[i]);
|
||
}
|
||
return newArgs;
|
||
};
|
||
|
||
EventBaton.prototype.trigger = function(name) {
|
||
// arguments is weird and doesnt do slice right
|
||
var argsToApply = this.sliceOffArgs(1, arguments);
|
||
|
||
var listeners = this.eventMap[name];
|
||
if (!listeners || !listeners.length) {
|
||
console.warn('no listeners for', name);
|
||
return;
|
||
}
|
||
|
||
// call the top most listener with context and such
|
||
var toCall = listeners.slice(-1)[0];
|
||
toCall.func.apply(toCall.context, argsToApply);
|
||
};
|
||
|
||
EventBaton.prototype.getListenersThrow = function(name) {
|
||
var listeners = this.eventMap[name];
|
||
if (!listeners || !listeners.length) {
|
||
throw new Error('no one has that baton!' + name);
|
||
}
|
||
return listeners;
|
||
};
|
||
|
||
EventBaton.prototype.passBatonBack = function(name, func, context, args) {
|
||
// this method will call the listener BEFORE the name/func pair. this
|
||
// basically allows you to put in shims, where you steal batons but pass
|
||
// them back if they don't meet certain conditions
|
||
var listeners = this.getListenersThrow(name);
|
||
|
||
var indexBefore;
|
||
_.each(listeners, function(listenerObj, index) {
|
||
// skip the first
|
||
if (index === 0) { return; }
|
||
if (listenerObj.func === func && listenerObj.context === context) {
|
||
indexBefore = index - 1;
|
||
}
|
||
}, this);
|
||
if (indexBefore === undefined) {
|
||
throw new Error('you are the last baton holder! or i didnt find you');
|
||
}
|
||
var toCallObj = listeners[indexBefore];
|
||
|
||
toCallObj.func.apply(toCallObj.context, args);
|
||
};
|
||
|
||
EventBaton.prototype.releaseBaton = function(name, func, context) {
|
||
// might be in the middle of the stack, so we have to loop instead of
|
||
// just popping blindly
|
||
var listeners = this.getListenersThrow(name);
|
||
|
||
var newListeners = [];
|
||
var found = false;
|
||
_.each(listeners, function(listenerObj) {
|
||
if (listenerObj.func === func && listenerObj.context === context) {
|
||
if (found) { console.warn('woah duplicates!!!'); }
|
||
found = true;
|
||
} else {
|
||
newListeners.push(listenerObj);
|
||
}
|
||
}, this);
|
||
|
||
if (!found) {
|
||
console.log('did not find that function', func, context, name, arguments);
|
||
throw new Error('cant releasebaton if yu dont have it');
|
||
}
|
||
this.eventMap[name] = newListeners;
|
||
};
|
||
|
||
exports.EventBaton = EventBaton;
|
||
|
||
|
||
});
|
||
require("/src/js/util/eventBaton.js");
|
||
|
||
require.define("/src/js/util/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
exports.isBrowser = function() {
|
||
var inBrowser = String(typeof window) !== 'undefined';
|
||
return inBrowser;
|
||
};
|
||
|
||
exports.splitTextCommand = function(value, func, context) {
|
||
func = _.bind(func, context);
|
||
_.each(value.split(';'), function(command, index) {
|
||
command = _.escape(command);
|
||
command = command
|
||
.replace(/^(\s+)/, '')
|
||
.replace(/(\s+)$/, '')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, "'");
|
||
|
||
if (index > 0 && !command.length) {
|
||
return;
|
||
}
|
||
func(command);
|
||
});
|
||
};
|
||
|
||
});
|
||
require("/src/js/util/index.js");
|
||
|
||
require.define("/src/js/util/keyboard.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var mapKeycodeToKey = function(keycode) {
|
||
// HELP WANTED -- internationalize? Dvorak? I have no idea
|
||
var keyMap = {
|
||
37: 'left',
|
||
38: 'up',
|
||
39: 'right',
|
||
40: 'down',
|
||
27: 'esc',
|
||
13: 'enter'
|
||
};
|
||
return keyMap[keycode];
|
||
};
|
||
|
||
function KeyboardListener(options) {
|
||
this.events = options.events || _.clone(Backbone.Events);
|
||
this.aliasMap = options.aliasMap || {};
|
||
|
||
this.keydownListener = _.bind(this.keydown, this);
|
||
this.listen();
|
||
}
|
||
|
||
KeyboardListener.prototype.listen = function() {
|
||
$(document).bind('keydown', this.keydownListener);
|
||
};
|
||
|
||
KeyboardListener.prototype.mute = function() {
|
||
$(document).unbind('keydown', this.keydownListener);
|
||
};
|
||
|
||
KeyboardListener.prototype.keydown = function(e) {
|
||
var which = e.which;
|
||
|
||
var key = mapKeycodeToKey(which);
|
||
if (key === undefined) {
|
||
return;
|
||
}
|
||
|
||
this.fireEvent(key);
|
||
};
|
||
|
||
KeyboardListener.prototype.fireEvent = function(eventName) {
|
||
eventName = this.aliasMap[eventName] || eventName;
|
||
this.events.trigger(eventName);
|
||
};
|
||
|
||
exports.KeyboardListener = KeyboardListener;
|
||
exports.mapKeycodeToKey = mapKeycodeToKey;
|
||
|
||
|
||
});
|
||
require("/src/js/util/keyboard.js");
|
||
|
||
require.define("/src/js/util/mock.js",function(require,module,exports,__dirname,__filename,process,global){exports.mock = function(Constructor) {
|
||
var dummy = {};
|
||
var stub = function() {};
|
||
|
||
for (var key in Constructor.prototype) {
|
||
dummy[key] = stub;
|
||
}
|
||
return dummy;
|
||
};
|
||
|
||
|
||
});
|
||
require("/src/js/util/mock.js");
|
||
|
||
require.define("/src/js/util/zoomLevel.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
|
||
var warnOnce = true;
|
||
|
||
function detectZoom() {
|
||
/**
|
||
* Note: this method has only been tested on Chrome
|
||
* but seems to work. A much more elaborate library is available here:
|
||
* https://github.com/yonran/detect-zoom
|
||
* but seems to return a "2" zoom level for my computer (who knows)
|
||
* so I can't use it. The ecosystem for zoom level detection is a mess
|
||
*/
|
||
if (!window.outerWidth || !window.innerWidth) {
|
||
if (warnOnce) {
|
||
console.warn("Can't detect zoom level correctly :-/");
|
||
warnOnce = false;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
return window.outerWidth / window.innerWidth;
|
||
}
|
||
|
||
var setupZoomPoll = function(callback, context) {
|
||
var currentZoom = 0;
|
||
|
||
setInterval(function() {
|
||
var newZoom = detectZoom();
|
||
if (newZoom !== currentZoom) {
|
||
currentZoom = newZoom;
|
||
callback.apply(context, [newZoom]);
|
||
}
|
||
}, 500);
|
||
};
|
||
|
||
exports.setupZoomPoll = setupZoomPoll;
|
||
exports.detectZoom = detectZoom;
|
||
|
||
|
||
});
|
||
require("/src/js/util/zoomLevel.js");
|
||
|
||
require.define("/src/js/views/commandViews.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
|
||
var CommandEntryCollection = require('../models/collections').CommandEntryCollection;
|
||
var Main = require('../app');
|
||
var Command = require('../models/commandModel').Command;
|
||
var CommandEntry = require('../models/commandModel').CommandEntry;
|
||
|
||
var Errors = require('../util/errors');
|
||
var Warning = Errors.Warning;
|
||
|
||
var util = require('../util');
|
||
var keyboard = require('../util/keyboard');
|
||
|
||
var CommandPromptView = Backbone.View.extend({
|
||
initialize: function(options) {
|
||
Main.getEvents().on('commandSubmittedPassive', this.addToCommandHistory, this);
|
||
|
||
// uses local storage
|
||
this.commands = new CommandEntryCollection();
|
||
this.commands.fetch({
|
||
success: _.bind(function() {
|
||
// reverse the commands. this is ugly but needs to be done...
|
||
var commands = [];
|
||
this.commands.each(function(c) {
|
||
commands.push(c);
|
||
});
|
||
|
||
commands.reverse();
|
||
this.commands.reset();
|
||
|
||
_.each(commands, function(c) {
|
||
this.commands.add(c);
|
||
}, this);
|
||
}, this)
|
||
});
|
||
|
||
this.index = -1;
|
||
this.commandSpan = this.$('#prompt span.command')[0];
|
||
this.commandCursor = this.$('#prompt span.cursor')[0];
|
||
this.focus();
|
||
|
||
Main.getEvents().on('rollupCommands', this.rollupCommands, this);
|
||
|
||
Main.getEventBaton().stealBaton('keydown', this.onKeyDown, this);
|
||
Main.getEventBaton().stealBaton('keyup', this.onKeyUp, this);
|
||
},
|
||
|
||
events: {
|
||
'blur #commandTextField': 'hideCursor',
|
||
'focus #commandTextField': 'showCursor'
|
||
},
|
||
|
||
blur: function() {
|
||
this.hideCursor();
|
||
},
|
||
|
||
focus: function() {
|
||
this.$('#commandTextField').focus();
|
||
this.showCursor();
|
||
},
|
||
|
||
hideCursor: function() {
|
||
this.toggleCursor(false);
|
||
},
|
||
|
||
showCursor: function() {
|
||
this.toggleCursor(true);
|
||
},
|
||
|
||
toggleCursor: function(state) {
|
||
$(this.commandCursor).toggleClass('shown', state);
|
||
},
|
||
|
||
onKeyDown: function(e) {
|
||
var el = e.srcElement;
|
||
this.updatePrompt(el);
|
||
},
|
||
|
||
onKeyUp: function(e) {
|
||
this.onKeyDown(e);
|
||
|
||
// we need to capture some of these events.
|
||
var keyToFuncMap = {
|
||
enter: _.bind(function() {
|
||
this.submit();
|
||
}, this),
|
||
up: _.bind(function() {
|
||
this.commandSelectChange(1);
|
||
}, this),
|
||
down: _.bind(function() {
|
||
this.commandSelectChange(-1);
|
||
}, this)
|
||
};
|
||
|
||
var key = keyboard.mapKeycodeToKey(e.which);
|
||
if (keyToFuncMap[key] !== undefined) {
|
||
e.preventDefault();
|
||
keyToFuncMap[key]();
|
||
this.onKeyDown(e);
|
||
}
|
||
},
|
||
|
||
badHtmlEncode: function(text) {
|
||
return text.replace(/&/g,'&')
|
||
.replace(/</g,'<')
|
||
.replace(/</g,'<')
|
||
.replace(/ /g,' ')
|
||
.replace(/\n/g,'');
|
||
},
|
||
|
||
updatePrompt: function(el) {
|
||
// i WEEEPPPPPPpppppppppppp that this reflow takes so long. it adds this
|
||
// super annoying delay to every keystroke... I have tried everything
|
||
// to make this more performant. getting the srcElement from the event,
|
||
// getting the value directly from the dom, etc etc. yet still,
|
||
// there's a very annoying and sightly noticeable command delay.
|
||
// try.github.com also has this, so I'm assuming those engineers gave up as
|
||
// well...
|
||
var val = this.badHtmlEncode(el.value);
|
||
this.commandSpan.innerHTML = val;
|
||
|
||
// now mutate the cursor...
|
||
this.cursorUpdate(el.value.length, el.selectionStart, el.selectionEnd);
|
||
// and scroll down due to some weird bug
|
||
Main.getEvents().trigger('commandScrollDown');
|
||
},
|
||
|
||
cursorUpdate: function(commandLength, selectionStart, selectionEnd) {
|
||
// 10px for monospaced font at "1" zoom
|
||
var zoom = require('../util/zoomLevel').detectZoom();
|
||
var widthPerChar = 10 * zoom;
|
||
|
||
var numCharsSelected = Math.max(1, selectionEnd - selectionStart);
|
||
var width = String(numCharsSelected * widthPerChar) + 'px';
|
||
|
||
// now for positioning
|
||
var numLeft = Math.max(commandLength - selectionStart, 0);
|
||
var left = String(-numLeft * widthPerChar) + 'px';
|
||
// one reflow? :D
|
||
$(this.commandCursor).css({
|
||
width: width,
|
||
left: left
|
||
});
|
||
},
|
||
|
||
commandSelectChange: function(delta) {
|
||
this.index += delta;
|
||
|
||
// if we are over / under, display blank line. yes this eliminates your
|
||
// partially edited 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
|
||
var commandEntry = this.commands.toArray()[this.index].get('text');
|
||
this.setTextField(commandEntry);
|
||
},
|
||
|
||
clearLocalStorage: function() {
|
||
this.commands.each(function(c) {
|
||
Backbone.sync('delete', c, function() { });
|
||
}, this);
|
||
localStorage.setItem('CommandEntries', '');
|
||
},
|
||
|
||
setTextField: function(value) {
|
||
this.$('#commandTextField').val(value);
|
||
},
|
||
|
||
clear: function() {
|
||
this.setTextField('');
|
||
},
|
||
|
||
submit: function() {
|
||
var value = this.$('#commandTextField').val().replace('\n', '');
|
||
this.clear();
|
||
|
||
this.submitCommand(value);
|
||
this.index = -1;
|
||
},
|
||
|
||
rollupCommands: function(numBack) {
|
||
var which = this.commands.toArray().slice(1, Number(numBack) + 1);
|
||
which.reverse();
|
||
|
||
var str = '';
|
||
_.each(which, function(commandEntry) {
|
||
str += commandEntry.get('text') + ';';
|
||
}, this);
|
||
|
||
var rolled = new CommandEntry({text: str});
|
||
this.commands.unshift(rolled);
|
||
Backbone.sync('create', rolled, function() { });
|
||
},
|
||
|
||
addToCommandHistory: function(value) {
|
||
// we should add the command to our local storage history
|
||
// if it's not a blank line and this is a new command...
|
||
// or if we edited the command in place in history
|
||
var shouldAdd = (value.length && this.index === -1) ||
|
||
((value.length && this.index !== -1 &&
|
||
this.commands.toArray()[this.index].get('text') !== value));
|
||
|
||
if (this.index !== -1) {
|
||
console.log(this.commands.toArray()[this.index]);
|
||
}
|
||
|
||
if (!shouldAdd) {
|
||
return;
|
||
}
|
||
|
||
var commandEntry = new CommandEntry({text: value});
|
||
this.commands.unshift(commandEntry);
|
||
|
||
// store to local storage
|
||
Backbone.sync('create', commandEntry, function() { });
|
||
|
||
// if our length is too egregious, reset
|
||
if (this.commands.length > 100) {
|
||
this.clearLocalStorage();
|
||
}
|
||
},
|
||
|
||
submitCommand: function(value) {
|
||
Main.getEventBaton().trigger('commandSubmitted', value);
|
||
}
|
||
});
|
||
|
||
// 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) {
|
||
},
|
||
|
||
initialize: function() {
|
||
this.model.bind('change', this.wasChanged, this);
|
||
this.model.bind('destroy', this.remove, this);
|
||
},
|
||
|
||
wasChanged: function(model, changeEvent) {
|
||
// for changes that are just comestic, we actually only want to toggle classes
|
||
// with jquery rather than brutally delete a html. doing so allows us
|
||
// to nicely fade things
|
||
var changes = changeEvent.changes;
|
||
var changeKeys = _.keys(changes);
|
||
if (_.difference(changeKeys, ['status']).length === 0) {
|
||
this.updateStatus();
|
||
} 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: '',
|
||
formattedWarnings: this.model.getFormattedWarnings()
|
||
},
|
||
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);
|
||
Main.getEvents().on('commandScrollDown', this.scrollDown, this);
|
||
},
|
||
|
||
addWarning: function(msg) {
|
||
var err = new Warning({
|
||
msg: msg
|
||
});
|
||
|
||
var command = new Command({
|
||
error: err,
|
||
rawStr: 'Warning:'
|
||
});
|
||
|
||
this.collection.add(command);
|
||
},
|
||
|
||
scrollDown: function() {
|
||
// if commandDisplay is ever bigger than #terminal, we need to
|
||
// add overflow-y to terminal and scroll down
|
||
var cD = $('#commandDisplay')[0];
|
||
var t = $('#terminal')[0];
|
||
|
||
if ($(t).hasClass('scrolling')) {
|
||
t.scrollTop = t.scrollHeight;
|
||
return;
|
||
}
|
||
if (cD.clientHeight > t.clientHeight) {
|
||
$(t).css('overflow-y', 'scroll');
|
||
$(t).css('overflow-x', 'hidden');
|
||
$(t).addClass('scrolling');
|
||
t.scrollTop = t.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);
|
||
}
|
||
});
|
||
|
||
exports.CommandPromptView = CommandPromptView;
|
||
exports.CommandLineHistoryView = CommandLineHistoryView;
|
||
|
||
});
|
||
require("/src/js/views/commandViews.js");
|
||
|
||
require.define("/src/js/views/index.js",function(require,module,exports,__dirname,__filename,process,global){var GitError = require('../util/errors').GitError;
|
||
var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;
|
||
|
||
var Main = require('../app');
|
||
var Constants = require('../util/constants');
|
||
|
||
var BaseView = Backbone.View.extend({
|
||
getDestination: function() {
|
||
return this.destination || this.container.getInsideElement();
|
||
},
|
||
|
||
tearDown: function() {
|
||
this.$el.html('');
|
||
if (this.container) {
|
||
this.container.tearDown();
|
||
}
|
||
},
|
||
|
||
render: function(HTML) {
|
||
// flexibility
|
||
var destination = this.getDestination();
|
||
HTML = HTML || this.template(this.JSON);
|
||
|
||
this.$el.html(HTML);
|
||
$(destination).append(this.el);
|
||
}
|
||
});
|
||
|
||
var ResolveRejectBase = BaseView.extend({
|
||
resolve: function() {
|
||
this.deferred.resolve();
|
||
},
|
||
|
||
reject: function() {
|
||
this.deferred.reject();
|
||
}
|
||
});
|
||
|
||
var PositiveNegativeBase = BaseView.extend({
|
||
positive: function() {
|
||
this.navEvents.trigger('positive');
|
||
},
|
||
|
||
negative: function() {
|
||
this.navEvents.trigger('negative');
|
||
}
|
||
});
|
||
|
||
var ContainedBase = BaseView.extend({
|
||
getAnimationTime: function() { return 700; },
|
||
|
||
show: function() {
|
||
this.container.show();
|
||
},
|
||
|
||
hide: function() {
|
||
this.container.hide();
|
||
},
|
||
|
||
die: function() {
|
||
this.hide();
|
||
setTimeout(_.bind(function() {
|
||
this.tearDown();
|
||
}, this), this.getAnimationTime() * 1.1);
|
||
}
|
||
});
|
||
|
||
var ConfirmCancelView = ResolveRejectBase.extend({
|
||
tagName: 'div',
|
||
className: 'confirmCancelView box horizontal justify',
|
||
template: _.template($('#confirm-cancel-template').html()),
|
||
events: {
|
||
'click .confirmButton': 'resolve',
|
||
'click .cancelButton': 'reject'
|
||
},
|
||
|
||
initialize: function(options) {
|
||
if (!options.destination || !options.deferred) {
|
||
throw new Error('needmore');
|
||
}
|
||
|
||
this.destination = options.destination;
|
||
this.deferred = options.deferred;
|
||
this.JSON = {
|
||
confirm: options.confirm || 'Confirm',
|
||
cancel: options.cancel || 'Cancel'
|
||
};
|
||
|
||
this.render();
|
||
}
|
||
});
|
||
|
||
var LeftRightView = PositiveNegativeBase.extend({
|
||
tagName: 'div',
|
||
className: 'leftRightView box horizontal center',
|
||
template: _.template($('#left-right-template').html()),
|
||
events: {
|
||
'click .left': 'negative',
|
||
'click .right': 'positive'
|
||
},
|
||
|
||
initialize: function(options) {
|
||
if (!options.destination || !options.events) {
|
||
throw new Error('needmore');
|
||
}
|
||
|
||
this.destination = options.destination;
|
||
this.navEvents = options.events;
|
||
this.JSON = {
|
||
showLeft: (options.showLeft === undefined) ? true : options.showLeft,
|
||
lastNav: (options.lastNav === undefined) ? false : options.lastNav
|
||
};
|
||
|
||
this.render();
|
||
}
|
||
});
|
||
|
||
var ModalView = Backbone.View.extend({
|
||
tagName: 'div',
|
||
className: 'modalView box horizontal center transitionOpacityLinear',
|
||
template: _.template($('#modal-view-template').html()),
|
||
|
||
getAnimationTime: function() { return 700; },
|
||
|
||
initialize: function(options) {
|
||
this.shown = false;
|
||
this.render();
|
||
this.stealKeyboard();
|
||
},
|
||
|
||
render: function() {
|
||
// add ourselves to the DOM
|
||
this.$el.html(this.template({}));
|
||
$('body').append(this.el);
|
||
// this doesnt necessarily show us though...
|
||
},
|
||
|
||
stealKeyboard: function() {
|
||
Main.getEventBaton().stealBaton('keydown', this.onKeyDown, this);
|
||
Main.getEventBaton().stealBaton('keyup', this.onKeyUp, this);
|
||
Main.getEventBaton().stealBaton('windowFocus', this.onWindowFocus, this);
|
||
Main.getEventBaton().stealBaton('documentClick', this.onDocumentClick, this);
|
||
|
||
// blur the text input field so keydown events will not be caught by our
|
||
// preventDefaulters, allowing people to still refresh and launch inspector (etc)
|
||
$('#commandTextField').blur();
|
||
},
|
||
|
||
releaseKeyboard: function() {
|
||
Main.getEventBaton().releaseBaton('keydown', this.onKeyDown, this);
|
||
Main.getEventBaton().releaseBaton('keyup', this.onKeyUp, this);
|
||
Main.getEventBaton().releaseBaton('windowFocus', this.onWindowFocus, this);
|
||
Main.getEventBaton().releaseBaton('documentClick', this.onDocumentClick, this);
|
||
|
||
Main.getEventBaton().trigger('windowFocus');
|
||
},
|
||
|
||
onWindowFocus: function(e) {
|
||
console.log('window focus doing nothing', e);
|
||
},
|
||
|
||
onDocumentClick: function(e) {
|
||
console.log('doc click doing nothing', e);
|
||
},
|
||
|
||
onKeyDown: function(e) {
|
||
e.preventDefault();
|
||
},
|
||
|
||
onKeyUp: function(e) {
|
||
e.preventDefault();
|
||
},
|
||
|
||
show: function() {
|
||
this.toggleZ(true);
|
||
// on reflow, change our class to animate. for whatever
|
||
// reason if this is done immediately, chrome might combine
|
||
// the two changes and lose the ability to animate and it looks bad.
|
||
process.nextTick(_.bind(function() {
|
||
this.toggleShow(true);
|
||
}, this));
|
||
},
|
||
|
||
hide: function() {
|
||
this.toggleShow(false);
|
||
setTimeout(_.bind(function() {
|
||
// if we are still hidden...
|
||
if (!this.shown) {
|
||
this.toggleZ(false);
|
||
}
|
||
}, this), this.getAnimationTime());
|
||
},
|
||
|
||
getInsideElement: function() {
|
||
return this.$('.contentHolder');
|
||
},
|
||
|
||
toggleShow: function(value) {
|
||
this.shown = value;
|
||
this.$el.toggleClass('show', value);
|
||
},
|
||
|
||
toggleZ: function(value) {
|
||
this.$el.toggleClass('inFront', value);
|
||
},
|
||
|
||
tearDown: function() {
|
||
this.$el.html('');
|
||
$('body')[0].removeChild(this.el);
|
||
this.releaseKeyboard();
|
||
}
|
||
});
|
||
|
||
var ModalTerminal = ContainedBase.extend({
|
||
tagName: 'div',
|
||
className: 'box flex1',
|
||
template: _.template($('#terminal-window-template').html()),
|
||
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
|
||
this.container = new ModalView();
|
||
this.JSON = {
|
||
title: options.title || 'Heed This Warning!'
|
||
};
|
||
|
||
this.render();
|
||
},
|
||
|
||
getInsideElement: function() {
|
||
return this.$('#inside');
|
||
}
|
||
});
|
||
|
||
var ModalAlert = ContainedBase.extend({
|
||
tagName: 'div',
|
||
template: _.template($('#modal-alert-template').html()),
|
||
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
this.JSON = {
|
||
title: options.title || 'Something to say',
|
||
text: options.text || 'Here is a paragraph',
|
||
markdown: options.markdown
|
||
};
|
||
|
||
if (options.markdowns) {
|
||
this.JSON.markdown = options.markdowns.join('\n');
|
||
}
|
||
|
||
this.container = new ModalTerminal({
|
||
title: 'Alert!'
|
||
});
|
||
this.render();
|
||
},
|
||
|
||
render: function() {
|
||
var HTML = (this.JSON.markdown) ?
|
||
require('markdown').markdown.toHTML(this.JSON.markdown) :
|
||
this.template(this.JSON);
|
||
|
||
// call to super, not super elegant but better than
|
||
// copy paste code
|
||
ModalAlert.__super__.render.apply(this, [HTML]);
|
||
}
|
||
});
|
||
|
||
var ZoomAlertWindow = Backbone.View.extend({
|
||
initialize: function(options) {
|
||
this.grabBatons();
|
||
this.modalAlert = new ModalAlert({
|
||
markdowns: [
|
||
'## That zoom level is not supported :-/',
|
||
'Please zoom back to a supported zoom level with Ctrl + and Ctrl -',
|
||
'',
|
||
'(and of course, pull requests to fix this are appreciated :D)'
|
||
]
|
||
});
|
||
|
||
this.modalAlert.show();
|
||
},
|
||
|
||
grabBatons: function() {
|
||
Main.getEventBaton().stealBaton('zoomChange', this.zoomChange, this);
|
||
},
|
||
|
||
releaseBatons: function() {
|
||
Main.getEventBaton().releaseBaton('zoomChange', this.zoomChange, this);
|
||
},
|
||
|
||
zoomChange: function(level) {
|
||
if (level <= Constants.VIEWPORT.maxZoom &&
|
||
level >= Constants.VIEWPORT.minZoom) {
|
||
this.finish();
|
||
}
|
||
},
|
||
|
||
finish: function() {
|
||
this.releaseBatons();
|
||
this.modalAlert.die();
|
||
}
|
||
});
|
||
|
||
exports.ModalView = ModalView;
|
||
exports.ModalTerminal = ModalTerminal;
|
||
exports.ModalAlert = ModalAlert;
|
||
exports.ContainedBase = ContainedBase;
|
||
exports.ConfirmCancelView = ConfirmCancelView;
|
||
exports.LeftRightView = LeftRightView;
|
||
exports.ZoomAlertWindow = ZoomAlertWindow;
|
||
|
||
|
||
});
|
||
require("/src/js/views/index.js");
|
||
|
||
require.define("/src/js/views/multiView.js",function(require,module,exports,__dirname,__filename,process,global){var GitError = require('../util/errors').GitError;
|
||
var _ = require('underscore');
|
||
var Q = require('q');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;
|
||
|
||
var ModalTerminal = require('../views').ModalTerminal;
|
||
var ContainedBase = require('../views').ContainedBase;
|
||
var ConfirmCancelView = require('../views').ConfirmCancelView;
|
||
var LeftRightView = require('../views').LeftRightView;
|
||
var ModalAlert = require('../views').ModalAlert;
|
||
var KeyboardListener = require('../util/keyboard').KeyboardListener;
|
||
|
||
var MultiView = Backbone.View.extend({
|
||
tagName: 'div',
|
||
className: 'multiView',
|
||
// ms to debounce the nav functions
|
||
navEventDebounce: 750,
|
||
deathTime: 700,
|
||
|
||
// a simple mapping of what childViews we support
|
||
typeToConstructor: {
|
||
ModalAlert: ModalAlert
|
||
},
|
||
|
||
initialize: function(options) {
|
||
options = options || {};
|
||
this.childViewJSONs = options.childViews || [{
|
||
type: 'ModalAlert',
|
||
options: {
|
||
markdown: 'Woah wtf!!'
|
||
}
|
||
}, {
|
||
type: 'ModalAlert',
|
||
options: {
|
||
markdown: 'Im second'
|
||
}
|
||
}];
|
||
this.deferred = options.deferred || Q.defer();
|
||
|
||
this.childViews = [];
|
||
this.currentIndex = 0;
|
||
|
||
this.navEvents = _.clone(Backbone.Events);
|
||
this.navEvents.on('negative', this.getNegFunc(), this);
|
||
this.navEvents.on('positive', this.getPosFunc(), this);
|
||
|
||
this.keyboardListener = new KeyboardListener({
|
||
events: this.navEvents,
|
||
aliasMap: {
|
||
left: 'negative',
|
||
right: 'positive',
|
||
enter: 'positive'
|
||
}
|
||
});
|
||
|
||
this.render();
|
||
if (!options.wait) {
|
||
this.start();
|
||
}
|
||
},
|
||
|
||
onWindowFocus: function() {
|
||
// nothing here for now...
|
||
// TODO -- add a cool glow effect?
|
||
},
|
||
|
||
getAnimationTime: function() {
|
||
return 700;
|
||
},
|
||
|
||
getPromise: function() {
|
||
return this.deferred.promise;
|
||
},
|
||
|
||
getPosFunc: function() {
|
||
return _.debounce(_.bind(function() {
|
||
this.navForward();
|
||
}, this), this.navEventDebounce, true);
|
||
},
|
||
|
||
getNegFunc: function() {
|
||
return _.debounce(_.bind(function() {
|
||
this.navBackward();
|
||
}, this), this.navEventDebounce, true);
|
||
},
|
||
|
||
navForward: function() {
|
||
if (this.currentIndex === this.childViews.length - 1) {
|
||
this.hideViewIndex(this.currentIndex);
|
||
this.finish();
|
||
return;
|
||
}
|
||
|
||
this.navIndexChange(1);
|
||
},
|
||
|
||
navBackward: function() {
|
||
if (this.currentIndex === 0) {
|
||
return;
|
||
}
|
||
|
||
this.navIndexChange(-1);
|
||
},
|
||
|
||
navIndexChange: function(delta) {
|
||
this.hideViewIndex(this.currentIndex);
|
||
this.currentIndex += delta;
|
||
this.showViewIndex(this.currentIndex);
|
||
},
|
||
|
||
hideViewIndex: function(index) {
|
||
this.childViews[index].hide();
|
||
},
|
||
|
||
showViewIndex: function(index) {
|
||
this.childViews[index].show();
|
||
},
|
||
|
||
finish: function() {
|
||
// first we stop listening to keyboard and give that back to UI, which
|
||
// other views will take if they need to
|
||
this.keyboardListener.mute();
|
||
|
||
_.each(this.childViews, function(childView) {
|
||
childView.die();
|
||
});
|
||
|
||
this.deferred.resolve();
|
||
},
|
||
|
||
start: function() {
|
||
// steal the window focus baton
|
||
this.showViewIndex(this.currentIndex);
|
||
},
|
||
|
||
createChildView: function(viewJSON) {
|
||
var type = viewJSON.type;
|
||
if (!this.typeToConstructor[type]) {
|
||
throw new Error('no constructor for type "' + type + '"');
|
||
}
|
||
var view = new this.typeToConstructor[type](viewJSON.options);
|
||
return view;
|
||
},
|
||
|
||
addNavToView: function(view, index) {
|
||
var leftRight = new LeftRightView({
|
||
events: this.navEvents,
|
||
// we want the arrows to be on the same level as the content (not
|
||
// beneath), so we go one level up with getDestination()
|
||
destination: view.getDestination(),
|
||
showLeft: (index !== 0),
|
||
lastNav: (index === this.childViewJSONs.length - 1)
|
||
});
|
||
},
|
||
|
||
render: function() {
|
||
// go through each and render... show the first
|
||
_.each(this.childViewJSONs, function(childViewJSON, index) {
|
||
var childView = this.createChildView(childViewJSON);
|
||
this.childViews.push(childView);
|
||
this.addNavToView(childView, index);
|
||
}, this);
|
||
}
|
||
});
|
||
|
||
exports.MultiView = MultiView;
|
||
|
||
|
||
});
|
||
require("/src/js/views/multiView.js");
|
||
|
||
require.define("/src/js/views/rebaseView.js",function(require,module,exports,__dirname,__filename,process,global){var GitError = require('../util/errors').GitError;
|
||
var _ = require('underscore');
|
||
var Q = require('q');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? require('backbone') : window.Backbone;
|
||
|
||
var ModalTerminal = require('../views').ModalTerminal;
|
||
var ContainedBase = require('../views').ContainedBase;
|
||
var ConfirmCancelView = require('../views').ConfirmCancelView;
|
||
var LeftRightView = require('../views').LeftRightView;
|
||
|
||
var InteractiveRebaseView = ContainedBase.extend({
|
||
tagName: 'div',
|
||
template: _.template($('#interactive-rebase-template').html()),
|
||
|
||
initialize: function(options) {
|
||
this.deferred = options.deferred;
|
||
this.rebaseMap = {};
|
||
this.entryObjMap = {};
|
||
|
||
this.rebaseEntries = new RebaseEntryCollection();
|
||
_.each(options.toRebase, function(commit) {
|
||
var id = commit.get('id');
|
||
this.rebaseMap[id] = commit;
|
||
|
||
// make basic models for each commit
|
||
this.entryObjMap[id] = new RebaseEntry({
|
||
id: id
|
||
});
|
||
this.rebaseEntries.add(this.entryObjMap[id]);
|
||
}, this);
|
||
|
||
this.container = new ModalTerminal({
|
||
title: 'Interactive Rebase'
|
||
});
|
||
this.render();
|
||
|
||
// show the dialog holder
|
||
this.show();
|
||
},
|
||
|
||
confirm: function() {
|
||
this.die();
|
||
|
||
// get our ordering
|
||
var uiOrder = [];
|
||
this.$('ul.rebaseEntries li').each(function(i, obj) {
|
||
uiOrder.push(obj.id);
|
||
});
|
||
|
||
// now get the real array
|
||
var toRebase = [];
|
||
_.each(uiOrder, function(id) {
|
||
// the model pick check
|
||
if (this.entryObjMap[id].get('pick')) {
|
||
toRebase.unshift(this.rebaseMap[id]);
|
||
}
|
||
}, this);
|
||
|
||
toRebase.reverse();
|
||
this.deferred.resolve(toRebase);
|
||
// garbage collection will get us
|
||
this.$el.html('');
|
||
},
|
||
|
||
render: function() {
|
||
var json = {
|
||
num: _.keys(this.rebaseMap).length
|
||
};
|
||
|
||
var destination = this.container.getInsideElement();
|
||
this.$el.html(this.template(json));
|
||
$(destination).append(this.el);
|
||
|
||
// also render each entry
|
||
var listHolder = this.$('ul.rebaseEntries');
|
||
this.rebaseEntries.each(function(entry) {
|
||
new RebaseEntryView({
|
||
el: listHolder,
|
||
model: entry
|
||
});
|
||
}, this);
|
||
|
||
// then make it reorderable..
|
||
listHolder.sortable({
|
||
axis: 'y',
|
||
placeholder: 'rebaseEntry transitionOpacity ui-state-highlight',
|
||
appendTo: 'parent'
|
||
});
|
||
|
||
this.makeButtons();
|
||
},
|
||
|
||
makeButtons: function() {
|
||
// control for button
|
||
var deferred = Q.defer();
|
||
deferred.promise
|
||
.then(_.bind(function() {
|
||
this.confirm();
|
||
}, this))
|
||
.fail(_.bind(function() {
|
||
// empty array does nothing, just like in git
|
||
this.hide();
|
||
this.deferred.resolve([]);
|
||
}, this))
|
||
.done();
|
||
|
||
// finally get our buttons
|
||
new ConfirmCancelView({
|
||
destination: this.$('.confirmCancel'),
|
||
deferred: deferred
|
||
});
|
||
}
|
||
});
|
||
|
||
var RebaseEntry = Backbone.Model.extend({
|
||
defaults: {
|
||
pick: true
|
||
},
|
||
|
||
toggle: function() {
|
||
this.set('pick', !this.get('pick'));
|
||
}
|
||
});
|
||
|
||
var RebaseEntryCollection = Backbone.Collection.extend({
|
||
model: RebaseEntry
|
||
});
|
||
|
||
var RebaseEntryView = Backbone.View.extend({
|
||
tagName: 'li',
|
||
template: _.template($('#interactive-rebase-entry-template').html()),
|
||
|
||
toggle: function() {
|
||
this.model.toggle();
|
||
|
||
// toggle a class also
|
||
this.listEntry.toggleClass('notPicked', !this.model.get('pick'));
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.render();
|
||
},
|
||
|
||
render: function() {
|
||
var json = this.model.toJSON();
|
||
this.$el.append(this.template(this.model.toJSON()));
|
||
|
||
// hacky :( who would have known jquery barfs on ids with %'s and quotes
|
||
this.listEntry = this.$el.children(':last');
|
||
|
||
this.listEntry.delegate('#toggleButton', 'click', _.bind(function() {
|
||
this.toggle();
|
||
}, this));
|
||
}
|
||
});
|
||
|
||
exports.InteractiveRebaseView = InteractiveRebaseView;
|
||
|
||
});
|
||
require("/src/js/views/rebaseView.js");
|
||
|
||
require.define("/src/js/visuals/animation/animationFactory.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var Animation = require('./index').Animation;
|
||
var GRAPHICS = require('../../util/constants').GRAPHICS;
|
||
|
||
/******************
|
||
* This class is responsible for a lot of the heavy lifting around creating an animation at a certain state in time.
|
||
* The tricky thing is that when a new commit has to be "born," say in the middle of a rebase
|
||
* or something, it must animate out from the parent position to it's birth position.
|
||
|
||
* These two positions though may not be where the commit finally ends up. So we actually need to take a snapshot of the tree,
|
||
* store all those positions, take a snapshot of the tree after a layout refresh afterwards, and then animate between those two spots.
|
||
* and then essentially animate the entire tree too.
|
||
*/
|
||
|
||
// essentially a static class
|
||
var AnimationFactory = function() {
|
||
|
||
};
|
||
|
||
AnimationFactory.prototype.genCommitBirthAnimation = function(animationQueue, commit, gitVisuals) {
|
||
if (!animationQueue) {
|
||
throw new Error("Need animation queue to add closure to!");
|
||
}
|
||
|
||
var time = GRAPHICS.defaultAnimationTime * 1.0;
|
||
var bounceTime = time * 2;
|
||
|
||
// essentially refresh the entire tree, but do a special thing for the commit
|
||
var visNode = commit.get('visNode');
|
||
|
||
var animation = function() {
|
||
// this takes care of refs and all that jazz, and updates all the positions
|
||
gitVisuals.refreshTree(time);
|
||
|
||
visNode.setBirth();
|
||
visNode.parentInFront();
|
||
gitVisuals.visBranchesFront();
|
||
|
||
visNode.animateUpdatedPosition(bounceTime, 'bounce');
|
||
visNode.animateOutgoingEdges(time);
|
||
};
|
||
|
||
animationQueue.add(new Animation({
|
||
closure: animation,
|
||
duration: Math.max(time, bounceTime)
|
||
}));
|
||
};
|
||
|
||
AnimationFactory.prototype.overrideOpacityDepth2 = function(attr, opacity) {
|
||
opacity = (opacity === undefined) ? 1 : opacity;
|
||
|
||
var newAttr = {};
|
||
|
||
_.each(attr, function(partObj, partName) {
|
||
newAttr[partName] = {};
|
||
_.each(partObj, function(val, key) {
|
||
if (key == 'opacity') {
|
||
newAttr[partName][key] = opacity;
|
||
} else {
|
||
newAttr[partName][key] = val;
|
||
}
|
||
});
|
||
});
|
||
return newAttr;
|
||
};
|
||
|
||
AnimationFactory.prototype.overrideOpacityDepth3 = function(snapShot, opacity) {
|
||
var newSnap = {};
|
||
|
||
_.each(snapShot, function(visObj, visID) {
|
||
newSnap[visID] = this.overrideOpacityDepth2(visObj, opacity);
|
||
}, this);
|
||
return newSnap;
|
||
};
|
||
|
||
AnimationFactory.prototype.genCommitBirthClosureFromSnapshot = function(step, gitVisuals) {
|
||
var time = GRAPHICS.defaultAnimationTime * 1.0;
|
||
var bounceTime = time * 1.5;
|
||
|
||
var visNode = step.newCommit.get('visNode');
|
||
var afterAttrWithOpacity = this.overrideOpacityDepth2(step.afterSnapshot[visNode.getID()]);
|
||
var afterSnapWithOpacity = this.overrideOpacityDepth3(step.afterSnapshot);
|
||
|
||
var animation = function() {
|
||
visNode.setBirthFromSnapshot(step.beforeSnapshot);
|
||
visNode.parentInFront();
|
||
gitVisuals.visBranchesFront();
|
||
|
||
visNode.animateToAttr(afterAttrWithOpacity, bounceTime, 'bounce');
|
||
visNode.animateOutgoingEdgesToAttr(afterSnapWithOpacity, bounceTime);
|
||
};
|
||
|
||
return animation;
|
||
};
|
||
|
||
AnimationFactory.prototype.refreshTree = function(animationQueue, gitVisuals) {
|
||
animationQueue.add(new Animation({
|
||
closure: function() {
|
||
gitVisuals.refreshTree();
|
||
}
|
||
}));
|
||
};
|
||
|
||
AnimationFactory.prototype.rebaseAnimation = function(animationQueue, rebaseResponse,
|
||
gitEngine, gitVisuals) {
|
||
|
||
this.rebaseHighlightPart(animationQueue, rebaseResponse, gitEngine);
|
||
this.rebaseBirthPart(animationQueue, rebaseResponse, gitEngine, gitVisuals);
|
||
};
|
||
|
||
AnimationFactory.prototype.rebaseHighlightPart = function(animationQueue, rebaseResponse, gitEngine) {
|
||
var fullTime = GRAPHICS.defaultAnimationTime * 0.66;
|
||
var slowTime = fullTime * 2.0;
|
||
|
||
// we want to highlight all the old commits
|
||
var oldCommits = rebaseResponse.toRebaseArray;
|
||
// we are either highlighting to a visBranch or a visNode
|
||
var visBranch = rebaseResponse.destinationBranch.get('visBranch');
|
||
if (!visBranch) {
|
||
// in the case where we rebase onto a commit
|
||
visBranch = rebaseResponse.destinationBranch.get('visNode');
|
||
}
|
||
|
||
_.each(oldCommits, function(oldCommit) {
|
||
var visNode = oldCommit.get('visNode');
|
||
animationQueue.add(new Animation({
|
||
closure: function() {
|
||
visNode.highlightTo(visBranch, slowTime, 'easeInOut');
|
||
},
|
||
duration: fullTime * 1.5
|
||
}));
|
||
|
||
}, this);
|
||
|
||
this.delay(animationQueue, fullTime * 2);
|
||
};
|
||
|
||
AnimationFactory.prototype.rebaseBirthPart = function(animationQueue, rebaseResponse,
|
||
gitEngine, gitVisuals) {
|
||
var rebaseSteps = rebaseResponse.rebaseSteps;
|
||
|
||
var newVisNodes = [];
|
||
_.each(rebaseSteps, function(step) {
|
||
var visNode = step.newCommit.get('visNode');
|
||
|
||
newVisNodes.push(visNode);
|
||
visNode.setOpacity(0);
|
||
visNode.setOutgoingEdgesOpacity(0);
|
||
}, this);
|
||
|
||
var previousVisNodes = [];
|
||
_.each(rebaseSteps, function(rebaseStep, index) {
|
||
var toOmit = newVisNodes.slice(index + 1);
|
||
|
||
var snapshotPart = this.genFromToSnapshotAnimation(
|
||
rebaseStep.beforeSnapshot,
|
||
rebaseStep.afterSnapshot,
|
||
toOmit,
|
||
previousVisNodes,
|
||
gitVisuals
|
||
);
|
||
var birthPart = this.genCommitBirthClosureFromSnapshot(rebaseStep, gitVisuals);
|
||
|
||
var animation = function() {
|
||
snapshotPart();
|
||
birthPart();
|
||
};
|
||
|
||
animationQueue.add(new Animation({
|
||
closure: animation,
|
||
duration: GRAPHICS.defaultAnimationTime * 1.5
|
||
}));
|
||
|
||
previousVisNodes.push(rebaseStep.newCommit.get('visNode'));
|
||
}, this);
|
||
|
||
// need to delay to let bouncing finish
|
||
this.delay(animationQueue);
|
||
|
||
this.refreshTree(animationQueue, gitVisuals);
|
||
};
|
||
|
||
AnimationFactory.prototype.delay = function(animationQueue, time) {
|
||
time = time || GRAPHICS.defaultAnimationTime;
|
||
animationQueue.add(new Animation({
|
||
closure: function() { },
|
||
duration: time
|
||
}));
|
||
};
|
||
|
||
AnimationFactory.prototype.genSetAllCommitOpacities = function(visNodes, opacity) {
|
||
// need to slice for closure
|
||
var nodesToAnimate = visNodes.slice(0);
|
||
|
||
return function() {
|
||
_.each(nodesToAnimate, function(visNode) {
|
||
visNode.setOpacity(opacity);
|
||
visNode.setOutgoingEdgesOpacity(opacity);
|
||
});
|
||
};
|
||
};
|
||
|
||
AnimationFactory.prototype.stripObjectsFromSnapshot = function(snapShot, toOmit) {
|
||
var ids = [];
|
||
_.each(toOmit, function(obj) {
|
||
ids.push(obj.getID());
|
||
});
|
||
|
||
var newSnapshot = {};
|
||
_.each(snapShot, function(val, key) {
|
||
if (_.include(ids, key)) {
|
||
// omit
|
||
return;
|
||
}
|
||
newSnapshot[key] = val;
|
||
}, this);
|
||
return newSnapshot;
|
||
};
|
||
|
||
AnimationFactory.prototype.genFromToSnapshotAnimation = function(
|
||
beforeSnapshot,
|
||
afterSnapshot,
|
||
commitsToOmit,
|
||
commitsToFixOpacity,
|
||
gitVisuals) {
|
||
|
||
// we want to omit the commit outgoing edges
|
||
var toOmit = [];
|
||
_.each(commitsToOmit, function(visNode) {
|
||
toOmit.push(visNode);
|
||
toOmit = toOmit.concat(visNode.get('outgoingEdges'));
|
||
});
|
||
|
||
var fixOpacity = function(obj) {
|
||
if (!obj) { return; }
|
||
_.each(obj, function(attr, partName) {
|
||
obj[partName].opacity = 1;
|
||
});
|
||
};
|
||
|
||
// HORRIBLE loop to fix opacities all throughout the snapshot
|
||
_.each([beforeSnapshot, afterSnapshot], function(snapShot) {
|
||
_.each(commitsToFixOpacity, function(visNode) {
|
||
fixOpacity(snapShot[visNode.getID()]);
|
||
_.each(visNode.get('outgoingEdges'), function(visEdge) {
|
||
fixOpacity(snapShot[visEdge.getID()]);
|
||
});
|
||
});
|
||
});
|
||
|
||
return function() {
|
||
gitVisuals.animateAllFromAttrToAttr(beforeSnapshot, afterSnapshot, toOmit);
|
||
};
|
||
};
|
||
|
||
exports.AnimationFactory = AnimationFactory;
|
||
|
||
|
||
});
|
||
require("/src/js/visuals/animation/animationFactory.js");
|
||
|
||
require.define("/src/js/visuals/animation/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var GLOBAL = require('../../util/constants').GLOBAL;
|
||
|
||
var Animation = Backbone.Model.extend({
|
||
defaults: {
|
||
duration: 300,
|
||
closure: null
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (!this.get('closure')) {
|
||
throw new Error('give me a closure!');
|
||
}
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.validateAtInit();
|
||
},
|
||
|
||
run: function() {
|
||
this.get('closure')();
|
||
}
|
||
});
|
||
|
||
var AnimationQueue = Backbone.Model.extend({
|
||
defaults: {
|
||
animations: null,
|
||
index: 0,
|
||
callback: null,
|
||
defer: false
|
||
},
|
||
|
||
initialize: function(options) {
|
||
this.set('animations', []);
|
||
if (!options.callback) {
|
||
console.warn('no callback');
|
||
}
|
||
},
|
||
|
||
add: function(animation) {
|
||
if (!animation instanceof Animation) {
|
||
throw new Error("Need animation not something else");
|
||
}
|
||
|
||
this.get('animations').push(animation);
|
||
},
|
||
|
||
start: function() {
|
||
this.set('index', 0);
|
||
|
||
// set the global lock that we are animating
|
||
GLOBAL.isAnimating = true;
|
||
this.next();
|
||
},
|
||
|
||
finish: function() {
|
||
// release lock here
|
||
GLOBAL.isAnimating = false;
|
||
this.get('callback')();
|
||
},
|
||
|
||
next: function() {
|
||
// ok so call the first animation, and then set a timeout to call the next.
|
||
// since an animation is defined as taking a specific amount of time,
|
||
// we can simply just use timeouts rather than promises / deferreds.
|
||
// for graphical displays that require an unknown amount of time, use deferreds
|
||
// but not animation queue (see the finishAnimation for that)
|
||
var animations = this.get('animations');
|
||
var index = this.get('index');
|
||
if (index >= animations.length) {
|
||
this.finish();
|
||
return;
|
||
}
|
||
|
||
var next = animations[index];
|
||
var duration = next.get('duration');
|
||
|
||
next.run();
|
||
|
||
this.set('index', index + 1);
|
||
setTimeout(_.bind(function() {
|
||
this.next();
|
||
}, this), duration);
|
||
}
|
||
});
|
||
|
||
exports.Animation = Animation;
|
||
exports.AnimationQueue = AnimationQueue;
|
||
|
||
});
|
||
require("/src/js/visuals/animation/index.js");
|
||
|
||
require.define("/src/js/visuals/index.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Q = require('q');
|
||
var Backbone = require('backbone');
|
||
|
||
var GRAPHICS = require('../util/constants').GRAPHICS;
|
||
var GLOBAL = require('../util/constants').GLOBAL;
|
||
|
||
var Collections = require('../models/collections');
|
||
var CommitCollection = Collections.CommitCollection;
|
||
var BranchCollection = Collections.BranchCollection;
|
||
|
||
var VisNode = require('../visuals/visNode').VisNode;
|
||
var VisBranch = require('../visuals/visBranch').VisBranch;
|
||
var VisBranchCollection = require('../visuals/visBranch').VisBranchCollection;
|
||
var VisEdge = require('../visuals/visEdge').VisEdge;
|
||
var VisEdgeCollection = require('../visuals/visEdge').VisEdgeCollection;
|
||
|
||
function GitVisuals(options) {
|
||
this.commitCollection = options.commitCollection;
|
||
this.branchCollection = options.branchCollection;
|
||
this.visNodeMap = {};
|
||
|
||
this.visEdgeCollection = new VisEdgeCollection();
|
||
this.visBranchCollection = new VisBranchCollection();
|
||
this.commitMap = {};
|
||
|
||
this.rootCommit = null;
|
||
this.branchStackMap = null;
|
||
this.upstreamBranchSet = null;
|
||
this.upstreamHeadSet = null;
|
||
|
||
this.paper = options.paper;
|
||
this.gitReady = false;
|
||
|
||
this.branchCollection.on('add', this.addBranchFromEvent, this);
|
||
this.branchCollection.on('remove', this.removeBranch, this);
|
||
this.deferred = [];
|
||
|
||
var Main = require('../app');
|
||
Main.getEvents().on('refreshTree', this.refreshTree, this);
|
||
}
|
||
|
||
GitVisuals.prototype.defer = function(action) {
|
||
this.deferred.push(action);
|
||
};
|
||
|
||
GitVisuals.prototype.deferFlush = function() {
|
||
_.each(this.deferred, function(action) {
|
||
action();
|
||
}, this);
|
||
this.deferred = [];
|
||
};
|
||
|
||
GitVisuals.prototype.resetAll = function() {
|
||
// make sure to copy these collections because we remove
|
||
// items in place and underscore is too dumb to detect length change
|
||
var edges = this.visEdgeCollection.toArray();
|
||
_.each(edges, function(visEdge) {
|
||
visEdge.remove();
|
||
}, this);
|
||
|
||
var branches = this.visBranchCollection.toArray();
|
||
_.each(branches, function(visBranch) {
|
||
visBranch.remove();
|
||
}, this);
|
||
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.remove();
|
||
}, this);
|
||
|
||
this.visEdgeCollection.reset();
|
||
this.visBranchCollection.reset();
|
||
|
||
this.visNodeMap = {};
|
||
this.rootCommit = null;
|
||
this.commitMap = {};
|
||
};
|
||
|
||
GitVisuals.prototype.assignGitEngine = function(gitEngine) {
|
||
this.gitEngine = gitEngine;
|
||
this.initHeadBranch();
|
||
this.deferFlush();
|
||
};
|
||
|
||
GitVisuals.prototype.initHeadBranch = function() {
|
||
// it's unfortaunte we have to do this, but the head branch
|
||
// is an edge case because it's not part of a collection so
|
||
// we can't use events to load or unload it. thus we have to call
|
||
// this ugly method which will be deleted one day
|
||
|
||
// seed this with the HEAD pseudo-branch
|
||
this.addBranchFromEvent(this.gitEngine.HEAD);
|
||
};
|
||
|
||
GitVisuals.prototype.getScreenPadding = function() {
|
||
// for now we return the node radius subtracted from the walls
|
||
return {
|
||
widthPadding: GRAPHICS.nodeRadius * 1.5,
|
||
heightPadding: GRAPHICS.nodeRadius * 1.5
|
||
};
|
||
};
|
||
|
||
GitVisuals.prototype.toScreenCoords = function(pos) {
|
||
if (!this.paper.width) {
|
||
throw new Error('being called too early for screen coords');
|
||
}
|
||
var padding = this.getScreenPadding();
|
||
|
||
var shrink = function(frac, total, padding) {
|
||
return padding + frac * (total - padding * 2);
|
||
};
|
||
|
||
return {
|
||
x: shrink(pos.x, this.paper.width, padding.widthPadding),
|
||
y: shrink(pos.y, this.paper.height, padding.heightPadding)
|
||
};
|
||
};
|
||
|
||
GitVisuals.prototype.animateAllAttrKeys = function(keys, attr, speed, easing) {
|
||
var deferred = Q.defer();
|
||
|
||
var animate = function(visObj) {
|
||
visObj.animateAttrKeys(keys, attr, speed, easing);
|
||
};
|
||
|
||
this.visBranchCollection.each(animate);
|
||
this.visEdgeCollection.each(animate);
|
||
_.each(this.visNodeMap, animate);
|
||
|
||
var time = (speed !== undefined) ? speed : GRAPHICS.defaultAnimationTime;
|
||
setTimeout(function() {
|
||
deferred.resolve();
|
||
}, time);
|
||
|
||
return deferred.promise;
|
||
};
|
||
|
||
GitVisuals.prototype.finishAnimation = function() {
|
||
var _this = this;
|
||
var deferred = Q.defer();
|
||
var animationDone = Q.defer();
|
||
var defaultTime = GRAPHICS.defaultAnimationTime;
|
||
var nodeRadius = GRAPHICS.nodeRadius;
|
||
|
||
var textString = 'Solved!!\n:D';
|
||
var text = null;
|
||
var makeText = _.bind(function() {
|
||
text = this.paper.text(
|
||
this.paper.width / 2,
|
||
this.paper.height / 2,
|
||
textString
|
||
);
|
||
text.attr({
|
||
opacity: 0,
|
||
'font-weight': 500,
|
||
'font-size': '32pt',
|
||
'font-family': 'Monaco, Courier, font-monospace',
|
||
stroke: '#000',
|
||
'stroke-width': 2,
|
||
fill: '#000'
|
||
});
|
||
text.animate({ opacity: 1 }, defaultTime);
|
||
}, this);
|
||
|
||
// this is a BIG ANIMATION but it ends up just being
|
||
// a sweet chain of promises but is pretty nice. this is
|
||
// after I discovered promises / deferred's. Unfortunately
|
||
// I wrote a lot of the git stuff before promises, so
|
||
// that's somewhat ugly
|
||
|
||
deferred.promise
|
||
// first fade out everything but circles
|
||
.then(_.bind(function() {
|
||
return this.animateAllAttrKeys(
|
||
{ exclude: ['circle'] },
|
||
{ opacity: 0 },
|
||
defaultTime * 1.1
|
||
);
|
||
}, this))
|
||
// then make circle radii bigger
|
||
.then(_.bind(function() {
|
||
return this.animateAllAttrKeys(
|
||
{ exclude: ['arrow', 'rect', 'path', 'text'] },
|
||
{ r: nodeRadius * 2 },
|
||
defaultTime * 1.5
|
||
);
|
||
}, this))
|
||
// then shrink em super fast
|
||
.then(_.bind(function() {
|
||
return this.animateAllAttrKeys(
|
||
{ exclude: ['arrow', 'rect', 'path', 'text'] },
|
||
{ r: nodeRadius * 0.75 },
|
||
defaultTime * 0.5
|
||
);
|
||
}, this))
|
||
// then explode them and display text
|
||
.then(_.bind(function() {
|
||
makeText();
|
||
return this.explodeNodes();
|
||
}, this))
|
||
.then(_.bind(function() {
|
||
return this.explodeNodes();
|
||
}, this))
|
||
// then fade circles (aka everything) in and back
|
||
.then(_.bind(function() {
|
||
return this.animateAllAttrKeys(
|
||
{ exclude: ['arrow', 'rect', 'path', 'text'] },
|
||
{},
|
||
defaultTime * 1.25
|
||
);
|
||
}, this))
|
||
// then fade everything in and remove text
|
||
.then(_.bind(function() {
|
||
text.animate({ opacity: 0 }, defaultTime, undefined, undefined, function() {
|
||
text.remove();
|
||
});
|
||
return this.animateAllAttrKeys(
|
||
{},
|
||
{}
|
||
);
|
||
}, this))
|
||
.then(function() {
|
||
animationDone.resolve();
|
||
})
|
||
.fail(function(reason) {
|
||
console.warn('animation error' + reason);
|
||
})
|
||
.done();
|
||
|
||
// start our animation chain right away
|
||
deferred.resolve();
|
||
return animationDone.promise;
|
||
};
|
||
|
||
GitVisuals.prototype.explodeNodes = function() {
|
||
var deferred = Q.defer();
|
||
var funcs = [];
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
funcs.push(visNode.getExplodeStepFunc());
|
||
});
|
||
|
||
var interval = setInterval(function() {
|
||
// object creation here is a bit ugly inside a loop,
|
||
// but the alternative is to just OR against a bunch
|
||
// of booleans which means the other stepFuncs
|
||
// are called unnecessarily when they have almost
|
||
// zero speed. would be interesting to see performance differences
|
||
var keepGoing = [];
|
||
_.each(funcs, function(func) {
|
||
if (func()) {
|
||
keepGoing.push(func);
|
||
}
|
||
});
|
||
|
||
if (!keepGoing.length) {
|
||
clearInterval(interval);
|
||
// next step :D wow I love promises
|
||
deferred.resolve();
|
||
return;
|
||
}
|
||
|
||
funcs = keepGoing;
|
||
}, 1/40);
|
||
|
||
return deferred.promise;
|
||
};
|
||
|
||
GitVisuals.prototype.animateAllFromAttrToAttr = function(fromSnapshot, toSnapshot, idsToOmit) {
|
||
var animate = function(obj) {
|
||
var id = obj.getID();
|
||
if (_.include(idsToOmit, id)) {
|
||
return;
|
||
}
|
||
|
||
if (!fromSnapshot[id] || !toSnapshot[id]) {
|
||
// its actually ok it doesnt exist yet
|
||
return;
|
||
}
|
||
obj.animateFromAttrToAttr(fromSnapshot[id], toSnapshot[id]);
|
||
};
|
||
|
||
this.visBranchCollection.each(animate);
|
||
this.visEdgeCollection.each(animate);
|
||
_.each(this.visNodeMap, animate);
|
||
};
|
||
|
||
/***************************************
|
||
== BEGIN Tree Calculation Parts ==
|
||
_ __ __ _
|
||
\\/ / \ \//_
|
||
\ \ / __| __
|
||
\ \___/ /_____/ /
|
||
| _______ \
|
||
\ ( ) / \_\
|
||
\ /
|
||
| |
|
||
| |
|
||
____+-_=+-^ ^+-=_=__________
|
||
|
||
^^ I drew that :D
|
||
|
||
**************************************/
|
||
|
||
GitVisuals.prototype.genSnapshot = function() {
|
||
this.fullCalc();
|
||
|
||
var snapshot = {};
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
snapshot[visNode.get('id')] = visNode.getAttributes();
|
||
}, this);
|
||
|
||
this.visBranchCollection.each(function(visBranch) {
|
||
snapshot[visBranch.getID()] = visBranch.getAttributes();
|
||
}, this);
|
||
|
||
this.visEdgeCollection.each(function(visEdge) {
|
||
snapshot[visEdge.getID()] = visEdge.getAttributes();
|
||
}, this);
|
||
|
||
return snapshot;
|
||
};
|
||
|
||
GitVisuals.prototype.refreshTree = function(speed) {
|
||
if (!this.gitReady) {
|
||
return;
|
||
}
|
||
|
||
// this method can only be called after graphics are rendered
|
||
this.fullCalc();
|
||
|
||
this.animateAll(speed);
|
||
};
|
||
|
||
GitVisuals.prototype.refreshTreeHarsh = function() {
|
||
this.fullCalc();
|
||
|
||
this.animateAll(0);
|
||
};
|
||
|
||
GitVisuals.prototype.animateAll = function(speed) {
|
||
this.zIndexReflow();
|
||
|
||
this.animateEdges(speed);
|
||
this.animateNodePositions(speed);
|
||
this.animateRefs(speed);
|
||
};
|
||
|
||
GitVisuals.prototype.fullCalc = function() {
|
||
this.calcTreeCoords();
|
||
this.calcGraphicsCoords();
|
||
};
|
||
|
||
GitVisuals.prototype.calcTreeCoords = function() {
|
||
// this method can only contain things that dont rely on graphics
|
||
if (!this.rootCommit) {
|
||
throw new Error('grr, no root commit!');
|
||
}
|
||
|
||
this.calcUpstreamSets();
|
||
this.calcBranchStacks();
|
||
|
||
this.calcDepth();
|
||
this.calcWidth();
|
||
};
|
||
|
||
GitVisuals.prototype.calcGraphicsCoords = function() {
|
||
this.visBranchCollection.each(function(visBranch) {
|
||
visBranch.updateName();
|
||
});
|
||
};
|
||
|
||
GitVisuals.prototype.calcUpstreamSets = function() {
|
||
this.upstreamBranchSet = this.gitEngine.getUpstreamBranchSet();
|
||
this.upstreamHeadSet = this.gitEngine.getUpstreamHeadSet();
|
||
};
|
||
|
||
GitVisuals.prototype.getCommitUpstreamBranches = function(commit) {
|
||
return this.branchStackMap[commit.get('id')];
|
||
};
|
||
|
||
GitVisuals.prototype.getBlendedHuesForCommit = function(commit) {
|
||
var branches = this.upstreamBranchSet[commit.get('id')];
|
||
if (!branches) {
|
||
throw new Error('that commit doesnt have upstream branches!');
|
||
}
|
||
|
||
return this.blendHuesFromBranchStack(branches);
|
||
};
|
||
|
||
GitVisuals.prototype.blendHuesFromBranchStack = function(branchStackArray) {
|
||
var hueStrings = [];
|
||
_.each(branchStackArray, function(branchWrapper) {
|
||
var fill = branchWrapper.obj.get('visBranch').get('fill');
|
||
|
||
if (fill.slice(0,3) !== 'hsb') {
|
||
// crap! convert
|
||
var color = Raphael.color(fill);
|
||
fill = 'hsb(' + String(color.h) + ',' + String(color.l);
|
||
fill = fill + ',' + String(color.s) + ')';
|
||
}
|
||
|
||
hueStrings.push(fill);
|
||
});
|
||
|
||
return blendHueStrings(hueStrings);
|
||
};
|
||
|
||
GitVisuals.prototype.getCommitUpstreamStatus = function(commit) {
|
||
if (!this.upstreamBranchSet) {
|
||
throw new Error("Can't calculate this yet!");
|
||
}
|
||
|
||
var id = commit.get('id');
|
||
var branch = this.upstreamBranchSet;
|
||
var head = this.upstreamHeadSet;
|
||
|
||
if (branch[id]) {
|
||
return 'branch';
|
||
} else if (head[id]) {
|
||
return 'head';
|
||
} else {
|
||
return 'none';
|
||
}
|
||
};
|
||
|
||
GitVisuals.prototype.calcBranchStacks = function() {
|
||
var branches = this.gitEngine.getBranches();
|
||
var map = {};
|
||
_.each(branches, function(branch) {
|
||
var thisId = branch.target.get('id');
|
||
|
||
map[thisId] = map[thisId] || [];
|
||
map[thisId].push(branch);
|
||
map[thisId].sort(function(a, b) {
|
||
var aId = a.obj.get('id');
|
||
var bId = b.obj.get('id');
|
||
if (aId == 'master' || bId == 'master') {
|
||
return aId == 'master' ? -1 : 1;
|
||
}
|
||
return aId.localeCompare(bId);
|
||
});
|
||
});
|
||
this.branchStackMap = map;
|
||
};
|
||
|
||
GitVisuals.prototype.calcWidth = function() {
|
||
this.maxWidthRecursive(this.rootCommit);
|
||
|
||
this.assignBoundsRecursive(this.rootCommit, 0, 1);
|
||
};
|
||
|
||
GitVisuals.prototype.maxWidthRecursive = function(commit) {
|
||
var childrenTotalWidth = 0;
|
||
_.each(commit.get('children'), function(child) {
|
||
// only include this if we are the "main" parent of
|
||
// this child
|
||
if (child.isMainParent(commit)) {
|
||
var childWidth = this.maxWidthRecursive(child);
|
||
childrenTotalWidth += childWidth;
|
||
}
|
||
}, this);
|
||
|
||
var maxWidth = Math.max(1, childrenTotalWidth);
|
||
commit.get('visNode').set('maxWidth', maxWidth);
|
||
return maxWidth;
|
||
};
|
||
|
||
GitVisuals.prototype.assignBoundsRecursive = function(commit, min, max) {
|
||
// I always center myself within my bounds
|
||
var myWidthPos = (min + max) / 2.0;
|
||
commit.get('visNode').get('pos').x = myWidthPos;
|
||
|
||
if (commit.get('children').length === 0) {
|
||
return;
|
||
}
|
||
|
||
// i have a certain length to divide up
|
||
var myLength = max - min;
|
||
// I will divide up that length based on my children's max width in a
|
||
// basic box-flex model
|
||
var totalFlex = 0;
|
||
var children = commit.get('children');
|
||
_.each(children, function(child) {
|
||
if (child.isMainParent(commit)) {
|
||
totalFlex += child.get('visNode').getMaxWidthScaled();
|
||
}
|
||
}, this);
|
||
|
||
var prevBound = min;
|
||
|
||
// now go through and do everything
|
||
// TODO: order so the max width children are in the middle!!
|
||
_.each(children, function(child) {
|
||
if (!child.isMainParent(commit)) {
|
||
return;
|
||
}
|
||
|
||
var flex = child.get('visNode').getMaxWidthScaled();
|
||
var portion = (flex / totalFlex) * myLength;
|
||
var childMin = prevBound;
|
||
var childMax = childMin + portion;
|
||
this.assignBoundsRecursive(child, childMin, childMax);
|
||
prevBound = childMax;
|
||
}, this);
|
||
};
|
||
|
||
GitVisuals.prototype.calcDepth = function() {
|
||
var maxDepth = this.calcDepthRecursive(this.rootCommit, 0);
|
||
if (maxDepth > 15) {
|
||
// issue warning
|
||
console.warn('graphics are degrading from too many layers');
|
||
}
|
||
|
||
var depthIncrement = this.getDepthIncrement(maxDepth);
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.setDepthBasedOn(depthIncrement);
|
||
}, this);
|
||
};
|
||
|
||
/***************************************
|
||
== END Tree Calculation ==
|
||
_ __ __ _
|
||
\\/ / \ \//_
|
||
\ \ / __| __
|
||
\ \___/ /_____/ /
|
||
| _______ \
|
||
\ ( ) / \_\
|
||
\ /
|
||
| |
|
||
| |
|
||
____+-_=+-^ ^+-=_=__________
|
||
|
||
^^ I drew that :D
|
||
|
||
**************************************/
|
||
|
||
GitVisuals.prototype.animateNodePositions = function(speed) {
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.animateUpdatedPosition(speed);
|
||
}, this);
|
||
};
|
||
|
||
GitVisuals.prototype.turnOnPaper = function() {
|
||
this.gitReady = false;
|
||
};
|
||
|
||
// does making an accessor method make it any less hacky? that is the true question
|
||
GitVisuals.prototype.turnOffPaper = function() {
|
||
this.gitReady = true;
|
||
};
|
||
|
||
GitVisuals.prototype.addBranchFromEvent = function(branch, collection, index) {
|
||
var action = _.bind(function() {
|
||
this.addBranch(branch);
|
||
}, this);
|
||
|
||
if (!this.gitEngine || !this.gitReady) {
|
||
this.defer(action);
|
||
} else {
|
||
action();
|
||
}
|
||
};
|
||
|
||
GitVisuals.prototype.addBranch = function(branch) {
|
||
var visBranch = new VisBranch({
|
||
branch: branch,
|
||
gitVisuals: this,
|
||
gitEngine: this.gitEngine
|
||
});
|
||
|
||
this.visBranchCollection.add(visBranch);
|
||
if (this.gitReady) {
|
||
visBranch.genGraphics(this.paper);
|
||
}
|
||
};
|
||
|
||
GitVisuals.prototype.removeVisBranch = function(visBranch) {
|
||
this.visBranchCollection.remove(visBranch);
|
||
};
|
||
|
||
GitVisuals.prototype.removeVisNode = function(visNode) {
|
||
this.visNodeMap[visNode.getID()] = undefined;
|
||
};
|
||
|
||
GitVisuals.prototype.removeVisEdge = function(visEdge) {
|
||
this.visEdgeCollection.remove(visEdge);
|
||
};
|
||
|
||
GitVisuals.prototype.animateRefs = function(speed) {
|
||
this.visBranchCollection.each(function(visBranch) {
|
||
visBranch.animateUpdatedPos(speed);
|
||
}, this);
|
||
};
|
||
|
||
GitVisuals.prototype.animateEdges = function(speed) {
|
||
this.visEdgeCollection.each(function(edge) {
|
||
edge.animateUpdatedPath(speed);
|
||
}, this);
|
||
};
|
||
|
||
GitVisuals.prototype.getDepthIncrement = function(maxDepth) {
|
||
// assume there are at least 7 layers until later
|
||
maxDepth = Math.max(maxDepth, 7);
|
||
var increment = 1.0 / maxDepth;
|
||
return increment;
|
||
};
|
||
|
||
GitVisuals.prototype.calcDepthRecursive = function(commit, depth) {
|
||
commit.get('visNode').setDepth(depth);
|
||
|
||
var children = commit.get('children');
|
||
var maxDepth = depth;
|
||
_.each(children, function(child) {
|
||
var d = this.calcDepthRecursive(child, depth + 1);
|
||
maxDepth = Math.max(d, maxDepth);
|
||
}, this);
|
||
|
||
return maxDepth;
|
||
};
|
||
|
||
// we debounce here so we aren't firing a resize call on every resize event
|
||
// but only after they stop
|
||
GitVisuals.prototype.canvasResize = _.debounce(function(width, height) {
|
||
// refresh when we are ready
|
||
if (GLOBAL.isAnimating) {
|
||
var Main = require('../app');
|
||
Main.getEventBaton().trigger('commandSubmitted', 'refresh');
|
||
} else {
|
||
this.refreshTree();
|
||
}
|
||
}, 200);
|
||
|
||
GitVisuals.prototype.addNode = function(id, commit) {
|
||
this.commitMap[id] = commit;
|
||
if (commit.get('rootCommit')) {
|
||
this.rootCommit = commit;
|
||
}
|
||
|
||
var visNode = new VisNode({
|
||
id: id,
|
||
commit: commit,
|
||
gitVisuals: this,
|
||
gitEngine: this.gitEngine
|
||
});
|
||
this.visNodeMap[id] = visNode;
|
||
|
||
if (this.gitReady) {
|
||
visNode.genGraphics(this.paper);
|
||
}
|
||
return visNode;
|
||
};
|
||
|
||
GitVisuals.prototype.addEdge = function(idTail, idHead) {
|
||
var visNodeTail = this.visNodeMap[idTail];
|
||
var visNodeHead = this.visNodeMap[idHead];
|
||
|
||
if (!visNodeTail || !visNodeHead) {
|
||
throw new Error('one of the ids in (' + idTail +
|
||
', ' + idHead + ') does not exist');
|
||
}
|
||
|
||
var edge = new VisEdge({
|
||
tail: visNodeTail,
|
||
head: visNodeHead,
|
||
gitVisuals: this,
|
||
gitEngine: this.gitEngine
|
||
});
|
||
this.visEdgeCollection.add(edge);
|
||
|
||
if (this.gitReady) {
|
||
edge.genGraphics(this.paper);
|
||
}
|
||
};
|
||
|
||
GitVisuals.prototype.zIndexReflow = function() {
|
||
this.visNodesFront();
|
||
this.visBranchesFront();
|
||
};
|
||
|
||
GitVisuals.prototype.visNodesFront = function() {
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.toFront();
|
||
});
|
||
};
|
||
|
||
GitVisuals.prototype.visBranchesFront = function() {
|
||
this.visBranchCollection.each(function(vBranch) {
|
||
vBranch.nonTextToFront();
|
||
vBranch.textToFront();
|
||
});
|
||
};
|
||
|
||
GitVisuals.prototype.drawTreeFromReload = function() {
|
||
this.gitReady = true;
|
||
// gen all the graphics we need
|
||
this.deferFlush();
|
||
|
||
this.calcTreeCoords();
|
||
};
|
||
|
||
GitVisuals.prototype.drawTreeFirstTime = function() {
|
||
this.gitReady = true;
|
||
this.calcTreeCoords();
|
||
|
||
_.each(this.visNodeMap, function(visNode) {
|
||
visNode.genGraphics(this.paper);
|
||
}, this);
|
||
|
||
this.visEdgeCollection.each(function(edge) {
|
||
edge.genGraphics(this.paper);
|
||
}, this);
|
||
|
||
this.visBranchCollection.each(function(visBranch) {
|
||
visBranch.genGraphics(this.paper);
|
||
}, this);
|
||
|
||
this.zIndexReflow();
|
||
};
|
||
|
||
|
||
/************************
|
||
* Random util functions, some from liquidGraph
|
||
***********************/
|
||
function blendHueStrings(hueStrings) {
|
||
// assumes a sat of 0.7 and brightness of 1
|
||
|
||
var x = 0;
|
||
var y = 0;
|
||
var totalSat = 0;
|
||
var totalBright = 0;
|
||
var length = hueStrings.length;
|
||
|
||
_.each(hueStrings, function(hueString) {
|
||
var exploded = hueString.split('(')[1];
|
||
exploded = exploded.split(')')[0];
|
||
exploded = exploded.split(',');
|
||
|
||
totalSat += parseFloat(exploded[1]);
|
||
totalBright += parseFloat(exploded[2]);
|
||
var hue = parseFloat(exploded[0]);
|
||
|
||
var angle = hue * Math.PI * 2;
|
||
x += Math.cos(angle);
|
||
y += Math.sin(angle);
|
||
});
|
||
|
||
x = x / length;
|
||
y = y / length;
|
||
totalSat = totalSat / length;
|
||
totalBright = totalBright / length;
|
||
|
||
var hue = Math.atan2(y, x) / (Math.PI * 2); // could fail on 0's
|
||
if (hue < 0) {
|
||
hue = hue + 1;
|
||
}
|
||
return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')';
|
||
}
|
||
|
||
exports.GitVisuals = GitVisuals;
|
||
|
||
});
|
||
require("/src/js/visuals/index.js");
|
||
|
||
require.define("/src/js/visuals/tree.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
|
||
var VisBase = Backbone.Model.extend({
|
||
removeKeys: function(keys) {
|
||
_.each(keys, function(key) {
|
||
if (this.get(key)) {
|
||
this.get(key).remove();
|
||
}
|
||
}, this);
|
||
},
|
||
|
||
animateAttrKeys: function(keys, attrObj, speed, easing) {
|
||
// either we animate a specific subset of keys or all
|
||
// possible things we could animate
|
||
keys = _.extend(
|
||
{},
|
||
{
|
||
include: ['circle', 'arrow', 'rect', 'path', 'text'],
|
||
exclude: []
|
||
},
|
||
keys || {}
|
||
);
|
||
|
||
var attr = this.getAttributes();
|
||
|
||
// safely insert this attribute into all the keys we want
|
||
_.each(keys.include, function(key) {
|
||
attr[key] = _.extend(
|
||
{},
|
||
attr[key],
|
||
attrObj
|
||
);
|
||
});
|
||
|
||
_.each(keys.exclude, function(key) {
|
||
delete attr[key];
|
||
});
|
||
|
||
this.animateToAttr(attr, speed, easing);
|
||
}
|
||
});
|
||
|
||
exports.VisBase = VisBase;
|
||
|
||
|
||
});
|
||
require("/src/js/visuals/tree.js");
|
||
|
||
require.define("/src/js/visuals/visBranch.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var GRAPHICS = require('../util/constants').GRAPHICS;
|
||
|
||
var VisBase = require('../visuals/tree').VisBase;
|
||
|
||
var randomHueString = function() {
|
||
var hue = Math.random();
|
||
var str = 'hsb(' + String(hue) + ',0.7,1)';
|
||
return str;
|
||
};
|
||
|
||
var VisBranch = VisBase.extend({
|
||
defaults: {
|
||
pos: null,
|
||
text: null,
|
||
rect: null,
|
||
arrow: null,
|
||
isHead: false,
|
||
flip: 1,
|
||
|
||
fill: GRAPHICS.rectFill,
|
||
stroke: GRAPHICS.rectStroke,
|
||
'stroke-width': GRAPHICS.rectStrokeWidth,
|
||
|
||
offsetX: GRAPHICS.nodeRadius * 4.75,
|
||
offsetY: 0,
|
||
arrowHeight: 14,
|
||
arrowInnerSkew: 0,
|
||
arrowEdgeHeight: 6,
|
||
arrowLength: 14,
|
||
arrowOffsetFromCircleX: 10,
|
||
|
||
vPad: 5,
|
||
hPad: 5,
|
||
|
||
animationSpeed: GRAPHICS.defaultAnimationTime,
|
||
animationEasing: GRAPHICS.defaultEasing
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (!this.get('branch')) {
|
||
throw new Error('need a branch!');
|
||
}
|
||
},
|
||
|
||
getID: function() {
|
||
return this.get('branch').get('id');
|
||
},
|
||
|
||
initialize: function() {
|
||
this.validateAtInit();
|
||
|
||
// shorthand notation for the main objects
|
||
this.gitVisuals = this.get('gitVisuals');
|
||
this.gitEngine = this.get('gitEngine');
|
||
if (!this.gitEngine) {
|
||
console.log('throw damnit');
|
||
throw new Error('asd');
|
||
}
|
||
|
||
this.get('branch').set('visBranch', this);
|
||
var id = this.get('branch').get('id');
|
||
|
||
if (id == 'HEAD') {
|
||
// switch to a head ref
|
||
this.set('isHead', true);
|
||
this.set('flip', -1);
|
||
|
||
this.set('fill', GRAPHICS.headRectFill);
|
||
} else if (id !== 'master') {
|
||
// we need to set our color to something random
|
||
this.set('fill', randomHueString());
|
||
}
|
||
},
|
||
|
||
getCommitPosition: function() {
|
||
var commit = this.gitEngine.getCommitFromRef(this.get('branch'));
|
||
var visNode = commit.get('visNode');
|
||
return visNode.getScreenCoords();
|
||
},
|
||
|
||
getBranchStackIndex: function() {
|
||
if (this.get('isHead')) {
|
||
// head is never stacked with other branches
|
||
return 0;
|
||
}
|
||
|
||
var myArray = this.getBranchStackArray();
|
||
var index = -1;
|
||
_.each(myArray, function(branch, i) {
|
||
if (branch.obj == this.get('branch')) {
|
||
index = i;
|
||
}
|
||
}, this);
|
||
return index;
|
||
},
|
||
|
||
getBranchStackLength: function() {
|
||
if (this.get('isHead')) {
|
||
// head is always by itself
|
||
return 1;
|
||
}
|
||
|
||
return this.getBranchStackArray().length;
|
||
},
|
||
|
||
getBranchStackArray: function() {
|
||
var arr = this.gitVisuals.branchStackMap[this.get('branch').get('target').get('id')];
|
||
if (arr === undefined) {
|
||
// this only occurs when we are generating graphics inside of
|
||
// a new Branch instantiation, so we need to force the update
|
||
this.gitVisuals.calcBranchStacks();
|
||
return this.getBranchStackArray();
|
||
}
|
||
return arr;
|
||
},
|
||
|
||
getTextPosition: function() {
|
||
var pos = this.getCommitPosition();
|
||
|
||
// then order yourself accordingly. we use alphabetical sorting
|
||
// so everything is independent
|
||
var myPos = this.getBranchStackIndex();
|
||
return {
|
||
x: pos.x + this.get('flip') * this.get('offsetX'),
|
||
y: pos.y + myPos * GRAPHICS.multiBranchY + this.get('offsetY')
|
||
};
|
||
},
|
||
|
||
getRectPosition: function() {
|
||
var pos = this.getTextPosition();
|
||
var f = this.get('flip');
|
||
|
||
// first get text width and height
|
||
var textSize = this.getTextSize();
|
||
return {
|
||
x: pos.x - 0.5 * textSize.w - this.get('hPad'),
|
||
y: pos.y - 0.5 * textSize.h - this.get('vPad')
|
||
};
|
||
},
|
||
|
||
getArrowPath: function() {
|
||
// should make these util functions...
|
||
var offset2d = function(pos, x, y) {
|
||
return {
|
||
x: pos.x + x,
|
||
y: pos.y + y
|
||
};
|
||
};
|
||
var toStringCoords = function(pos) {
|
||
return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y));
|
||
};
|
||
var f = this.get('flip');
|
||
|
||
var arrowTip = offset2d(this.getCommitPosition(),
|
||
f * this.get('arrowOffsetFromCircleX'),
|
||
0
|
||
);
|
||
var arrowEdgeUp = offset2d(arrowTip, f * this.get('arrowLength'), -this.get('arrowHeight'));
|
||
var arrowEdgeLow = offset2d(arrowTip, f * this.get('arrowLength'), this.get('arrowHeight'));
|
||
|
||
var arrowInnerUp = offset2d(arrowEdgeUp,
|
||
f * this.get('arrowInnerSkew'),
|
||
this.get('arrowEdgeHeight')
|
||
);
|
||
var arrowInnerLow = offset2d(arrowEdgeLow,
|
||
f * this.get('arrowInnerSkew'),
|
||
-this.get('arrowEdgeHeight')
|
||
);
|
||
|
||
var tailLength = 49;
|
||
var arrowStartUp = offset2d(arrowInnerUp, f * tailLength, 0);
|
||
var arrowStartLow = offset2d(arrowInnerLow, f * tailLength, 0);
|
||
|
||
var pathStr = '';
|
||
pathStr += 'M' + toStringCoords(arrowStartUp) + ' ';
|
||
var coords = [
|
||
arrowInnerUp,
|
||
arrowEdgeUp,
|
||
arrowTip,
|
||
arrowEdgeLow,
|
||
arrowInnerLow,
|
||
arrowStartLow
|
||
];
|
||
_.each(coords, function(pos) {
|
||
pathStr += 'L' + toStringCoords(pos) + ' ';
|
||
}, this);
|
||
pathStr += 'z';
|
||
return pathStr;
|
||
},
|
||
|
||
getTextSize: function() {
|
||
var getTextWidth = function(visBranch) {
|
||
var textNode = visBranch.get('text').node;
|
||
return (textNode === null) ? 1 : textNode.clientWidth;
|
||
};
|
||
|
||
var textNode = this.get('text').node;
|
||
if (this.get('isHead')) {
|
||
// HEAD is a special case
|
||
return {
|
||
w: textNode.clientWidth,
|
||
h: textNode.clientHeight
|
||
};
|
||
}
|
||
|
||
var maxWidth = 0;
|
||
_.each(this.getBranchStackArray(), function(branch) {
|
||
maxWidth = Math.max(maxWidth, getTextWidth(
|
||
branch.obj.get('visBranch')
|
||
));
|
||
});
|
||
|
||
return {
|
||
w: maxWidth,
|
||
h: textNode.clientHeight
|
||
};
|
||
},
|
||
|
||
getSingleRectSize: function() {
|
||
var textSize = this.getTextSize();
|
||
var vPad = this.get('vPad');
|
||
var hPad = this.get('hPad');
|
||
return {
|
||
w: textSize.w + vPad * 2,
|
||
h: textSize.h + hPad * 2
|
||
};
|
||
},
|
||
|
||
getRectSize: function() {
|
||
var textSize = this.getTextSize();
|
||
// enforce padding
|
||
var vPad = this.get('vPad');
|
||
var hPad = this.get('hPad');
|
||
|
||
// number of other branch names we are housing
|
||
var totalNum = this.getBranchStackLength();
|
||
return {
|
||
w: textSize.w + vPad * 2,
|
||
h: textSize.h * totalNum * 1.1 + hPad * 2
|
||
};
|
||
},
|
||
|
||
getName: function() {
|
||
var name = this.get('branch').get('id');
|
||
var selected = this.gitEngine.HEAD.get('target').get('id');
|
||
|
||
var add = (selected == name) ? '*' : '';
|
||
return name + add;
|
||
},
|
||
|
||
nonTextToFront: function() {
|
||
this.get('arrow').toFront();
|
||
this.get('rect').toFront();
|
||
},
|
||
|
||
textToFront: function() {
|
||
this.get('text').toFront();
|
||
},
|
||
|
||
getFill: function() {
|
||
// in the easy case, just return your own fill if you are:
|
||
// - the HEAD ref
|
||
// - by yourself (length of 1)
|
||
// - part of a multi branch, but your thing is hidden
|
||
if (this.get('isHead') ||
|
||
this.getBranchStackLength() == 1 ||
|
||
this.getBranchStackIndex() !== 0) {
|
||
return this.get('fill');
|
||
}
|
||
|
||
// woof. now it's hard, we need to blend hues...
|
||
return this.gitVisuals.blendHuesFromBranchStack(this.getBranchStackArray());
|
||
},
|
||
|
||
remove: function() {
|
||
this.removeKeys(['text', 'arrow', 'rect']);
|
||
// also need to remove from this.gitVisuals
|
||
this.gitVisuals.removeVisBranch(this);
|
||
},
|
||
|
||
genGraphics: function(paper) {
|
||
var textPos = this.getTextPosition();
|
||
var name = this.getName();
|
||
var text;
|
||
|
||
// when from a reload, we dont need to generate the text
|
||
text = paper.text(textPos.x, textPos.y, String(name));
|
||
text.attr({
|
||
'font-size': 14,
|
||
'font-family': 'Monaco, Courier, font-monospace',
|
||
opacity: this.getTextOpacity()
|
||
});
|
||
this.set('text', text);
|
||
|
||
var rectPos = this.getRectPosition();
|
||
var sizeOfRect = this.getRectSize();
|
||
var rect = paper
|
||
.rect(rectPos.x, rectPos.y, sizeOfRect.w, sizeOfRect.h, 8)
|
||
.attr(this.getAttributes().rect);
|
||
this.set('rect', rect);
|
||
|
||
var arrowPath = this.getArrowPath();
|
||
var arrow = paper
|
||
.path(arrowPath)
|
||
.attr(this.getAttributes().arrow);
|
||
this.set('arrow', arrow);
|
||
|
||
this.attachClickHandlers();
|
||
rect.toFront();
|
||
text.toFront();
|
||
},
|
||
|
||
attachClickHandlers: function() {
|
||
var commandStr = 'git checkout ' + this.get('branch').get('id');
|
||
var Main = require('../app');
|
||
var objs = [this.get('rect'), this.get('text'), this.get('arrow')];
|
||
|
||
_.each(objs, function(rObj) {
|
||
rObj.click(function() {
|
||
Main.getEventBaton().trigger('commandSubmitted', commandStr);
|
||
});
|
||
$(rObj.node).css('cursor', 'pointer');
|
||
});
|
||
},
|
||
|
||
updateName: function() {
|
||
this.get('text').attr({
|
||
text: this.getName()
|
||
});
|
||
},
|
||
|
||
getNonTextOpacity: function() {
|
||
if (this.get('isHead')) {
|
||
return this.gitEngine.getDetachedHead() ? 1 : 0;
|
||
}
|
||
return this.getBranchStackIndex() === 0 ? 1 : 0.0;
|
||
},
|
||
|
||
getTextOpacity: function() {
|
||
if (this.get('isHead')) {
|
||
return this.gitEngine.getDetachedHead() ? 1 : 0;
|
||
}
|
||
return 1;
|
||
},
|
||
|
||
getAttributes: function() {
|
||
var nonTextOpacity = this.getNonTextOpacity();
|
||
var textOpacity = this.getTextOpacity();
|
||
this.updateName();
|
||
|
||
var textPos = this.getTextPosition();
|
||
var rectPos = this.getRectPosition();
|
||
var rectSize = this.getRectSize();
|
||
|
||
var arrowPath = this.getArrowPath();
|
||
|
||
return {
|
||
text: {
|
||
x: textPos.x,
|
||
y: textPos.y,
|
||
opacity: textOpacity
|
||
},
|
||
rect: {
|
||
x: rectPos.x,
|
||
y: rectPos.y,
|
||
width: rectSize.w,
|
||
height: rectSize.h,
|
||
opacity: nonTextOpacity,
|
||
fill: this.getFill(),
|
||
stroke: this.get('stroke'),
|
||
'stroke-width': this.get('stroke-width')
|
||
},
|
||
arrow: {
|
||
path: arrowPath,
|
||
opacity: nonTextOpacity,
|
||
fill: this.getFill(),
|
||
stroke: this.get('stroke'),
|
||
'stroke-width': this.get('stroke-width')
|
||
}
|
||
};
|
||
},
|
||
|
||
animateUpdatedPos: function(speed, easing) {
|
||
var attr = this.getAttributes();
|
||
this.animateToAttr(attr, speed, easing);
|
||
},
|
||
|
||
animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) {
|
||
// an animation of 0 is essentially setting the attribute directly
|
||
this.animateToAttr(fromAttr, 0);
|
||
this.animateToAttr(toAttr, speed, easing);
|
||
},
|
||
|
||
animateToAttr: function(attr, speed, easing) {
|
||
if (speed === 0) {
|
||
this.get('text').attr(attr.text);
|
||
this.get('rect').attr(attr.rect);
|
||
this.get('arrow').attr(attr.arrow);
|
||
return;
|
||
}
|
||
|
||
var s = speed !== undefined ? speed : this.get('animationSpeed');
|
||
var e = easing || this.get('animationEasing');
|
||
|
||
this.get('text').stop().animate(attr.text, s, e);
|
||
this.get('rect').stop().animate(attr.rect, s, e);
|
||
this.get('arrow').stop().animate(attr.arrow, s, e);
|
||
}
|
||
});
|
||
|
||
var VisBranchCollection = Backbone.Collection.extend({
|
||
model: VisBranch
|
||
});
|
||
|
||
exports.VisBranchCollection = VisBranchCollection;
|
||
exports.VisBranch = VisBranch;
|
||
|
||
});
|
||
require("/src/js/visuals/visBranch.js");
|
||
|
||
require.define("/src/js/visuals/visEdge.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var GRAPHICS = require('../util/constants').GRAPHICS;
|
||
|
||
var VisBase = require('../visuals/tree').VisBase;
|
||
|
||
var VisEdge = VisBase.extend({
|
||
defaults: {
|
||
tail: null,
|
||
head: null,
|
||
animationSpeed: GRAPHICS.defaultAnimationTime,
|
||
animationEasing: GRAPHICS.defaultEasing
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
var required = ['tail', 'head'];
|
||
_.each(required, function(key) {
|
||
if (!this.get(key)) {
|
||
throw new Error(key + ' is required!');
|
||
}
|
||
}, this);
|
||
},
|
||
|
||
getID: function() {
|
||
return this.get('tail').get('id') + '.' + this.get('head').get('id');
|
||
},
|
||
|
||
initialize: function() {
|
||
this.validateAtInit();
|
||
|
||
// shorthand for the main objects
|
||
this.gitVisuals = this.get('gitVisuals');
|
||
this.gitEngine = this.get('gitEngine');
|
||
|
||
this.get('tail').get('outgoingEdges').push(this);
|
||
},
|
||
|
||
remove: function() {
|
||
this.removeKeys(['path']);
|
||
this.gitVisuals.removeVisEdge(this);
|
||
},
|
||
|
||
genSmoothBezierPathString: function(tail, head) {
|
||
var tailPos = tail.getScreenCoords();
|
||
var headPos = head.getScreenCoords();
|
||
return this.genSmoothBezierPathStringFromCoords(tailPos, headPos);
|
||
},
|
||
|
||
genSmoothBezierPathStringFromCoords: function(tailPos, headPos) {
|
||
// we need to generate the path and control points for the bezier. format
|
||
// is M(move abs) C (curve to) (control point 1) (control point 2) (final point)
|
||
// the control points have to be __below__ to get the curve starting off straight.
|
||
|
||
var coords = function(pos) {
|
||
return String(Math.round(pos.x)) + ',' + String(Math.round(pos.y));
|
||
};
|
||
var offset = function(pos, dir, delta) {
|
||
delta = delta || GRAPHICS.curveControlPointOffset;
|
||
return {
|
||
x: pos.x,
|
||
y: pos.y + delta * dir
|
||
};
|
||
};
|
||
var offset2d = function(pos, x, y) {
|
||
return {
|
||
x: pos.x + x,
|
||
y: pos.y + y
|
||
};
|
||
};
|
||
|
||
// first offset tail and head by radii
|
||
tailPos = offset(tailPos, -1, this.get('tail').getRadius());
|
||
headPos = offset(headPos, 1, this.get('head').getRadius());
|
||
|
||
var str = '';
|
||
// first move to bottom of tail
|
||
str += 'M' + coords(tailPos) + ' ';
|
||
// start bezier
|
||
str += 'C';
|
||
// then control points above tail and below head
|
||
str += coords(offset(tailPos, -1)) + ' ';
|
||
str += coords(offset(headPos, 1)) + ' ';
|
||
// now finish
|
||
str += coords(headPos);
|
||
|
||
// arrow head
|
||
var delta = GRAPHICS.arrowHeadSize || 10;
|
||
str += ' L' + coords(offset2d(headPos, -delta, delta));
|
||
str += ' L' + coords(offset2d(headPos, delta, delta));
|
||
str += ' L' + coords(headPos);
|
||
|
||
// then go back, so we can fill correctly
|
||
str += 'C';
|
||
str += coords(offset(headPos, 1)) + ' ';
|
||
str += coords(offset(tailPos, -1)) + ' ';
|
||
str += coords(tailPos);
|
||
|
||
return str;
|
||
},
|
||
|
||
getBezierCurve: function() {
|
||
return this.genSmoothBezierPathString(this.get('tail'), this.get('head'));
|
||
},
|
||
|
||
getStrokeColor: function() {
|
||
return GRAPHICS.visBranchStrokeColorNone;
|
||
},
|
||
|
||
setOpacity: function(opacity) {
|
||
opacity = (opacity === undefined) ? 1 : opacity;
|
||
|
||
this.get('path').attr({opacity: opacity});
|
||
},
|
||
|
||
genGraphics: function(paper) {
|
||
var pathString = this.getBezierCurve();
|
||
|
||
var path = paper.path(pathString).attr({
|
||
'stroke-width': GRAPHICS.visBranchStrokeWidth,
|
||
'stroke': this.getStrokeColor(),
|
||
'stroke-linecap': 'round',
|
||
'stroke-linejoin': 'round',
|
||
'fill': this.getStrokeColor()
|
||
});
|
||
path.toBack();
|
||
this.set('path', path);
|
||
},
|
||
|
||
getOpacity: function() {
|
||
var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('tail'));
|
||
var map = {
|
||
'branch': 1,
|
||
'head': GRAPHICS.edgeUpstreamHeadOpacity,
|
||
'none': GRAPHICS.edgeUpstreamNoneOpacity
|
||
};
|
||
|
||
if (map[stat] === undefined) { throw new Error('bad stat'); }
|
||
return map[stat];
|
||
},
|
||
|
||
getAttributes: function() {
|
||
var newPath = this.getBezierCurve();
|
||
var opacity = this.getOpacity();
|
||
return {
|
||
path: {
|
||
path: newPath,
|
||
opacity: opacity
|
||
}
|
||
};
|
||
},
|
||
|
||
animateUpdatedPath: function(speed, easing) {
|
||
var attr = this.getAttributes();
|
||
this.animateToAttr(attr, speed, easing);
|
||
},
|
||
|
||
animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) {
|
||
// an animation of 0 is essentially setting the attribute directly
|
||
this.animateToAttr(fromAttr, 0);
|
||
this.animateToAttr(toAttr, speed, easing);
|
||
},
|
||
|
||
animateToAttr: function(attr, speed, easing) {
|
||
if (speed === 0) {
|
||
this.get('path').attr(attr.path);
|
||
return;
|
||
}
|
||
|
||
this.get('path').toBack();
|
||
this.get('path').stop().animate(
|
||
attr.path,
|
||
speed !== undefined ? speed : this.get('animationSpeed'),
|
||
easing || this.get('animationEasing')
|
||
);
|
||
}
|
||
});
|
||
|
||
var VisEdgeCollection = Backbone.Collection.extend({
|
||
model: VisEdge
|
||
});
|
||
|
||
exports.VisEdgeCollection = VisEdgeCollection;
|
||
exports.VisEdge = VisEdge;
|
||
|
||
});
|
||
require("/src/js/visuals/visEdge.js");
|
||
|
||
require.define("/src/js/visuals/visNode.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
var Backbone = require('backbone');
|
||
var GRAPHICS = require('../util/constants').GRAPHICS;
|
||
|
||
var VisBase = require('../visuals/tree').VisBase;
|
||
|
||
var VisNode = VisBase.extend({
|
||
defaults: {
|
||
depth: undefined,
|
||
maxWidth: null,
|
||
outgoingEdges: null,
|
||
|
||
circle: null,
|
||
text: null,
|
||
|
||
id: null,
|
||
pos: null,
|
||
radius: null,
|
||
|
||
commit: null,
|
||
animationSpeed: GRAPHICS.defaultAnimationTime,
|
||
animationEasing: GRAPHICS.defaultEasing,
|
||
|
||
fill: GRAPHICS.defaultNodeFill,
|
||
'stroke-width': GRAPHICS.defaultNodeStrokeWidth,
|
||
stroke: GRAPHICS.defaultNodeStroke
|
||
},
|
||
|
||
getID: function() {
|
||
return this.get('id');
|
||
},
|
||
|
||
validateAtInit: function() {
|
||
if (!this.get('id')) {
|
||
throw new Error('need id for mapping');
|
||
}
|
||
if (!this.get('commit')) {
|
||
throw new Error('need commit for linking');
|
||
}
|
||
|
||
if (!this.get('pos')) {
|
||
this.set('pos', {
|
||
x: Math.random(),
|
||
y: Math.random()
|
||
});
|
||
}
|
||
},
|
||
|
||
initialize: function() {
|
||
this.validateAtInit();
|
||
// shorthand for the main objects
|
||
this.gitVisuals = this.get('gitVisuals');
|
||
this.gitEngine = this.get('gitEngine');
|
||
|
||
this.set('outgoingEdges', []);
|
||
},
|
||
|
||
setDepth: function(depth) {
|
||
// for merge commits we need to max the depths across all
|
||
this.set('depth', Math.max(this.get('depth') || 0, depth));
|
||
},
|
||
|
||
setDepthBasedOn: function(depthIncrement) {
|
||
if (this.get('depth') === undefined) {
|
||
debugger;
|
||
throw new Error('no depth yet!');
|
||
}
|
||
var pos = this.get('pos');
|
||
pos.y = this.get('depth') * depthIncrement;
|
||
},
|
||
|
||
getMaxWidthScaled: function() {
|
||
// returns our max width scaled based on if we are visible
|
||
// from a branch or not
|
||
var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit'));
|
||
var map = {
|
||
branch: 1,
|
||
head: 0.3,
|
||
none: 0.1
|
||
};
|
||
if (map[stat] === undefined) { throw new Error('bad stat'); }
|
||
return map[stat] * this.get('maxWidth');
|
||
},
|
||
|
||
toFront: function() {
|
||
this.get('circle').toFront();
|
||
this.get('text').toFront();
|
||
},
|
||
|
||
getOpacity: function() {
|
||
var map = {
|
||
'branch': 1,
|
||
'head': GRAPHICS.upstreamHeadOpacity,
|
||
'none': GRAPHICS.upstreamNoneOpacity
|
||
};
|
||
|
||
var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit'));
|
||
if (map[stat] === undefined) {
|
||
throw new Error('invalid status');
|
||
}
|
||
return map[stat];
|
||
},
|
||
|
||
getTextScreenCoords: function() {
|
||
return this.getScreenCoords();
|
||
},
|
||
|
||
getAttributes: function() {
|
||
var pos = this.getScreenCoords();
|
||
var textPos = this.getTextScreenCoords();
|
||
var opacity = this.getOpacity();
|
||
|
||
return {
|
||
circle: {
|
||
cx: pos.x,
|
||
cy: pos.y,
|
||
opacity: opacity,
|
||
r: this.getRadius(),
|
||
fill: this.getFill(),
|
||
'stroke-width': this.get('stroke-width'),
|
||
stroke: this.get('stroke')
|
||
},
|
||
text: {
|
||
x: textPos.x,
|
||
y: textPos.y,
|
||
opacity: opacity
|
||
}
|
||
};
|
||
},
|
||
|
||
highlightTo: function(visObj, speed, easing) {
|
||
// a small function to highlight the color of a node for demonstration purposes
|
||
var color = visObj.get('fill');
|
||
|
||
var attr = {
|
||
circle: {
|
||
fill: color,
|
||
stroke: color,
|
||
'stroke-width': this.get('stroke-width') * 5
|
||
},
|
||
text: {}
|
||
};
|
||
|
||
this.animateToAttr(attr, speed, easing);
|
||
},
|
||
|
||
animateUpdatedPosition: function(speed, easing) {
|
||
var attr = this.getAttributes();
|
||
this.animateToAttr(attr, speed, easing);
|
||
},
|
||
|
||
animateFromAttrToAttr: function(fromAttr, toAttr, speed, easing) {
|
||
// an animation of 0 is essentially setting the attribute directly
|
||
this.animateToAttr(fromAttr, 0);
|
||
this.animateToAttr(toAttr, speed, easing);
|
||
},
|
||
|
||
animateToSnapshot: function(snapShot, speed, easing) {
|
||
if (!snapShot[this.getID()]) {
|
||
return;
|
||
}
|
||
this.animateToAttr(snapShot[this.getID()], speed, easing);
|
||
},
|
||
|
||
animateToAttr: function(attr, speed, easing) {
|
||
if (speed === 0) {
|
||
this.get('circle').attr(attr.circle);
|
||
this.get('text').attr(attr.text);
|
||
return;
|
||
}
|
||
|
||
var s = speed !== undefined ? speed : this.get('animationSpeed');
|
||
var e = easing || this.get('animationEasing');
|
||
|
||
this.get('circle').stop().animate(attr.circle, s, e);
|
||
this.get('text').stop().animate(attr.text, s, e);
|
||
|
||
if (easing == 'bounce' &&
|
||
attr.circle && attr.circle.cx !== undefined &&
|
||
attr.text && attr.text.x !== undefined ) {
|
||
// animate the x attribute without bouncing so it looks like there's
|
||
// gravity in only one direction. Just a small animation polish
|
||
this.get('circle').animate(attr.circle.cx, s, 'easeInOut');
|
||
this.get('text').animate(attr.text.x, s, 'easeInOut');
|
||
}
|
||
},
|
||
|
||
getScreenCoords: function() {
|
||
var pos = this.get('pos');
|
||
return this.gitVisuals.toScreenCoords(pos);
|
||
},
|
||
|
||
getRadius: function() {
|
||
return this.get('radius') || GRAPHICS.nodeRadius;
|
||
},
|
||
|
||
getParentScreenCoords: function() {
|
||
return this.get('commit').get('parents')[0].get('visNode').getScreenCoords();
|
||
},
|
||
|
||
setBirthPosition: function() {
|
||
// utility method for animating it out from underneath a parent
|
||
var parentCoords = this.getParentScreenCoords();
|
||
|
||
this.get('circle').attr({
|
||
cx: parentCoords.x,
|
||
cy: parentCoords.y,
|
||
opacity: 0,
|
||
r: 0
|
||
});
|
||
this.get('text').attr({
|
||
x: parentCoords.x,
|
||
y: parentCoords.y,
|
||
opacity: 0
|
||
});
|
||
},
|
||
|
||
setBirthFromSnapshot: function(beforeSnapshot) {
|
||
// first get parent attribute
|
||
// woof this is pretty bad data access...
|
||
var parentID = this.get('commit').get('parents')[0].get('visNode').getID();
|
||
var parentAttr = beforeSnapshot[parentID];
|
||
|
||
// then set myself faded on top of parent
|
||
this.get('circle').attr({
|
||
opacity: 0,
|
||
r: 0,
|
||
cx: parentAttr.circle.cx,
|
||
cy: parentAttr.circle.cy
|
||
});
|
||
|
||
this.get('text').attr({
|
||
opacity: 0,
|
||
x: parentAttr.text.x,
|
||
y: parentAttr.text.y
|
||
});
|
||
|
||
// then do edges
|
||
var parentCoords = {
|
||
x: parentAttr.circle.cx,
|
||
y: parentAttr.circle.cy
|
||
};
|
||
this.setOutgoingEdgesBirthPosition(parentCoords);
|
||
},
|
||
|
||
setBirth: function() {
|
||
this.setBirthPosition();
|
||
this.setOutgoingEdgesBirthPosition(this.getParentScreenCoords());
|
||
},
|
||
|
||
setOutgoingEdgesOpacity: function(opacity) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
edge.setOpacity(opacity);
|
||
});
|
||
},
|
||
|
||
animateOutgoingEdgesToAttr: function(snapShot, speed, easing) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
var attr = snapShot[edge.getID()];
|
||
edge.animateToAttr(attr);
|
||
}, this);
|
||
},
|
||
|
||
animateOutgoingEdges: function(speed, easing) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
edge.animateUpdatedPath(speed, easing);
|
||
}, this);
|
||
},
|
||
|
||
animateOutgoingEdgesFromSnapshot: function(snapshot, speed, easing) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
var attr = snapshot[edge.getID()];
|
||
edge.animateToAttr(attr, speed, easing);
|
||
}, this);
|
||
},
|
||
|
||
setOutgoingEdgesBirthPosition: function(parentCoords) {
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
var headPos = edge.get('head').getScreenCoords();
|
||
var path = edge.genSmoothBezierPathStringFromCoords(parentCoords, headPos);
|
||
edge.get('path').stop().attr({
|
||
path: path,
|
||
opacity: 0
|
||
});
|
||
}, this);
|
||
},
|
||
|
||
parentInFront: function() {
|
||
// woof! talk about bad data access
|
||
this.get('commit').get('parents')[0].get('visNode').toFront();
|
||
},
|
||
|
||
getFontSize: function(str) {
|
||
if (str.length < 3) {
|
||
return 12;
|
||
} else if (str.length < 5) {
|
||
return 10;
|
||
} else {
|
||
return 8;
|
||
}
|
||
},
|
||
|
||
getFill: function() {
|
||
// first get our status, might be easy from this
|
||
var stat = this.gitVisuals.getCommitUpstreamStatus(this.get('commit'));
|
||
if (stat == 'head') {
|
||
return GRAPHICS.headRectFill;
|
||
} else if (stat == 'none') {
|
||
return GRAPHICS.orphanNodeFill;
|
||
}
|
||
|
||
// now we need to get branch hues
|
||
return this.gitVisuals.getBlendedHuesForCommit(this.get('commit'));
|
||
},
|
||
|
||
attachClickHandlers: function() {
|
||
var commandStr = 'git checkout ' + this.get('commit').get('id');
|
||
var Main = require('../app');
|
||
_.each([this.get('circle'), this.get('text')], function(rObj) {
|
||
rObj.click(function() {
|
||
Main.getEventBaton().trigger('commandSubmitted', commandStr);
|
||
});
|
||
$(rObj.node).css('cursor', 'pointer');
|
||
});
|
||
},
|
||
|
||
setOpacity: function(opacity) {
|
||
opacity = (opacity === undefined) ? 1 : opacity;
|
||
|
||
// set the opacity on my stuff
|
||
var keys = ['circle', 'text'];
|
||
_.each(keys, function(key) {
|
||
this.get(key).attr({
|
||
opacity: opacity
|
||
});
|
||
}, this);
|
||
},
|
||
|
||
remove: function() {
|
||
this.removeKeys(['circle'], ['text']);
|
||
// needs a manual removal of text for whatever reason
|
||
this.get('text').remove();
|
||
|
||
this.gitVisuals.removeVisNode(this);
|
||
},
|
||
|
||
removeAll: function() {
|
||
this.remove();
|
||
_.each(this.get('outgoingEdges'), function(edge) {
|
||
edge.remove();
|
||
}, this);
|
||
},
|
||
|
||
getExplodeStepFunc: function() {
|
||
var circle = this.get('circle');
|
||
|
||
// decide on a speed
|
||
var speedMag = 20;
|
||
// aim upwards
|
||
var angle = Math.PI + Math.random() * 1 * Math.PI;
|
||
var gravity = 1 / 5;
|
||
var drag = 1 / 100;
|
||
|
||
var vx = speedMag * Math.cos(angle);
|
||
var vy = speedMag * Math.sin(angle);
|
||
var x = circle.attr('cx');
|
||
var y = circle.attr('cy');
|
||
|
||
var maxWidth = this.gitVisuals.paper.width;
|
||
var maxHeight = this.gitVisuals.paper.height;
|
||
var elasticity = 0.8;
|
||
var dt = 1.0;
|
||
|
||
var stepFunc = function() {
|
||
// lol epic runge kutta here... not
|
||
vy += gravity * dt - drag * vy;
|
||
vx -= drag * vx;
|
||
x += vx * dt;
|
||
y += vy * dt;
|
||
|
||
if (x < 0 || x > maxWidth) {
|
||
vx = elasticity * -vx;
|
||
x = (x < 0) ? 0 : maxWidth;
|
||
}
|
||
if (y < 0 || y > maxHeight) {
|
||
vy = elasticity * -vy;
|
||
y = (y < 0) ? 0 : maxHeight;
|
||
}
|
||
|
||
circle.attr({
|
||
cx: x,
|
||
cy: y
|
||
});
|
||
// continuation calculation
|
||
if ((vx * vx + vy * vy) < 0.01 && Math.abs(y - maxHeight) === 0) {
|
||
// dont need to animate anymore, we are on ground
|
||
return false;
|
||
}
|
||
// keep animating!
|
||
return true;
|
||
};
|
||
return stepFunc;
|
||
},
|
||
|
||
genGraphics: function() {
|
||
var paper = this.gitVisuals.paper;
|
||
|
||
var pos = this.getScreenCoords();
|
||
var textPos = this.getTextScreenCoords();
|
||
|
||
var circle = paper.circle(
|
||
pos.x,
|
||
pos.y,
|
||
this.getRadius()
|
||
).attr(this.getAttributes().circle);
|
||
|
||
var text = paper.text(textPos.x, textPos.y, String(this.get('id')));
|
||
text.attr({
|
||
'font-size': this.getFontSize(this.get('id')),
|
||
'font-weight': 'bold',
|
||
'font-family': 'Monaco, Courier, font-monospace',
|
||
opacity: this.getOpacity()
|
||
});
|
||
|
||
this.set('circle', circle);
|
||
this.set('text', text);
|
||
|
||
this.attachClickHandlers();
|
||
}
|
||
});
|
||
|
||
exports.VisNode = VisNode;
|
||
|
||
});
|
||
require("/src/js/visuals/visNode.js");
|
||
|
||
require.define("/src/js/visuals/visualization.js",function(require,module,exports,__dirname,__filename,process,global){var _ = require('underscore');
|
||
// horrible hack to get localStorage Backbone plugin
|
||
var Backbone = (!require('../util').isBrowser()) ? Backbone = require('backbone') : Backbone = window.Backbone;
|
||
|
||
var Collections = require('../models/collections');
|
||
var CommitCollection = Collections.CommitCollection;
|
||
var BranchCollection = Collections.BranchCollection;
|
||
|
||
var GitVisuals = require('../visuals').GitVisuals;
|
||
|
||
var Visualization = Backbone.View.extend({
|
||
initialize: function(options) {
|
||
var _this = this;
|
||
this.customEvents = _.clone(Backbone.Events);
|
||
|
||
new Raphael(10, 10, 200, 200, function() {
|
||
|
||
// for some reason raphael calls this function with a predefined
|
||
// context...
|
||
// so switch it
|
||
_this.paperInitialize(this, options);
|
||
});
|
||
},
|
||
|
||
paperInitialize: function(paper, options) {
|
||
options = options || {};
|
||
this.treeString = options.treeString;
|
||
this.paper = paper;
|
||
|
||
var Main = require('../app');
|
||
this.events = options.events || Main.getEvents();
|
||
this.eventBaton = options.eventBaton || Main.getEventBaton();
|
||
|
||
this.commitCollection = new CommitCollection();
|
||
this.branchCollection = new BranchCollection();
|
||
|
||
this.gitVisuals = new GitVisuals({
|
||
commitCollection: this.commitCollection,
|
||
branchCollection: this.branchCollection,
|
||
paper: this.paper
|
||
});
|
||
|
||
var GitEngine = require('../git').GitEngine;
|
||
this.gitEngine = new GitEngine({
|
||
collection: this.commitCollection,
|
||
branches: this.branchCollection,
|
||
gitVisuals: this.gitVisuals,
|
||
eventBaton: this.eventBaton
|
||
});
|
||
this.gitEngine.init();
|
||
this.gitVisuals.assignGitEngine(this.gitEngine);
|
||
|
||
this.myResize();
|
||
$(window).on('resize', _.bind(this.myResize, this));
|
||
this.gitVisuals.drawTreeFirstTime();
|
||
|
||
if (this.treeString) {
|
||
this.gitEngine.loadTreeFromString(this.treeString);
|
||
}
|
||
|
||
this.setTreeOpacity(0);
|
||
this.fadeTreeIn();
|
||
|
||
this.customEvents.trigger('gitEngineReady');
|
||
},
|
||
|
||
setTreeOpacity: function(level) {
|
||
$(this.paper.canvas).css('opacity', 0);
|
||
},
|
||
|
||
fadeTreeIn: function() {
|
||
$(this.paper.canvas).animate({opacity: 1}, 300);
|
||
},
|
||
|
||
myResize: function() {
|
||
var smaller = 1;
|
||
var el = this.el;
|
||
|
||
var left = el.offsetLeft;
|
||
var top = el.offsetTop;
|
||
var width = el.clientWidth - smaller;
|
||
var height = el.clientHeight - smaller;
|
||
|
||
$(this.paper.canvas).css({
|
||
left: left + 'px',
|
||
top: top + 'px'
|
||
});
|
||
this.paper.setSize(width, height);
|
||
this.gitVisuals.canvasResize(width, height);
|
||
}
|
||
});
|
||
|
||
exports.Visualization = Visualization;
|
||
|
||
});
|
||
require("/src/js/visuals/visualization.js");
|
||
|
||
})();
|