Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First version

  • Loading branch information...
commit 5c19708aa3de308cd70fbf2933efaea2c9ac1f82 0 parents
@judofyr authored
Showing with 2,640 additions and 0 deletions.
  1. +43 −0 COPYING
  2. +212 −0 README.markdown
  3. +5 −0 Rakefile
  4. +98 −0 examples/ghb.html
  5. +1 −0  spec/fixtures/blob.basic.js
  6. +1 −0  spec/fixtures/commit.basic.js
  7. +1 −0  spec/fixtures/issue.basic.js
  8. +1 −0  spec/fixtures/repo.basic.js
  9. +1 −0  spec/fixtures/repo.branches.js
  10. +1 −0  spec/fixtures/repo.tags.js
  11. +1 −0  spec/fixtures/tree.basic.js
  12. +1 −0  spec/fixtures/user.basic.js
  13. +1 −0  spec/fixtures/user.followers.js
  14. +1 −0  spec/fixtures/user.following.js
  15. +1 −0  spec/fixtures/user.repos.js
  16. +46 −0 spec/spec.core.js
  17. +40 −0 spec/spec.helpers.js
  18. +28 −0 spec/spec.html
  19. +78 −0 spec/spec.loader.js
  20. +59 −0 spec/spec.repo.js
  21. +76 −0 spec/spec.rest.js
  22. +45 −0 spec/spec.simple.js
  23. +71 −0 spec/spec.user.js
  24. +408 −0 src/github.js
  25. +1 −0  vendor/jsclass/class.js
  26. +205 −0 vendor/jsmin.rb
  27. BIN  vendor/jspec/images/bg.png
  28. BIN  vendor/jspec/images/hr.png
  29. BIN  vendor/jspec/images/sprites.bg.png
  30. BIN  vendor/jspec/images/sprites.png
  31. BIN  vendor/jspec/images/vr.png
  32. +130 −0 vendor/jspec/jspec.css
  33. +1,084 −0 vendor/jspec/jspec.js
43 COPYING
@@ -0,0 +1,43 @@
+
+ Potion is free software, released under an MIT license -- the very
+ brief paragraphs below. There is satisfaction simply in having
+ created this. Please use this how you may, even in commercial or
+ academic software. I've had a good time and am want for nothing.
+
+ ~
+
+ Copyright (c) 2008 why the lucky stiff
+
+ HOWEVER:
+ Be it known, parts of the object model taken from obj.c
+ (c) 2007 Ian Piumarta
+ <http://www.piumarta.com/software/id-objmodel/> (MIT licensed)
+ And, also, the design of the VM bytecode is from Lua
+ (c) 1994-2006 Lua.org, PUC-Rio
+ <http://lua.org/license.html> (MIT licensed)
+ <http://luaforge.net/docman/view.php/83/98/ANoFrillsIntroToLua51VMInstructions.pdf>
+ Lastly, khash.h
+ (c) 2008, by Attractive Chaos
+ <http://attractivechaos.awardspace.com/khash.h.html> (MIT licensed)
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+ SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
+ OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
212 README.markdown
@@ -0,0 +1,212 @@
+GitHub.js
+=========
+
+GitHub.js is a simple library for interacting with GitHub's v2 API.
+
+Quickstart
+----------
+
+ /*
+ * LOADERS
+ */
+
+ var user = new GitHub.User('judofyr');
+
+ // Let's load some data:
+ user.load('basic')
+
+ // That command is async, so let's add a callback too:
+ // (since basic already has been loaded,
+ // this would not make any calls to GitHub)
+ user.load('basic', function(obj) {
+ user == obj; // The object itself is passed to the callback,
+ // so you can easily pass in a normal function.
+
+ user.name; // Now our object has many properties (see API below)
+ });
+
+ // You can give it multiple loaders too:
+ user.load('basic', 'followers');
+ user.load('basic', 'following', callback);
+
+ // The loader defaults to 'basic':
+ user.load() // equals to user.load('basic')
+ user.load(callback) // equals to user.load('basic', callback)
+
+ // More stuff
+ user.isLoaded('basic') == true
+ user.reload('basic') // Load basic again
+
+ /*
+ * FETCHERS
+ */
+
+ var repo = user.repo('something');
+
+ // Let's load the issues:
+ repo.issues('open', function(issues) {
+ repo != issues; // Now issues is a completely different object.
+ });
+
+ // If we fetch them again, it will cause a new call to GitHub:
+ repo.issues('open', callback);
+
+Use
+---
+
+Run `rake build` and you'll get two versions:
+
+* github.only.js, which depends on JS.Class being loaded already
+* github.js, which already includes JS.Class
+
+Read the quickstart at the top, the API at the bottom and take a look at examples/
+
+Use the fork, Luke
+------------------
+
+I've used the excellent [JS.Class](http://jsclass.jcoglan.com/) which fakes Ruby's object model in JavaScript. It allowed me to quickly build this and get something working pretty well. The goal is however to *not* have any other dependencies and make it so idomatic JavaScript as possible.
+
+I would be very grateful if someone could take a look at the code and point out all my silly mistakes. This applies to everything, both the frontend API which you'll be using and the backend code which handles API calls. Fork away, don't be afraid of refactoring and deleting my code, and I'll merge your changes right away!
+
+Magnus Holm <judofyr@gmail.com>
+
+---
+
+API
+---
+
+### GitHub.User
+
+ var user = new GitHub.User(login);
+
+ user.load(function() {
+ user.company;
+ user.name;
+ user.following_count;
+ user.blog;
+ user.public_repo_count;
+ user.public_gist_count;
+ user.id;
+ user.login;
+ user.followers_count;
+ user.created_at;
+ user.email;
+ user.location;
+ });
+
+ user.load('followers', 'following', function() {
+ user.followers;
+ user.following;
+ });
+
+ user.load('repos', function() {
+ user.repos; // => [GitHub.Repo]
+ });
+
+ user.repo(project) // => GitHub.Repo
+ // Preloaded if you've already loaded repos
+
+### GitHub.Repo
+
+ var repo = new GitHub.Repo(owner, name);
+
+ repo.load(function() {
+ repo.name;
+ repo.watchers;
+ repo.private;
+ repo.url;
+ repo.fork;
+ repo.forks;
+ repo.description;
+ repo.homepage;
+ repo.owner;
+ });
+
+ repo.load('tags', 'branches', function() {
+ repo.tags; // { name: sha }
+ repo.branches // { name: sha }
+ });
+
+ repo.tree(sha) // GitHub.Tree
+
+ repo.issues('open' OR 'closed', function(issues) {
+ issues; // [GitHub.Issue]
+ });
+
+ repo.issue(number) // GitHub.Issue
+
+ repo.commits(branch, function(commits) {
+ commits; // [GitHub.Commit]
+ });
+
+ repo.commits(branch, filename, function(commits) {
+ commits; // [GitHub.Commit]
+ });
+
+ repo.commit(number) // GitHub.Commit
+
+### GitHub.Issue
+
+ var issue = new GitHub.Issue(repo, number); // Please use repo.issue(number)
+
+ issue.load(function() {
+ issue.user;
+ issue.updated_at;
+ issue.votes;
+ issue.number;
+ issue.title;
+ issue.body;
+ issue.position;
+ issue.state;
+ issue.created_at;
+ });
+
+### GitHub.Commit
+
+ var commit = new GitHub.Commit(repo, sha); // Please use repo.commit(sha)
+
+ commit.load(function() {
+ commit.message;
+ commit.parents;
+ commit.author;
+ commit.url;
+ commit.id;
+ commit.committed_date;
+ commit.authored_date;
+ commit.tree;
+ commit.committer;
+ });
+
+ commit.load('detailed', function() {
+ commit.added;
+ commit.removed;
+ commit.modified;
+ });
+
+### GitHub.Tree
+
+ var tree = new GitHub.Tree(repo, sha); // Please use repo.tree(sha)
+
+ tree.load(function() {
+ tree.children; // [GitHub.Tree OR GitHub.Blob]
+
+ var child = tree.children[0];
+
+ child.name;
+ child.sha;
+ child.mode;
+ child.type;
+ });
+
+### GitHub.Blob
+
+ var blob = new GitHub.Blob(tree, filename);
+
+ blob.load(function() {
+ blob.name;
+ blob.sha;
+ blob.size;
+ blob.mode;
+ blob.mime_type;
+ blob.data;
+ })
5 Rakefile
@@ -0,0 +1,5 @@
+task :build do
+ mkdir_p 'build'
+ sh "ruby vendor/jsmin.rb < src/github.js > build/github.only.js"
+ sh "cat vendor/jsclass/class.js src/github.js | ruby vendor/jsmin.rb > build/github.js"
+end
98 examples/ghb.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>GitHub Browser</title>
+ <!-- Combo-handled YUI CSS files: -->
+ <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/combo?2.7.0/build/treeview/assets/skins/sam/treeview.css">
+ <!-- Combo-handled YUI JS files: -->
+ <script type="text/javascript" src="http://yui.yahooapis.com/combo?2.7.0/build/yahoo-dom-event/yahoo-dom-event.js&2.7.0/build/treeview/treeview-min.js"></script>
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
+ <script type="text/javascript" src="../deps/jsclass.js"></script>
+ <script type="text/javascript" src="../src/github.js"></script>
+ <script type="text/javascript">
+ $(function(){
+ $('form').submit(function(){
+ $('#user').text('Loading...');
+ $('#file').text('');
+ $('#tree').text('');
+ var user = new GitHub.User($('#username').attr('value'));
+ user.load('basic', showUser);
+ return false;
+ });
+ });
+
+ function showUser(user) {
+ var box = $('#user');
+ box.text('Welcome ' + user.name + '. Here are your repos:');
+ $('#tree').text('Loading...');
+ user.load('repos', showRepos);
+ }
+
+ function showRepos(user) {
+ var tree = new YAHOO.widget.TreeView("tree");
+ tree.setDynamicLoad(loadLeaves);
+ tree.subscribe('clickEvent', clicked);
+ var root = tree.getRoot();
+
+ for (var i in user.repos) {
+ var repo = user.repos[i];
+ var node = new YAHOO.widget.TextNode(repo.name, root, false);
+ // Store the object in the node
+ $(node).data('object', repo);
+ }
+
+ tree.draw();
+ }
+
+ function clicked(thing) {
+ var object = $(thing.node).data('object');
+
+ // Load the file if it's a blob
+ if (object.isA(GitHub.Blob)) {
+ $('#file').text('Loading...');
+ object.load(function() {
+ $('#file').text(object.data);
+ });
+ }
+ }
+
+ function loadLeaves(node, completed) {
+ var object = $(node).data('object');
+
+ // If it's a repo, load the branches
+ if (object.isA(GitHub.Repo)) {
+ object.load('branches', function(){
+ for (var i in object.branches) {
+ var tree = object.tree(object.branches[i]);
+ var child = new YAHOO.widget.TextNode(i, node, false);
+ $(child).data('object', tree);
+ }
+ completed();
+ });
+ // If it's a tree, load the children
+ } else if (object.isA(GitHub.Tree)) {
+ object.load(function() {
+ for (var i in object.children) {
+ var tree = object.children[i];
+ var child = new YAHOO.widget.TextNode(tree.name, node, false);
+ $(child).data('object', tree);
+ if (tree.type == 'blob') {
+ child.isLeaf = true;
+ }
+ }
+ completed();
+ });
+ }
+ }
+ </script>
+</head>
+<body>
+ <form>
+ Username: <input type="text" id="username">
+ <input type="submit" value="Fetch info">
+ </form>
+ <div id="user"></div>
+ <div id="tree"></div>
+ <pre id="file"></pre>
+</body>
+</html>
1  spec/fixtures/blob.basic.js
@@ -0,0 +1 @@
+{"blob": {"name": "README", "size": 2840, "sha": "9e61400dbf0bf8b01dc2ba0792a6ad9d8cc8bf53", "mode": "100644", "mime_type": "text/plain", "data": "== Camping, a Microframework\n\nCamping is a web framework which consistently stays at less than 3kb of code.\nYou can probably view the complete source code on a single page. But, you\nknow, it's so small that, if you think about it, what can it really do?\n\nThe idea here is to store a complete fledgling web application in a single\nfile like many small CGIs. But to organize it as a Model-View-Controller\napplication like Rails does. You can then easily move it to Rails once you've\ngot it going.\n\n== A Camping Skeleton\n\nA skeletal Camping blog could look like this:\n\n require 'camping'\n \n Camping.goes :Blog\n\n module Blog::Models\n class Post \u003C Base; belongs_to :user; end\n class Comment \u003C Base; belongs_to :user; end\n class User \u003C Base; end\n end\n\n module Blog::Controllers\n class Index\n def get\n @posts = Post.find :all\n render :index\n end\n end\n end\n\n module Blog::Views\n def layout\n html do\n head { title \"My Blog\" }\n body do\n h1 \"My Blog\"\n self \u003C\u003C yield\n end\n end\n end\n\n def index\n for post in @posts\n h1 post.title\n end\n end\n end\n \n== Installation\n\nInterested yet? Luckily it's quite easy to install Camping. We'll be using\na tool called RubyGems, so if you don't have that installed yet, go grab it!\nOnce that's sorted out, open up a Terminal or Command Line and enter:\n\n* \u003Ctt\u003Egem install camping\u003C/tt\u003E\n\nOr for the bleeding edge:\n\n* \u003Ctt\u003Egem install camping --source http://gems.judofyr.net\u003C/tt\u003E\n\nCamping itself only depends on Rack[http://rack.rubyforge.org] and using the\ncommand above will only install Camping and Rack. However, if you intend to\nuse the views you also need to install +markaby+, and if you're going to use\nthe database you need +activerecord+ as well:\n\n* \u003Ctt\u003Egem install markaby\u003C/tt\u003E\n* \u003Ctt\u003Egem install activerecord\u003C/tt\u003E\n\n== Learning\n\n* Start by reading the documentation in {the Camping module}[link:classes/Camping.html],\n it should get you started pretty quick.\n* {The wiki}[http://wiki.github.com/why/camping] is the place for all tiny,\n useful tricks that we've collected over the years. Don't be afraid to\n share your own discoveries; the more, the better!\n* Still wondering? Subscribe to {the mailing list}[http://rubyforge.org/mailman/listinfo/camping-list],\n the place where it's all happening! Don't worry, though, the volume is just\n as micro as Camping itself. \n \n== Authors\n\nCamping was originally started by {why the lucky stiff}[http://en.wikipedia.org/wiki/Why_the_lucky_stiff],\nbut is now maintained by the _community_. This simply means that if we like your\npatch, it will be applied. Everything is managed through {the mailing list}[http://rubyforge.org/mailman/listinfo/camping-list],\nso just subscribe and you can instantly take a part in shaping Camping.\n"}}
1  spec/fixtures/commit.basic.js
@@ -0,0 +1 @@
+{"commit": {"removed": [], "added": [], "message": "Supporting nested params (post[body]=Something)\n\nSigned-off-by: why the lucky stiff \u003Cwhy@whytheluckystiff.net\u003E", "modified": [{"diff": "@@ -279,6 +279,7 @@ module Camping\n #\n module Base\n attr_accessor :input, :cookies, :headers, :body, :status, :root\n+ M = proc { |_, o, n| o.merge(n, \u0026M) }\n \n # Display a view, calling it by its method name +m+. If a \u003Ctt\u003Elayout\u003C/tt\u003E\n # method is found in Camping::Views, it will be used to wrap the HTML.\n@@ -412,18 +413,14 @@ module Camping\n \n def initialize(env, m) #:nodoc: \n r = @request = Rack::Request.new(@env = env)\n- @root, @input, @cookies,\n+ @root, p, @cookies,\n @headers, @status, @method =\n (env.SCRIPT_NAME||'').sub(/\\/$/,''), \n H[r.params], H[r.cookies],\n {}, m =~ /r(\\d+)/ ? $1.to_i : 200, m\n- \n- @input.each do |k, v|\n- if k[-2..-1] == \"[]\"\n- @input[k[0..-3]] = @input.delete(k)\n- elsif k =~ /(.*)\\[([^\\]]+)\\]$/\n- (@input[$1] ||= H[])[$2] = @input.delete(k)\n- end\n+ \n+ @input = p.inject(H[]) do |h, (k, v)|\n+ h.merge(k.split(/[\\]\\[]+/).reverse.inject(v) { |x, i| H[i =\u003E x] }, \u0026M)\n end\n end\n ", "filename": "lib/camping-unabridged.rb"}], "parents": [{"id": "64bd85c8a03d3c812f6b6aac1ca9660ba8f61b91"}], "url": "http://github.com/judofyr/camping/commit/e7719af8714a4141b2637648892c82198242c167", "author": {"name": "Magnus Holm", "email": "judofyr@gmail.com"}, "id": "e7719af8714a4141b2637648892c82198242c167", "committed_date": "2009-02-24T07:49:37-08:00", "authored_date": "2009-01-23T13:07:42-08:00", "tree": "95504fe4284fa4f8524a31b51e5d1181aa251944", "committer": {"name": "why the lucky stiff", "email": "why@whytheluckystiff.net"}}}
1  spec/fixtures/issue.basic.js
@@ -0,0 +1 @@
+{"issue": {"user": "schacon", "updated_at": "2009/04/17 16:11:03 -0700", "body": "An issue", "title": "new", "number": 1, "votes": 0, "created_at": "2009/04/17 16:08:49 -0700", "state": "closed"}}
1  spec/fixtures/repo.basic.js
@@ -0,0 +1 @@
+{"repository": {"description": "the 4k pocket full-of-gags web microframework", "forks": 2, "name": "camping", "private": false, "url": "http://github.com/judofyr/camping", "watchers": 16, "fork": true, "owner": "judofyr", "homepage": "http://code.whytheluckystiff.net/camping/"}}
1  spec/fixtures/repo.branches.js
@@ -0,0 +1 @@
+{"branches": {"master": "7b914ab1883de2e946238015503b8eacecd5b65b", "blog": "61448a57bc6fea5790fcc0457aa6b90cdfb92fff", "experimental": "dbd02e5ca9296e4b96f1ee71a9f1b93cb1cc9846"}}
1  spec/fixtures/repo.tags.js
@@ -0,0 +1 @@
+{"tags": {"1.3": "df6471ad409f859c4c8fba7f0d42c82de96a577f", "1.4": "7b9d87db3218a8de7727242f75fb3b200381aa60", "1.5": "696e702fffcc2bd93ab5471ac378c5b4ae063586", "1.4.2": "3302af538d449bd5e9e1389b9358b4a69693cfd8", "1.0": "30acba3edcec86074f122df93998a2f5eb3c01f4", "1.1": "a154c141634308e5c4acaadc38a88cae668ae861", "1.2": "2bd07da716585172c9a65d89e3e8e2d73275756b"}}
1  spec/fixtures/tree.basic.js
@@ -0,0 +1 @@
+{"tree": [{"name": "CHANGELOG", "sha": "7a13c1e272149df5dc7b7d5d0887e8e71e4328f9", "mode": "100644", "type": "blob"}, {"name": "COPYING", "sha": "94b6b84bfe0cdfc36e0a535fec9b2ce8eecdae75", "mode": "100644", "type": "blob"}, {"name": "README", "sha": "9e61400dbf0bf8b01dc2ba0792a6ad9d8cc8bf53", "mode": "100644", "type": "blob"}, {"name": "Rakefile", "sha": "4a05ecfe8ed50e3ec3aa156c1ee09f380bbb992f", "mode": "100644", "type": "blob"}, {"name": "bin", "sha": "2aae7685bbe85d5b4919f559c66040eaf926fe99", "mode": "040000", "type": "tree"}, {"name": "doc", "sha": "9d634eb47d9dcc5828ab93e47cb41110e5eb3fb9", "mode": "040000", "type": "tree"}, {"name": "examples", "sha": "734a4fba3c462e08dc28fa497b1f5792d3ab4c68", "mode": "040000", "type": "tree"}, {"name": "extras", "sha": "4a22d38e91087fbddfd1f68c8cabdfb53b8664bc", "mode": "040000", "type": "tree"}, {"name": "lib", "sha": "22124d8fc09bfc6d14b8ea6f06b8ed4b7bc4ca21", "mode": "040000", "type": "tree"}, {"name": "setup.rb", "sha": "8f2397ad909819d7bc9037030f990e7dc8034bc3", "mode": "100644", "type": "blob"}, {"name": "test", "sha": "21107a275cfc498d9e6bbb08cf0627f39305ef90", "mode": "040000", "type": "tree"}]}
1  spec/fixtures/user.basic.js
@@ -0,0 +1 @@
+{"user": {"name": "Magnus Holm", "company": "Fatguy", "following_count": 12, "public_gist_count": 6, "public_repo_count": 16, "blog": "http://judofyr.net", "id": 499, "followers_count": 14, "login": "judofyr", "location": "Norway", "email": "judofyr@gmail.com", "created_at": "2008/02/20 08:48:23 -0800"}}
1  spec/fixtures/user.followers.js
@@ -0,0 +1 @@
+{"users":["augustl","jontingvold","zmalltalker","nakajima","ichverstehe","runeb","jchupp","julik","rick2047","elliottcable","joshbuddy","Mikoangelo","rkh","matth"]}
1  spec/fixtures/user.following.js
@@ -0,0 +1 @@
+{"users":["why","37signals","Bluebie","jontingvold","irbno","runeb","fatguy","Feiring","Mikoangelo","yrashk","kitallis","simonask"]}
1  spec/fixtures/user.repos.js
@@ -0,0 +1 @@
+{"repositories": [{"description": "An extremely simple way to create a RubyGem", "forks": 0, "name": "gemify", "private": false, "url": "http://github.com/judofyr/gemify", "watchers": 7, "fork": false, "owner": "judofyr", "homepage": "http://dojo.rubyforge.org/gemify"}, {"description": "oEmbed for Ruby", "forks": 3, "name": "ruby-oembed", "private": false, "url": "http://github.com/judofyr/ruby-oembed", "watchers": 15, "fork": false, "owner": "judofyr", "homepage": "http://oembed.com/"}, {"description": "the 4k pocket full-of-gags web microframework", "forks": 2, "name": "camping", "private": false, "url": "http://github.com/judofyr/camping", "watchers": 16, "fork": true, "owner": "judofyr", "homepage": "http://code.whytheluckystiff.net/camping/"}, {"description": "Adding before/after-filters to Camping", "forks": 0, "name": "filtering_camping", "private": false, "url": "http://github.com/judofyr/filtering_camping", "watchers": 4, "fork": false, "owner": "judofyr", "homepage": ""}, {"description": "my patches to the junebug wiki", "forks": 1, "name": "junebug", "private": false, "url": "http://github.com/judofyr/junebug", "watchers": 2, "fork": true, "owner": "judofyr", "homepage": "http://junebugwiki.com/"}, {"description": "Skull+", "forks": 0, "name": "sofaskull", "private": false, "url": "http://github.com/judofyr/sofaskull", "watchers": 1, "fork": false, "owner": "judofyr", "homepage": "http://esoteric.voxelperfect.net/wiki/Skull_plus"}, {"description": "Rock Scissors Paper for Ruby", "forks": 0, "name": "rubyrps", "private": false, "url": "http://github.com/judofyr/rubyrps", "watchers": 1, "fork": true, "owner": "judofyr", "homepage": ""}, {"description": "Git + Hash", "forks": 0, "name": "gash", "private": false, "url": "http://github.com/judofyr/gash", "watchers": 31, "fork": false, "owner": "judofyr", "homepage": "http://dojo.rubyforge.org/gash"}, {"description": "Lightweight testing framework for Camping", "forks": 0, "name": "camping-test", "private": false, "url": "http://github.com/judofyr/camping-test", "watchers": 3, "fork": false, "owner": "judofyr", "homepage": ""}, {"description": "The Recursive Benchmark", "forks": 0, "name": "recursive", "private": false, "url": "http://github.com/judofyr/recursive", "watchers": 3, "fork": false, "owner": "judofyr", "homepage": "http://judofyr.github.com/recursive/"}, {"description": "Easily copy folders and files to other Git branches", "forks": 1, "name": "grancher", "private": false, "url": "http://github.com/judofyr/grancher", "watchers": 10, "fork": false, "owner": "judofyr", "homepage": ""}, {"description": "", "forks": 0, "name": "random", "private": false, "url": "http://github.com/judofyr/random", "watchers": 1, "fork": false, "owner": "judofyr", "homepage": ""}, {"description": "Ruby on Rails", "forks": 0, "name": "rails", "private": false, "url": "http://github.com/judofyr/rails", "watchers": 1, "fork": true, "owner": "judofyr", "homepage": "http://rubyonrails.org"}, {"description": "Nokogirl \u2014 because developers can't spell", "forks": 0, "name": "nokogirl", "private": false, "url": "http://github.com/judofyr/nokogirl", "watchers": 4, "fork": false, "owner": "judofyr", "homepage": "http://github.com/tenderlove/nokogiri"}, {"description": "Forte implementation in Ruby", "forks": 0, "name": "forter", "private": false, "url": "http://github.com/judofyr/forter", "watchers": 1, "fork": false, "owner": "judofyr", "homepage": "http://esolangs.org/wiki/Forte"}, {"description": "API Documentation for GitHub", "forks": 0, "name": "develop.github.com", "private": false, "url": "http://github.com/judofyr/develop.github.com", "watchers": 1, "fork": true, "owner": "judofyr", "homepage": "http://develop.github.com"}]}
46 spec/spec.core.js
@@ -0,0 +1,46 @@
+GitHub.Helpers.jsonp = function(url) {
+ alert('Attempting to call API: ' + url)
+}
+
+JSpec.addMatchers({
+ include_object : function(actual, expected) {
+ var eql = JSpec.matchers['eql'].match
+ for (var i in expected) {
+ var e = expected[i]
+ var a = actual[i]
+ if (!eql(a, e)) {
+ return false
+ }
+ }
+ return true
+ },
+ load : function(actual, expected) {
+ return actual.isLoaded(expected)
+ }
+})
+
+JSpec.context = {
+ mockJSONP: function(res, test_path) {
+ var old = GitHub.Helpers.jsonp
+ GitHub.Helpers.jsonp = function(url, callback) {
+ if (test_path) {
+ var path = url.substring(30, url.indexOf('?'))
+ JSpec.match(path, 'should', 'be', [test_path])
+ }
+ if (callback) {
+ callback(res)
+ }
+ GitHub.Helpers.jsonp = old
+ }
+ },
+ fixtures: {},
+ fixture: function(name) {
+ if (this.fixtures[name])
+ return this.fixtures[name]
+
+ var path = 'fixtures/' + name + '.js'
+ var content = eval('(' + JSpec.load(path) + ')')
+ this.fixtures[name] = content
+ return content
+ }
+}
40 spec/spec.helpers.js
@@ -0,0 +1,40 @@
+describe 'GitHub.Helpers'
+ it 'should have empty'
+ GitHub.Helpers.empty.should.be_a Function
+ GitHub.Helpers.empty.toString().should.be 'function () {}'
+ end
+
+ it 'should have first'
+ var obj = { main: 123 }
+ GitHub.Helpers.first(obj).should.be 123
+ end
+
+ it 'should have extend'
+ var base = { one: 1 }
+ var other = { two: 2, three: 3 }
+
+ GitHub.Helpers.extend(base, other)
+
+ base.one.should.be 1
+ base.should.include_object other
+ end
+
+ it 'should have indexOf'
+ var arr = ['one', 'two', 'three']
+
+ GitHub.Helpers.indexOf(arr, 'one').should.eql 0
+ GitHub.Helpers.indexOf(arr, 'two').should.eql 1
+ GitHub.Helpers.indexOf(arr, 'three').should.eql 2
+ GitHub.Helpers.indexOf(arr, 'four').should.eql -1
+ end
+
+ it 'should have remove'
+ var arr = ['one', 'two', 'three']
+
+ GitHub.Helpers.remove(arr, 1)
+ arr.should.eql ['one', 'three']
+
+ GitHub.Helpers.remove(arr, -1)
+ arr.should.eql ['one']
+ end
+end
28 spec/spec.html
@@ -0,0 +1,28 @@
+<html>
+ <head>
+ <title>GitHub.js specs</title>
+ <link type="text/css" rel="stylesheet" href="../vendor/jspec/jspec.css" />
+ <script src="../vendor/jspec/jspec.js"></script>
+ <script src="../vendor/jsclass/class.js" type="text/javascript"></script>
+ <script src="../src/github.js" type="text/javascript"></script>
+ <script>
+ function runSuites() {
+ JSpec
+ .exec('spec.core.js')
+ .exec('spec.helpers.js')
+ .exec('spec.loader.js')
+ .exec('spec.simple.js')
+ .exec('spec.user.js')
+ .exec('spec.repo.js')
+ .exec('spec.rest.js')
+ .run()
+ .report()
+ }
+ </script>
+ </head>
+ <body class="jspec" onLoad="runSuites();">
+ <div id="jspec-top"><h2 id="jspec-title">GitHub.js <em>JSpec <script>document.write(JSpec.version)</script></em></h2></div>
+ <div id="jspec"></div>
+ <div id="jspec-bottom"></div>
+ </body>
+</html>
78 spec/spec.loader.js
@@ -0,0 +1,78 @@
+describe 'GitHub.Loader'
+ var thing
+
+ before_each
+ thing = new JS.Singleton({
+ id: 0,
+ include: GitHub.Loader,
+ loaders: {
+ basic: function(done) {
+ this.id++
+ done()
+ },
+
+ another: function(done) {
+ this.id += 2
+ done()
+ }
+ }
+ })
+ end
+
+ it 'should load'
+ thing.load('basic')
+ thing.id.should.be 1
+ end
+
+ it 'should default to basic'
+ thing.load()
+ thing.id.should.be 1
+ end
+
+ it 'should allow more than one loaders'
+ thing.load('basic', 'another')
+ thing.id.should.be 3
+ end
+
+ it 'should allow a callback'
+ thing.load(function(obj) {
+ obj.should.be thing
+ thing.id.should.be 1
+ })
+
+ thing.load('another', function(obj) {
+ obj.should.be thing
+ thing.id.should.be 3
+ })
+ end
+
+ it 'should allow a callback with multiple loaders'
+ thing.load('basic', 'another', function(obj) {
+ obj.should.be thing
+ thing.id.should.be 3
+ })
+ end
+
+ it 'should not load twice'
+ thing.load('basic')
+ thing.id.should.be 1
+
+ thing.load('basic')
+ thing.id.should.be 1
+ end
+
+ it 'should reload'
+ thing.load('basic')
+ thing.reload('basic')
+ thing.id.should.be 2
+ end
+
+ it 'isLoaded() should work'
+ thing.should.not.load
+ thing.load()
+ thing.should.load
+ thing.should.load 'basic'
+ thing.load('another')
+ thing.should.load 'another'
+ end
+end
59 spec/spec.repo.js
@@ -0,0 +1,59 @@
+describe 'GitHub.Repo'
+ var repo
+
+ before_each
+ repo = new GitHub.Repo('judofyr', 'camping')
+ end
+
+ it 'should start with owner and name'
+ repo.owner.should.eql 'judofyr'
+ repo.name.should.eql 'camping'
+ end
+
+ it 'should load basic'
+ var obj = .fixture('repo.basic')
+ .mockJSONP(obj, 'repos/show/judofyr/camping')
+
+ repo.load(-{
+ repo.should.include_object obj.repositories
+ })
+ end
+
+ it 'should load branches'
+ var obj = .fixture('repo.branches')
+ .mockJSONP(obj, 'repos/show/judofyr/camping/branches')
+
+ repo.load('branches', -{
+ repo.branches.should.eql obj.branches
+ })
+ end
+
+ it 'should load tags'
+ var obj = .fixture('repo.tags')
+ .mockJSONP(obj, 'repos/show/judofyr/camping/tags')
+
+ repo.load('tags', -{
+ repo.tags.should.eql obj.tags
+ })
+ end
+
+ it 'should have commits'
+ end
+
+ it 'should have shortcuts to a commit'
+ var commit = repo.commit('e7719af8714a4141b2637648892c82198242c167')
+ commit.should.be_a GitHub.Commit
+ commit.repo.should.be repo
+ commit.sha.should.be 'e7719af8714a4141b2637648892c82198242c167'
+ end
+
+ it 'should have issues'
+ end
+
+ it 'should have shortcuts to a issue'
+ var issue = repo.issue(1)
+ issue.should.be_a GitHub.Issue
+ issue.repo.should.be repo
+ issue.number.should.be 1
+ end
+end
76 spec/spec.rest.js
@@ -0,0 +1,76 @@
+var repo = new GitHub.Repo('judofyr', 'camping')
+var commit = new GitHub.Commit(repo, 'e7719af8714a4141b2637648892c82198242c167')
+var tree = new GitHub.Tree(repo, '7b914ab1883de2e946238015503b8eacecd5b65b')
+var blob = new GitHub.Blob(tree, 'README')
+var issue = new GitHub.Issue(repo, 1)
+
+describe 'GitHub.Commit'
+ it 'should start with repo and sha'
+ commit.repo.should.be repo
+ commit.sha.should.eql 'e7719af8714a4141b2637648892c82198242c167'
+ end
+
+ it 'should load'
+ var obj = .fixture('commit.basic')
+ .mockJSONP(obj, 'commits/show/judofyr/camping/e7719af8714a4141b2637648892c82198242c167')
+
+ commit.load(-{
+ commit.should.include_object obj.commit
+ })
+ end
+end
+
+describe 'GitHub.Tree'
+ it 'should start with repo and sha'
+ tree.repo.should.be repo
+ tree.sha.should.eql '7b914ab1883de2e946238015503b8eacecd5b65b'
+ end
+
+ it 'should load'
+ var obj = .fixture('tree.basic')
+ .mockJSONP(obj, 'tree/show/judofyr/camping/7b914ab1883de2e946238015503b8eacecd5b65b')
+
+ tree.load(-{
+ for (var i in tree.children) {
+ var child = tree.children[i]
+ var real = obj[i]
+
+ [GitHub.Tree, GitHub.Blob].should.include child.klass
+
+ child.should.include_object real
+ }
+ })
+ end
+end
+
+describe 'GitHub.Blob'
+ it 'should start with parent and name'
+ blob.parent.should.be tree
+ blob.name.should.eql 'README'
+ end
+
+ it 'should load'
+ var obj = .fixture('blob.basic')
+ .mockJSONP(obj, 'blob/show/judofyr/camping/7b914ab1883de2e946238015503b8eacecd5b65b/README')
+
+ blob.load(-{
+ blob.should.include_object obj.blob
+ })
+ end
+end
+
+describe 'GitHub.Issue'
+ it 'should start with repo and number'
+ issue.repo.should.be repo
+ issue.number.should.be 1
+ end
+
+ it 'should load'
+ var obj = .fixture('issue.basic')
+ .mockJSONP(obj, 'issues/show/judofyr/camping/1')
+
+ issue.load(-{
+ issue.should.include_object obj.issue
+ })
+ end
+end
45 spec/spec.simple.js
@@ -0,0 +1,45 @@
+describe 'GitHub.Simple'
+ it 'should have call'
+ .mockJSONP(123, 'hello')
+
+ GitHub.Simple.call('hello', function(data) {
+ 123.should.be 123
+ })
+ end
+
+ it 'should have callset'
+ .mockJSONP({main: {one: 1, two: 2, three: 3}}, 'hello')
+ var obj = {}
+
+ GitHub.Simple.callset('hello', obj, function() {
+ obj.one.should.be 1
+ obj.two.should.be 2
+ obj.three.should.be 3
+ })
+ end
+
+ it 'should have calleach'
+ .mockJSONP({main:['one', 'two', 'three']}, 'hello')
+ var expected = ['one', 'two', 'three']
+
+ GitHub.Simple.calleach('hello', function(value) {
+ value.should.be expected.shift()
+ })
+ end
+
+ it 'should have callmap'
+ .mockJSONP({main:['one', 'two', 'three']}, 'hello')
+ var expected = ['one', 'two', 'three']
+ var id = 0
+ var done = function(array) {
+ array.should.eql [1, 2, 3]
+ }
+ var iter = function(value) {
+ value.should.be expected.shift()
+ id++
+ return id
+ }
+
+ GitHub.Simple.callmap('hello', iter, done)
+ end
+end
71 spec/spec.user.js
@@ -0,0 +1,71 @@
+describe 'GitHub.User'
+ var user;
+
+ before_each
+ user = new GitHub.User('judofyr')
+ end
+
+ it 'should start with login'
+ user.login.should.eql 'judofyr'
+ end
+
+ it 'should load basic'
+ var obj = .fixture('user.basic')
+ .mockJSONP(obj, 'user/show/judofyr')
+
+ user.load(-{
+ user.should.include_object obj.user
+ })
+ end
+
+ it 'should load followers'
+ var obj = .fixture('user.followers')
+ .mockJSONP(obj, 'user/show/judofyr/followers')
+
+ user.load('followers', -{
+ user.followers.should.eql obj.users
+ })
+ end
+
+ it 'should load following'
+ var obj = .fixture('user.following')
+ .mockJSONP(obj, 'user/show/judofyr/following')
+
+ user.load('following', -{
+ user.following.should.eql obj.users
+ })
+ end
+
+ it 'should load repos'
+ var obj = .fixture('user.repos')
+ .mockJSONP(obj, 'repos/show/judofyr')
+
+ user.load('repos', -{
+ user.repos.should.have_length obj.repositories.length
+
+ for (var i in user.repos) {
+ var repo = user.repos[i]
+ var real = obj.repositories[i]
+ repo.should.include_object real
+ }
+ })
+ end
+ it 'should return a loaded repo when possible'
+ var camping = user.repo('camping')
+ camping.should.not.load
+
+ var unknown = user.repo('blablabla')
+ unknown.should.not.load
+
+ var obj = .fixture('user.repos')
+ .mockJSONP(obj)
+
+ user.load('repos', -{
+ camping = user.repo('camping')
+ camping.should.load
+
+ unknown = user.repo('blablabla')
+ unknown.should.not.load
+ })
+ end
+end
408 src/github.js
@@ -0,0 +1,408 @@
+var GitHub = {};
+
+// Base uri
+GitHub.Base = 'http://github.com/api/v2/json/';
+
+GitHub.Helpers = {
+ empty: function(){},
+
+ indexOf: function(arr, obj) {
+ for (var i in arr) {
+ if (arr[i] == obj) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ remove: function(array, from, to) {
+ var rest = array.slice((to || from) + 1 || array.length);
+ array.length = from < 0 ? array.length + from : from;
+ return array.push.apply(array, rest);
+ },
+
+ first: function(object) {
+ for (var i in object) {
+ return object[i];
+ }
+ },
+
+ extend: function(target, object) {
+ for (var i in object) {
+ target[i] = object[i];
+ }
+ },
+
+ jsonp: function(url, callback, name) {
+ var head = document.getElementsByTagName("head")[0];
+ var script = document.createElement("script");
+ script.type = "text/javascript";
+ script.src = url;
+
+ window[name] = function(data) {
+ callback(data);
+ window[name] = undefined;
+ try{ delete window[name]; } catch(e){}
+ head.removeChild(script);
+ };
+
+ head.appendChild(script);
+ }
+};
+
+// Simple
+GitHub.Simple = new JS.Singleton({
+ jsonpid: 0,
+ call: function(url, query, func) {
+ if (!func) {
+ func = query;
+ query = null;
+ }
+
+ var name = 'githubjsonp' + this.jsonpid;
+ this.jsonpid++;
+
+ var query_string = '?callback=' + name;
+ if (query) {
+ for (var i in query) {
+ query_string += '&' + encodeURIComponent(i) + '=' + encodeURIComponent(query[i]);
+ }
+ }
+
+ var full_url = GitHub.Base + url + query_string;
+ GitHub.Helpers.jsonp(full_url, func, name);
+ },
+
+ callset: function(url, object, done) {
+ this.call(url, function(data) {
+ var first = GitHub.Helpers.first(data);
+
+ GitHub.Helpers.extend(object, first);
+
+ if (done) {
+ done();
+ }
+ });
+ },
+
+ calleach: function(url, iter, done) {
+ this.call(url, function(data) {
+ var first = GitHub.Helpers.first(data);
+
+ for (var i in first) {
+ iter(first[i]);
+ }
+
+ if (done) {
+ done();
+ }
+ });
+ },
+
+ callmap: function(url, iter, done) {
+ var result = [];
+ this.calleach(url, function(thing) {
+ result.push(iter(thing));
+ }, function() { done(result); });
+ }
+});
+
+// Loader
+GitHub.Loader = new JS.Module({
+ initialize: function() {
+ this.loaded_set = [];
+ },
+
+ load: function() {
+ var loaders = Array.prototype.slice.call(arguments);
+ var callback;
+
+ if (loaders[loaders.length - 1] instanceof Function) {
+ callback = loaders.pop();
+ }
+
+ if (loaders.length === 0) {
+ loaders.push('basic');
+ }
+
+ var done;
+ if (callback) {
+ var finished_loaders = 0,
+ all_loaders = loaders.length,
+ self = this;
+
+ done = function() {
+ finished_loaders += 1;
+ if (finished_loaders == all_loaders) {
+ callback(self);
+ }
+ };
+ } else {
+ done = GitHub.Helpers.empty;
+ }
+
+ for (var i in loaders) {
+ this.runLoader(loaders[i], done);
+ }
+ },
+
+ reload: function() {
+ var loaders = Array.prototype.slice.call(arguments);
+
+ if (loaders[loaders.length - 1] instanceof Function) {
+ loaders.pop();
+ }
+
+ if (loaders.length === 0) {
+ loaders.push('basic');
+ }
+
+ for (var i in loaders) {
+ var index = GitHub.Helpers.indexOf(this.loaded_set, loaders[i]);
+ if (index != -1) {
+ GitHub.Helpers.remove(this.loaded_set, index);
+ }
+ }
+
+ this.load.apply(this, arguments);
+ },
+
+ runLoader: function(loader, done) {
+ if (this.isLoaded(loader)) {
+ done();
+ } else {
+ var self = this;
+ this.loaders[loader].call(this, function() {
+ self.loaded(loader);
+ done();
+ });
+ }
+ },
+
+ isLoaded: function(loader) {
+ if (!loader) { loader = 'basic'; }
+ return GitHub.Helpers.indexOf(this.loaded_set, loader) != -1;
+ },
+
+ loaded: function(loader) {
+ this.loaded_set.push(loader);
+ }
+});
+
+GitHub.User = new JS.Class({
+ initialize: function(login) {
+ this.login = login;
+ this.callSuper();
+ },
+
+ repo: function(name) {
+ var res;
+ if (this.repos) {
+ for (var i in this.repos) {
+ var repo = this.repos[i];
+ if (repo.name == name) {
+ return repo;
+ }
+ }
+ }
+ return new GitHub.Repo(this.login, name);
+ },
+
+ include: GitHub.Loader,
+ loaders: {
+ basic: function(done) {
+ var url = 'user/show/' + this.login;
+ GitHub.Simple.callset(url, this, done);
+ },
+
+ followers: function(done) {
+ this.followers = [];
+ var url = 'user/show/' + this.login + '/followers';
+ GitHub.Simple.callset(url, this.followers, done);
+ },
+
+ following: function(done) {
+ this.following = [];
+ var url = 'user/show/' + this.login + '/following';
+ GitHub.Simple.callset(url, this.following, done);
+ },
+
+ repos: function(done) {
+ var repos = (this.repos = []);
+ var url = 'repos/show/' + this.login;
+ GitHub.Simple.calleach(url, function(value) {
+ var repo = new GitHub.Repo(value.owner, value.name);
+ GitHub.Helpers.extend(repo, value);
+ repo.loaded('basic');
+ repos.push(repo);
+ }, done);
+ }
+ }
+});
+
+GitHub.Repo = new JS.Class({
+ initialize: function(owner, name) {
+ this.owner = owner;
+ this.name = name;
+ this.path = owner + '/' + name;
+ this.callSuper();
+ },
+
+ issues: function(state, callback) {
+ var repo = this;
+ var url = 'issues/list/' + this.path + '/' + state;
+
+ GitHub.Simple.callmap(url, function(value) {
+ var issue = new GitHub.Issue(repo, value.number);
+ GitHub.Helpers.extend(issue, value);
+ issue.loaded('basic');
+ return issue;
+ }, callback);
+ },
+
+ issue: function(number) {
+ return new GitHub.Issue(this, number);
+ },
+
+ commits: function(branch, file, callback) {
+ var repo = this;
+ var url;
+
+ if (!callback) {
+ callback = file;
+ url = 'commits/list/' + this.path + '/' + branch;
+ } else {
+ url = 'commits/list/' + this.path + '/' + branch + '/' + file;
+ }
+
+ GitHub.Simple.callmap(url, function(value) {
+ var commit = new GitHub.Commit(repo, value.id);
+ GitHub.Helpers.extend(commit, value);
+ commit.loaded('basic');
+ return commit;
+ }, callback);
+ },
+
+ commit: function(sha) {
+ return new GitHub.Commit(this, sha);
+ },
+
+ tree: function(sha) {
+ return new GitHub.Tree(this, sha);
+ },
+
+ include: GitHub.Loader,
+ loaders: {
+ basic: function(done) {
+ var url = 'repos/show/' + this.path;
+ GitHub.Simple.callset(url, this, done);
+ },
+
+ tags: function(done) {
+ this.tags = {};
+ var url = 'repos/show/' + this.path + '/tags';
+ GitHub.Simple.callset(url, this.tags, done);
+ },
+
+ branches: function(done) {
+ this.branches = {};
+ var url = 'repos/show/' + this.path + '/branches';
+ GitHub.Simple.callset(url, this.branches, done);
+ }
+ }
+});
+
+var commit_loader = function(done) {
+ var commit = this;
+ var url = 'commits/show/' + this.path;
+ GitHub.Simple.callset(url, this, function(){
+ done();
+
+ if (!commit.isLoaded('basic')) {
+ commit.loaded('basic');
+ }
+
+ if (!commit.isLoaded('detailed')) {
+ commit.loaded('detailed');
+ }
+ });
+};
+
+GitHub.Commit = new JS.Class({
+ initialize: function(repo, sha) {
+ this.repo = repo;
+ this.sha = sha;
+ this.path = this.repo.path + '/' + this.sha;
+ this.callSuper();
+ },
+
+ include: GitHub.Loader,
+ loaders: {
+ basic: commit_loader,
+ detailed: commit_loader,
+ }
+});
+
+GitHub.Tree = new JS.Class({
+ initialize: function(repo, sha) {
+ this.repo = repo;
+ this.sha = sha;
+ this.path = this.repo.path + '/' + this.sha;
+ this.callSuper();
+ },
+
+ include: GitHub.Loader,
+ loaders: {
+ basic: function(done) {
+ var children = (this.children = []);
+ var repo = this.repo;
+ var parent = this;
+ var url = 'tree/show/' + this.path;
+
+ GitHub.Simple.calleach(url, function(value) {
+ var res;
+ if (value.type == 'tree') {
+ res = new GitHub.Tree(repo, value.sha);
+ } else {
+ res = new GitHub.Blob(parent, value.name);
+ }
+ GitHub.Helpers.extend(res, value);
+ children.push(res);
+ }, done);
+ }
+ }
+});
+
+GitHub.Blob = new JS.Class({
+ initialize: function(parent, name) {
+ this.parent = parent;
+ this.name = name;
+ this.path = this.parent.path + '/' + this.name;
+ this.callSuper();
+ },
+
+ include: GitHub.Loader,
+ loaders: {
+ basic: function(done) {
+ var url = 'blob/show/' + this.path;
+ GitHub.Simple.callset(url, this, done);
+ }
+ }
+});
+
+GitHub.Issue = new JS.Class({
+ initialize: function(repo, number) {
+ this.repo = repo;
+ this.number = number;
+ this.path = this.repo.path + '/' + this.number;
+ this.callSuper();
+ },
+
+ include: GitHub.Loader,
+ loaders: {
+ basic: function(done) {
+ var url = 'issues/show/' + this.path;
+ GitHub.Simple.callset(url, this, done);
+ }
+ }
+});
1  vendor/jsclass/class.js
@@ -0,0 +1 @@
+JS={extend:function(a,b){b=b||{};for(var c in b){if(a[c]===b[c])continue;a[c]=b[c]}return a},makeFunction:function(){return function(){return this.initialize?(this.initialize.apply(this,arguments)||this):this}},makeBridge:function(a){var b=function(){};b.prototype=a.prototype;return new b},delegate:function(a,b){return function(){return this[a][b].apply(this[a],arguments)}},bind:function(){var a=JS.array(arguments),b=a.shift(),c=a.shift()||null;return function(){return b.apply(c,a.concat(JS.array(arguments)))}},callsSuper:function(a){return a.SUPER===undefined?a.SUPER=/\bcallSuper\b/.test(a.toString()):a.SUPER},mask:function(a){var b=a.toString().replace(/callSuper/g,'super');a.toString=function(){return b};return a},array:function(a){if(!a)return[];if(a.toArray)return a.toArray();var b=a.length,c=[];while(b--)c[b]=a[b];return c},indexOf:function(a,b){for(var c=0,d=a.length;c<d;c++){if(a[c]===b)return c}return-1},isFn:function(a){return a instanceof Function},ignore:function(a,b){return/^(include|extend)$/.test(a)&&typeof b==='object'}};JS.Module=JS.makeFunction();JS.extend(JS.Module.prototype,{initialize:function(a,b){b=b||{};this.__mod__=this;this.__inc__=[];this.__fns__={};this.__dep__=[];this.__res__=b._1||null;this.include(a||{})},define:function(a,b,c){c=c||{};this.__fns__[a]=b;if(JS.Module._0&&c._0&&JS.isFn(b))JS.Module._0(a,c._0);var d=this.__dep__.length;while(d--)this.__dep__[d].resolve()},instanceMethod:function(a){var b=this.lookup(a).pop();return JS.isFn(b)?b:null},include:function(a,b,c){if(!a)return c&&this.resolve();b=b||{};var d=a.include,f=a.extend,e,g,j,h,i=b._4||this;if(a.__inc__&&a.__fns__){this.__inc__.push(a);a.__dep__.push(this);if(b._2)a.extended&&a.extended(b._2);else a.included&&a.included(i)}else{if(b._5){for(h in a){if(JS.ignore(h,a[h]))continue;this.define(h,a[h],{_0:i||b._2||this})}}else{if(typeof d==='object'){e=[].concat(d);for(g=0,j=e.length;g<j;g++)i.include(e[g],b)}if(typeof f==='object'){e=[].concat(f);for(g=0,j=e.length;g<j;g++)i.extend(e[g],false);i.extend()}b._5=true;return i.include(a,b,c)}}c&&this.resolve()},includes:function(a){if(Object===a||this===a||this.__res__===a.prototype)return true;var b=this.__inc__.length;while(b--){if(this.__inc__[b].includes(a))return true}return false},ancestors:function(a){a=a||[];for(var b=0,c=this.__inc__.length;b<c;b++)this.__inc__[b].ancestors(a);var d=(this.__res__||{}).klass,f=(d&&this.__res__===d.prototype)?d:this;if(JS.indexOf(a,f)===-1)a.push(f);return a},lookup:function(a){var b=this.ancestors(),c=[],d,f,e;for(d=0,f=b.length;d<f;d++){e=b[d].__mod__.__fns__[a];if(e)c.push(e)}return c},make:function(a,b){if(!JS.isFn(b)||!JS.callsSuper(b))return b;var c=this;return function(){return c.chain(this,a,arguments)}},chain:JS.mask(function(c,d,f){var e=this.lookup(d),g=e.length-1,j=c.callSuper,h=JS.array(f),i;c.callSuper=function(){var a=arguments.length;while(a--)h[a]=arguments[a];g-=1;var b=e[g].apply(c,h);g+=1;return b};i=e.pop().apply(c,h);j?c.callSuper=j:delete c.callSuper;return i}),resolve:function(a){var a=a||this,b=a.__res__,c,d,f,e;if(a===this){c=this.__dep__.length;while(c--)this.__dep__[c].resolve()}if(!b)return;for(c=0,d=this.__inc__.length;c<d;c++)this.__inc__[c].resolve(a);for(f in this.__fns__){e=a.make(f,this.__fns__[f]);if(b[f]!==e)b[f]=e}}});JS.ObjectMethods=new JS.Module({__eigen__:function(){if(this.__meta__)return this.__meta__;var a=this.__meta__=new JS.Module({},{_1:this});a.include(this.klass.__mod__);return a},extend:function(a,b){return this.__eigen__().include(a,{_2:this},b!==false)},isA:function(a){return this.__eigen__().includes(a)},method:function(a){var b=this,c=b.__mcache__=b.__mcache__||{};if((c[a]||{}).fn===b[a])return c[a].bd;return(c[a]={fn:b[a],bd:JS.bind(b[a],b)}).bd}});JS.Class=JS.makeFunction();JS.extend(JS.Class.prototype=JS.makeBridge(JS.Module),{initialize:function(a,b){var c=JS.extend(JS.makeFunction(),this);c.klass=c.constructor=this.klass;if(!JS.isFn(a)){b=a;a=Object}c.inherit(a);c.include(b,null,false);c.resolve();do{a.inherited&&a.inherited(c)}while(a=a.superclass);return c},inherit:function(a){this.superclass=a;if(this.__eigen__){this.__eigen__().include(a.__eigen__?a.__eigen__():new JS.Module(a.prototype));this.__meta__.resolve()}this.subclasses=[];(a.subclasses||[]).push(this);var b=this.prototype=JS.makeBridge(a);b.klass=b.constructor=this;this.__mod__=new JS.Module({},{_1:this.prototype});this.include(JS.ObjectMethods,null,false);if(a!==Object)this.include(a.__mod__||new JS.Module(a.prototype,{_1:a.prototype}),null,false)},include:function(a,b,c){if(!a)return;var d=this.__mod__,b=b||{};b._4=this;return d.include(a,b,c!==false)},extend:function(a){if(!this.callSuper)return;this.callSuper();var b=this.subclasses.length;while(b--)this.subclasses[b].extend()},define:function(){var a=this.__mod__;a.define.apply(a,arguments);a.resolve()},includes:JS.delegate('__mod__','includes'),ancestors:JS.delegate('__mod__','ancestors'),resolve:JS.delegate('__mod__','resolve')});JS.Module=JS.extend(new JS.Class(JS.Module.prototype),JS.ObjectMethods.__fns__);JS.Module.include(JS.ObjectMethods);JS.Class=JS.extend(new JS.Class(JS.Module,JS.Class.prototype),JS.ObjectMethods.__fns__);JS.Module.klass=JS.Module.constructor=JS.Class.klass=JS.Class.constructor=JS.Class;JS.Module.extend({_3:[],methodAdded:function(a,b){this._3.push([a,b])},_0:function(a,b){var c=this._3,d=c.length;while(d--)c[d][0].call(c[d][1]||null,a,b)}});JS.extend(JS,{Interface:new JS.Class({initialize:function(d){this.test=function(a,b){var c=d.length;while(c--){if(!JS.isFn(a[d[c]]))return b?d[c]:false}return true}},extend:{ensure:function(){var a=JS.array(arguments),b=a.shift(),c,d;while(c=a.shift()){d=c.test(b,true);if(d!==true)throw new Error('object does not implement '+d+'()');}}}}),Singleton:new JS.Class({initialize:function(a,b){return new(new JS.Class(a,b))}})});
205 vendor/jsmin.rb
@@ -0,0 +1,205 @@
+#!/usr/bin/ruby
+# jsmin.rb 2007-07-20
+# Author: Uladzislau Latynski
+# This work is a translation from C to Ruby of jsmin.c published by
+# Douglas Crockford. Permission is hereby granted to use the Ruby
+# version under the same conditions as the jsmin.c on which it is
+# based.
+#
+# /* jsmin.c
+# 2003-04-21
+#
+# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is furnished to do
+# so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# The Software shall be used for Good, not Evil.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+EOF = -1
+$theA = ""
+$theB = ""
+
+# isAlphanum -- return true if the character is a letter, digit, underscore,
+# dollar sign, or non-ASCII character
+def isAlphanum(c)
+ return false if !c || c == EOF
+ return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
+ (c >= 'A' && c <= 'Z') || c == '_' || c == '$' ||
+ c == '\\' || c[0] > 126)
+end
+
+# get -- return the next character from stdin. Watch out for lookahead. If
+# the character is a control character, translate it to a space or linefeed.
+def get()
+ c = $stdin.getc
+ return EOF if(!c)
+ c = c.chr
+ return c if (c >= " " || c == "\n" || c.unpack("c") == EOF)
+ return "\n" if (c == "\r")
+ return " "
+end
+
+# Get the next character without getting it.
+def peek()
+ lookaheadChar = $stdin.getc
+ $stdin.ungetc(lookaheadChar)
+ return lookaheadChar.chr
+end
+
+# mynext -- get the next character, excluding comments.
+# peek() is used to see if a '/' is followed by a '/' or '*'.
+def mynext()
+ c = get
+ if (c == "/")
+ if(peek == "/")
+ while(true)
+ c = get
+ if (c <= "\n")
+ return c
+ end
+ end
+ end
+ if(peek == "*")
+ get
+ while(true)
+ case get
+ when "*"
+ if (peek == "/")
+ get
+ return " "
+ end
+ when EOF
+ raise "Unterminated comment"
+ end
+ end
+ end
+ end
+ return c
+end
+
+
+# action -- do something! What you do is determined by the argument: 1
+# Output A. Copy B to A. Get the next B. 2 Copy B to A. Get the next B.
+# (Delete A). 3 Get the next B. (Delete B). action treats a string as a
+# single character. Wow! action recognizes a regular expression if it is
+# preceded by ( or , or =.
+def action(a)
+ if(a==1)
+ $stdout.write $theA
+ end
+ if(a==1 || a==2)
+ $theA = $theB
+ if ($theA == "\'" || $theA == "\"")
+ while (true)
+ $stdout.write $theA
+ $theA = get
+ break if ($theA == $theB)
+ raise "Unterminated string literal" if ($theA <= "\n")
+ if ($theA == "\\")
+ $stdout.write $theA
+ $theA = get
+ end
+ end
+ end
+ end
+ if(a==1 || a==2 || a==3)
+ $theB = mynext
+ if ($theB == "/" && ($theA == "(" || $theA == "," || $theA == "=" ||
+ $theA == ":" || $theA == "[" || $theA == "!" ||
+ $theA == "&" || $theA == "|" || $theA == "?" ||
+ $theA == "{" || $theA == "}" || $theA == ";" ||
+ $theA == "\n"))
+ $stdout.write $theA
+ $stdout.write $theB
+ while (true)
+ $theA = get
+ if ($theA == "/")
+ break
+ elsif ($theA == "\\")
+ $stdout.write $theA
+ $theA = get
+ elsif ($theA <= "\n")
+ raise "Unterminated RegExp Literal"
+ end
+ $stdout.write $theA
+ end
+ $theB = mynext
+ end
+ end
+end
+
+# jsmin -- Copy the input to the output, deleting the characters which are
+# insignificant to JavaScript. Comments will be removed. Tabs will be
+# replaced with spaces. Carriage returns will be replaced with linefeeds.
+# Most spaces and linefeeds will be removed.
+def jsmin
+ $theA = "\n"
+ action(3)
+ while ($theA != EOF)
+ case $theA
+ when " "
+ if (isAlphanum($theB))
+ action(1)
+ else
+ action(2)
+ end
+ when "\n"
+ case ($theB)
+ when "{","[","(","+","-"
+ action(1)
+ when " "
+ action(3)
+ else
+ if (isAlphanum($theB))
+ action(1)
+ else
+ action(2)
+ end
+ end
+ else
+ case ($theB)
+ when " "
+ if (isAlphanum($theA))
+ action(1)
+ else
+ action(3)
+ end
+ when "\n"
+ case ($theA)
+ when "}","]",")","+","-","\"","\\", "'", '"'
+ action(1)
+ else
+ if (isAlphanum($theA))
+ action(1)
+ else
+ action(3)
+ end
+ end
+ else
+ action(1)
+ end
+ end
+ end
+end
+
+ARGV.each do |anArg|
+ $stdout.write "// #{anArg}\n"
+end
+
+jsmin
BIN  vendor/jspec/images/bg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  vendor/jspec/images/hr.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  vendor/jspec/images/sprites.bg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  vendor/jspec/images/sprites.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  vendor/jspec/images/vr.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
130 vendor/jspec/jspec.css
@@ -0,0 +1,130 @@
+body.jspec {
+ margin: 45px 0;
+ text-align: center;
+ font: 12px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+ background: #efefef url(images/bg.png) top left repeat-x;
+}
+#jspec {
+ margin: 0 auto;
+ padding-top: 25px;
+ width: 1008px;
+ background: url(images/vr.png) top left repeat-y;
+ text-align: left;
+}
+#jspec-top {
+ position: relative;
+ margin: 0 auto;
+ width: 1008px;
+ height: 40px;
+ background: url(images/sprites.bg.png) top left no-repeat;
+}
+#jspec-bottom {
+ margin: 0 auto;
+ width: 1008px;
+ height: 15px;
+ background: url(images/sprites.bg.png) bottom left no-repeat;
+}
+#jspec-title {
+ position: relative;
+ top: 35px;
+ left: 20px;
+ font-size: 22px;
+ font-weight: normal;
+ text-align: left;
+ padding-left: 40px;
+ background: url(images/sprites.png) 0 -126px no-repeat;
+}
+#jspec-title em {
+ font-size: 10px;
+ font-style: normal;
+ color: #BCC8D1;
+}
+#jspec-report * {
+ margin: 0;
+ padding: 0;
+ background: none;
+ border: none;
+}
+#jspec-report {
+ padding: 15px 40px;
+ font: 11px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+ color: #7B8D9B;
+}
+#jspec-report.has-failures {
+ padding-bottom: 30px;
+}
+#jspec-report .hidden {
+ display: none;
+}
+#jspec-report .heading {
+ margin-bottom: 15px;
+}
+#jspec-report .heading span {
+ padding-right: 10px;
+}
+#jspec-report .heading .passes em {
+ color: #0ea0eb;
+}
+#jspec-report .heading .failures em {
+ color: #FA1616;
+}
+#jspec-report table {
+ width: 100%;
+ font-size: 11px;
+ border-collapse: collapse;
+}
+#jspec-report td {
+ padding: 8px;
+ text-indent: 30px;
+ color: #7B8D9B;
+}
+#jspec-report tr td:first-child em {
+ font-style: normal;
+ font-weight: normal;
+ color: #7B8D9B;
+}
+#jspec-report tr:not(.description):hover {
+ text-shadow: 1px 1px 1px #fff;
+ background: #F2F5F7;
+}
+#jspec-report td + td {
+ padding-right: 0;
+ width: 15px;
+}
+#jspec-report td.pass {
+ background: url(images/sprites.png) 3px -7px no-repeat;
+}
+#jspec-report td.fail {
+ background: url(images/sprites.png) 3px -47px no-repeat;
+ font-weight: bold;
+ color: #FC0D0D;
+}
+#jspec-report td.requires-implementation {
+ background: url(images/sprites.png) 3px -87px no-repeat;
+}
+#jspec-report tr.description td {
+ margin-top: 25px;
+ padding-top: 25px;
+ font-size: 12px;
+ font-weight: bold;
+ text-indent: 0;
+ color: #1a1a1a;
+}
+#jspec-report tr.description:first-child td {
+ border-top: none;
+}
+#jspec-report .assertion {
+ display: block;
+ float: left;
+ margin: 0 0 0 1px;
+ padding: 0;
+ width: 1px;
+ height: 5px;
+ background: #7B8D9B;
+}
+#jspec-report .assertion.failed {
+ background: red;
+}
+.jspec-sandbox {
+ display: none;
+}
1,084 vendor/jspec/jspec.js
@@ -0,0 +1,1084 @@
+
+// JSpec - Core - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
+
+(function(){
+
+ JSpec = {
+
+ version : '1.1.5',
+ file : '',
+ suites : [],
+ matchers : {},
+ stats : { specs : 0, assertions : 0, failures : 0, passes : 0 },
+ options : { profile : false },
+
+ /**
+ * Default context in which bodies are evaluated.
+ * This allows specs and hooks to use the 'this' keyword in
+ * order to store variables, as well as allowing the context
+ * to provide helper methods or properties.
+ *
+ * Replace context simply by setting JSpec.context
+ * to your own like below:
+ *
+ * JSpec.context = { foo : 'bar' }
+ *
+ * Contexts can be changed within any body, this can be useful
+ * in order to provide specific helper methods to specific suites.
+ *
+ * To reset (usually in after hook) simply set to null like below:
+ *
+ * JSpec.context = null
+ *
+ */
+
+ defaultContext : {
+ sandbox : function(name) {
+ sandbox = document.createElement('div')
+ sandbox.setAttribute('class', 'jspec-sandbox')
+ document.body.appendChild(sandbox)
+ return sandbox
+ }
+ },
+
+ // --- Objects
+
+ formatters : {
+
+ /**
+ * Default formatter, outputting to the DOM.
+ *
+ * Options:
+ * - reportToId id of element to output reports to, defaults to 'jspec'
+ * - failuresOnly displays only suites with failing specs
+ *
+ * @api public
+ */
+
+ DOM : function(results, options) {
+ id = option('reportToId') || 'jspec'
+ report = document.getElementById(id)
+ failuresOnly = option('failuresOnly')
+ classes = results.stats.failures ? 'has-failures' : ''
+ if (!report) error('requires the element #' + id + ' to output its reports')
+
+ markup =
+ '<div id="jspec-report" class="' + classes + '"><div class="heading"> \
+ <span class="passes">Passes: <em>' + results.stats.passes + '</em></span> \
+ <span class="failures">Failures: <em>' + results.stats.failures + '</em></span> \
+ </div><table class="suites">'
+
+ renderSuite = function(suite) {
+ displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
+ if (displaySuite && suite.hasSpecs()) {
+ markup += '<tr class="description"><td colspan="2">' + suite.description + '</td></tr>'
+ each(suite.specs, function(i, spec){
+ markup += '<tr class="' + (i % 2 ? 'odd' : 'even') + '">'
+ if (spec.requiresImplementation())
+ markup += '<td class="requires-implementation" colspan="2">' + spec.description + '</td>'
+ else if (spec.passed() && !failuresOnly)
+ markup += '<td class="pass">' + spec.description+ '</td><td>' + spec.assertionsGraph() + '</td>'
+ else if(!spec.passed())
+ markup += '<td class="fail">' + spec.description + ' <em>' + spec.failure().message + '</em>' + '</td><td>' + spec.assertionsGraph() + '</td>'
+ markup += '<tr class="body" style="display: none;"><td colspan="2">' + spec.body + '</td></tr>'
+ })
+ markup += '</tr>'
+ }
+ }
+
+ renderSuites = function(suites) {
+ each(suites, function(suite){
+ renderSuite(suite)
+ if (suite.hasSuites()) renderSuites(suite.suites)
+ })
+ }
+
+ renderSuites(results.suites)
+
+ markup += '</table></div>'
+
+ report.innerHTML = markup
+ },
+
+ /**
+ * Terminal formatter.
+ *
+ * @api public
+ */
+
+ Terminal : function(results, options) {
+ failuresOnly = option('failuresOnly')
+ puts(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') +
+ color(" Failures: ", 'bold') + color(results.stats.failures, 'red') + "\n")
+
+ indent = function(string) {
+ return string.replace(/^(.)/gm, ' $1')
+ }
+
+ renderSuite = function(suite) {
+ displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran
+ if (displaySuite && suite.hasSpecs()) {
+ puts(color(' ' + suite.description, 'bold'))
+ results.each(suite.specs, function(spec){
+ assertionsGraph = inject(spec.assertions, '', function(graph, assertion){
+ return graph + color('.', assertion.passed ? 'green' : 'red')
+ })
+ if (spec.requiresImplementation())
+ puts(color(' ' + spec.description, 'blue') + assertionsGraph)
+ else if (spec.passed() && !failuresOnly)
+ puts(color(' ' + spec.description, 'green') + assertionsGraph)
+ else
+ puts(color(' ' + spec.description, 'red') + assertionsGraph +
+ "\n" + indent(spec.failure().message) + "\n")
+ })
+ puts('')
+ }
+ }
+
+ renderSuites = function(suites) {
+ each(suites, function(suite){
+ renderSuite(suite)
+ if (suite.hasSuites()) renderSuites(suite.suites)
+ })
+ }
+
+ renderSuites(results.suites)
+ },
+
+ /**
+ * Console formatter, tested with Firebug and Safari 4.
+ *
+ * @api public
+ */
+
+ Console : function(results, options) {
+ console.log('')
+ console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures)
+
+ renderSuite = function(suite) {
+ if (suite.ran) {
+ console.group(suite.description)
+ results.each(suite.specs, function(spec){
+ assertionCount = spec.assertions.length + ':'
+ if (spec.requiresImplementation())
+ console.warn(spec.description)
+ else if (spec.passed())
+ console.log(assertionCount + ' ' + spec.description)
+ else
+ console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message)
+ })
+ console.groupEnd()
+ }
+ }
+
+ renderSuites = function(suites) {
+ each(suites, function(suite){
+ renderSuite(suite)
+ if (suite.hasSuites()) renderSuites(suite.suites)
+ })
+ }
+
+ renderSuites(results.suites)
+ }
+ },
+
+ Assertion : function(matcher, actual, expected, negate) {
+ extend(this, {
+ message : '',
+ passed : false,
+ actual : actual,
+ negate : negate,
+ matcher : matcher,
+ expected : expected,
+ record : function(result) {
+ result ? JSpec.stats.passes++ : JSpec.stats.failures++
+ },
+
+ exec : function() {
+ // TODO: remove unshifting of expected
+ expected.unshift(actual == null ? null : actual.valueOf())
+ result = matcher.match.apply(JSpec, expected)
+ this.passed = negate ? !result : result
+ this.record(this.passed)
+ if (!this.passed) this.message = matcher.message(actual, expected, negate, matcher.name)
+ return this
+ }
+ })
+ },
+
+ /**
+ * Specification Suite block object.
+ *
+ * @param {string} description
+ * @param {function} body
+ * @api private
+ */
+
+ Suite : function(description, body) {
+ extend(this, {
+ body: body,
+ description: description,
+ suites: [],
+ specs: [],
+ ran: false,
+ hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] },
+
+ // Add a spec to the suite
+
+ it : function(description, body) {
+ spec = new JSpec.Spec(description, body)
+ this.specs.push(spec)
+ spec.suite = this
+ },
+
+ // Add a hook to the suite
+
+ addHook : function(hook, body) {
+ this.hooks[hook].push(body)
+ },
+
+ // Add a nested suite
+
+ describe : function(description, body) {
+ suite = new JSpec.Suite(description, body)
+ suite.description = this.description + ' ' + suite.description
+ this.suites.push(suite)
+ suite.suite = this
+ },
+
+ // Invoke a hook in context to this suite
+
+ hook : function(hook) {
+ if (this.suite) this.suite.hook(hook)
+ each(this.hooks[hook], function(body) {
+ JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + this.description + "': ")
+ })
+ },
+
+ // Check if nested suites are present
+
+ hasSuites : function() {
+ return this.suites.length
+ },
+
+ // Check if this suite has specs
+
+ hasSpecs : function() {
+ return this.specs.length
+ },
+
+ // Check if the entire suite passed
+
+ passed : function() {
+ var passed = true
+ each(this.specs, function(spec){
+ if (!spec.passed()) passed = false
+ })
+ return passed
+ }
+ })
+ },
+
+ /**
+ * Specification block object.
+ *
+ * @param {string} description
+ * @param {function} body
+ * @api private
+ */
+
+ Spec : function(description, body) {
+ extend(this, {
+ body : body,
+ description : description,
+ assertions : [],
+
+ // Find first failing assertion
+
+ failure : function() {
+ return inject(this.assertions, null, function(failure, assertion){
+ return !assertion.passed && !failure ? assertion : failure
+ })
+ },
+
+ // Find all failing assertions
+
+ failures : function() {
+ return inject(this.assertions, [], function(failures, assertion){
+ if (!assertion.passed) failures.push(assertion)
+ return failures
+ })
+ },
+
+ // Weither or not the spec passed
+
+ passed : function() {
+ return !this.failure()
+ },
+
+ // Weither or not the spec requires implementation (no assertions)
+
+ requiresImplementation : function() {
+ return this.assertions.length == 0
+ },
+
+ // Sprite based assertions graph
+
+ assertionsGraph : function() {
+ return map(this.assertions, function(assertion){
+ return '<span class="assertion ' + (assertion.passed ? 'passed' : 'failed') + '"></span>'
+ }).join('')
+ }
+ })
+ },
+
+ // --- Methods
+
+ /**
+ * Return ANSI-escaped colored string.
+ *
+ * @param {string} string
+ * @param {string} color
+ * @return {string}
+ * @api public
+ */
+
+ color : function(string, color) {
+ return "\u001B[" + {
+ bold : 1,
+ black : 30,
+ red : 31,
+ green : 32,
+ yellow : 33,
+ blue : 34,
+ magenta : 35,
+ cyan : 36,
+ white : 37,
+ }[color] + 'm' + string + "\u001B[0m"
+ },
+
+ /**
+ * Default matcher message callback.
+ *
+ * @api private
+ */
+
+ defaultMatcherMessage : function(actual, expected, negate, name) {
+ return 'expected ' + print(actual) + ' to ' +
+ (negate ? 'not ' : '') +
+ name.replace(/_/g, ' ') +
+ ' ' + print.apply(this, expected.slice(1))
+ },
+
+ /**
+ * Normalize a matcher message.
+ *
+ * When no messge callback is present the defaultMatcherMessage
+ * will be assigned, will suffice for most matchers.
+ *
+ * @param {hash} matcher
+ * @return {hash}
+ * @api public
+ */
+
+ normalizeMatcherMessage : function(matcher) {
+ if (typeof matcher.message != 'function')
+ matcher.message = this.defaultMatcherMessage
+ return matcher
+ },
+
+ /**
+ * Normalize a matcher body
+ *
+ * This process allows the following conversions until
+ * the matcher is in its final normalized hash state.
+ *
+ * - '==' becomes 'actual == expected'
+ * - 'actual == expected' becomes 'return actual == expected'
+ * - function(actual, expected) { return actual == expected } becomes
+ * { match : function(actual, expected) { return actual == expected }}
+ *
+ * @param {mixed} body
+ * @return {hash}
+ * @api public
+ */
+
+ normalizeMatcherBody : function(body) {
+ switch (body.constructor) {
+ case String:
+ if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)]
+ if (body.length < 4) body = 'actual ' + body + ' expected'
+ return { match : function(actual, expected) { return eval(body) }}
+
+ case Function:
+ return { match : body }
+
+ default:
+ return body
+ }
+ },
+
+ /**
+ * Get option value. This method first checks if
+ * the option key has been set via the query string,
+ * otherwise returning the options hash value.
+ *
+ * @param {string} key
+ * @return {mixed}
+ * @api public
+ */
+
+ option : function(key) {
+ return (value = query(key)) !== null ? value :
+ JSpec.options[key] || null
+ },
+
+ /**
+ * Generates a hash of the object passed.
+ *
+ * @param {object} object
+ * @return {string}
+ * @api private
+ */
+
+ hash : function(object) {
+ serialize = function(prefix) {
+ return inject(object, prefix + ':', function(buffer, key, value){
+ return buffer += hash(value)
+ })
+ }
+ switch (object.constructor) {
+ case Array: return serialize('a')
+ case Object: return serialize('o')
+ case RegExp: return 'r:' + object.toString()
+ case Number: return 'n:' + object.toString()
+ case String: return 's:' + object.toString()
+ default: return object.toString()
+ }
+ },
+
+ /**
+ * Return last element of an array.
+ *
+ * @param {array} array
+ * @return {object}
+ * @api public
+ */
+
+ last : function(array) {
+ return array[array.length - 1]
+ },
+
+ /**
+ * Convert object(s) to a print-friend string.
+ *
+ * @param {object, ...} object
+ * @return {string}
+ * @api public
+ */
+
+ print : function(object) {
+ if (arguments.length > 1) {
+ list = []
+ for (i = 0; i < arguments.length; i++) list.push(print(arguments[i]))
+ return list.join(', ')
+ }
+ if (object === undefined) return ''
+ if (object === null) return 'null'
+ if (object === true) return 'true'
+ if (object === false) return 'false'
+ if (object.jquery && object.selector.length > 0) return 'selector ' + print(object.selector) + ''
+ if (object.jquery) return escape(object.html())
+ if (object.nodeName) return escape(object.outerHTML)
+ switch (object.constructor) {
+ case String: return "'" + escape(object) + "'"
+ case Number: return object
+ case Array :
+ buff = '['
+ each(object, function(v){ buff += ', ' + print(v) })
+ return buff.replace('[,', '[') + ' ]'
+ case Object:
+ buff = '{'
+ each(object, function(k, v){ buff += ', ' + print(k) + ' : ' + print(v)})
+ return buff.replace('{,', '{') + ' }'
+ default:
+ return escape(object.toString())
+ }
+ },
+
+ /**
+ * Escape HTML.
+ *
+ * @param {string} html
+ * @return {string}
+ * @api public
+ */
+
+ escape : function(html) {
+ if (typeof html != 'string') return html
+ return html.
+ replace(/&/gmi, '&amp;').
+ replace(/"/gmi, '&quot;').
+ replace(/>/gmi, '&gt;').
+ replace(/</gmi, '&lt;')
+ },
+
+ /**
+ * Invoke a matcher.
+ *
+ * this.match('test', 'should', 'be_a', [String])
+ *
+ * @param {object} actual
+ * @param {bool, string} negate
+ * @param {string} name
+ * @param {array} expected
+ * @return {bool}
+ * @api private
+ */
+
+ match : function(actual, negate, name, expected) {
+ if (typeof negate == 'string') negate = negate == 'should' ? false : true
+ assertion = new JSpec.Assertion(this.matchers[name], actual, expected, negate)
+ this.currentSpec.assertions.push(assertion.exec())
+ return assertion.passed
+ },
+
+ /**
+ * Iterate an object, invoking the given callback.
+ *
+ * @param {hash, array, string} object
+ * @param {function} callback
+ * @return {JSpec}
+ * @api public
+ */
+
+ each : function(object, callback) {
+ if (typeof object == 'string') object = object.split(' ')
+ for (key in object) {
+ if (object.hasOwnProperty(key))
+ callback.length == 1 ?
+ callback.call(JSpec, object[key]):
+ callback.call(JSpec, key, object[key])
+ }
+ return JSpec
+ },
+
+ /**
+ * Iterate with memo.
+ *
+ * @param {hash, array} object
+ * @param {object} initial
+ * @param {function} callback
+ * @return {object}
+ * @api public
+ */
+
+ inject : function(object, initial, callback) {
+ each(object, function(key, value){
+ initial = callback.length == 2 ?
+ callback.call(JSpec, initial, value):
+ callback.call(JSpec, initial, key, value) || initial
+ })
+ return initial
+ },
+
+ /**
+ * Strim whitespace or chars.
+ *
+ * @param {string} string
+ * @param {string} chars
+ * @return {string}
+ * @api public
+ */
+
+ strip : function(string, chars) {
+ return string.
+ replace(new RegExp('[' + (chars || '\\s') + ']*$'), '').
+ replace(new RegExp('^[' + (chars || '\\s') + ']*'), '')
+ },
+
+ /**
+ * Extend an object with another.
+ *
+ * @param {object} object
+ * @param {object} other
+ * @api public
+ */
+
+ extend : function(object, other) {
+ each(other, function(property, value){
+ object[property] = value
+ })
+ },
+
+ /**
+ * Map callback return values.
+ *
+ * @param {hash, array} object
+ * @param {function} callback
+ * @return {array}
+ * @api public
+ */
+
+ map : function(object, callback) {
+ return inject(object, [], function(memo, key, value){
+ memo.push(callback.length == 1 ?
+ callback.call(JSpec, value):
+ callback.call(JSpec, key, value))
+ })
+ },
+
+ /**
+ * Returns true if the callback returns true at least once.
+ *
+ * @param {hash, array} object
+ * @param {function} callback
+ * @return {bool}
+ * @api public
+ */
+
+ any : function(object, callback) {
+ return inject(object, false, function(state, key, value){
+ if (state) return true
+ return callback.length == 1 ?
+