cans 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore CHANGED
@@ -19,3 +19,6 @@ rdoc
19
19
  pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
+ vendor/cache
23
+ .rvmrc
24
+ Gemfile.lock
data/README.rdoc CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  Interactive online source browser for Rack applications.
4
4
 
5
- Experimental and unfinished. This gem will probably kill
6
- you in your sleep if you try to use it right now.
7
-
8
5
  == Using in Rails 3
9
6
 
10
7
  Add "cans" to your Gemfile:
@@ -19,6 +16,10 @@ Start your Rails app, and use it a bit. Understand that cans will add an after_
19
16
 
20
17
  Then, visit the cans mountpoint and browse around.
21
18
 
19
+ == Testing
20
+
21
+ Unit tests are in the "test/unit" directory, using "shoulda" from ThoughtBot. Run them with "rake test".
22
+
22
23
  == Note on Patches/Pull Requests
23
24
 
24
25
  * Fork the project.
@@ -31,4 +32,4 @@ Then, visit the cans mountpoint and browse around.
31
32
 
32
33
  == Copyright
33
34
 
34
- Copyright (c) 2010 Bryce Kerley. See LICENSE for details.
35
+ Copyright (c) 2010-2011 Bryce Kerley. See LICENSE for details.
data/Rakefile CHANGED
@@ -1,32 +1,13 @@
1
- require 'rubygems'
2
- require 'rake'
3
-
1
+ require 'bundler'
4
2
  begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "cans"
8
- gem.summary = %Q{Source browser for Rack applications}
9
- gem.description = %Q{Interactive on-line source browser for rack applications}
10
- gem.email = "bkerley@brycekerley.net"
11
- gem.homepage = "http://github.com/bkerley/cans"
12
- gem.authors = ["Bryce Kerley"]
13
-
14
- gem.add_dependency 'sinatra', '~> 1.1.0'
15
- gem.add_dependency 'haml', '~> 3.0.22'
16
- gem.add_dependency 'method_extensions', '~> 0.0.8'
17
-
18
- gem.add_development_dependency "shoulda", "~> 2.11.3"
19
- gem.add_development_dependency 'rack-test', '~> 0.5.6'
20
- gem.add_development_dependency 'mocha', '~> 0.9.9'
21
-
22
- gem.required_ruby_version = '~> 1.9.2'
23
-
24
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
25
- end
26
- Jeweler::GemcutterTasks.new
27
- rescue LoadError
28
- puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
3
+ Bundler.setup(:default, :development)
4
+ Bundler::GemHelper.install_tasks
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
29
9
  end
10
+ require 'rake'
30
11
 
31
12
  require 'rake/testtask'
32
13
  Rake::TestTask.new(:test) do |test|
@@ -48,7 +29,7 @@ rescue LoadError
48
29
  end
49
30
  end
50
31
 
51
- task :test => :check_dependencies
32
+ task :test
52
33
 
53
34
  task :default => :test
54
35
 
@@ -61,3 +42,10 @@ Rake::RDocTask.new do |rdoc|
61
42
  rdoc.rdoc_files.include('README*')
62
43
  rdoc.rdoc_files.include('lib/**/*.rb')
63
44
  end
45
+
46
+ desc 'Compile the coffeescript files to javascript'
47
+ task :coffeescript => 'lib/cans/static/application.js'
48
+
49
+ file 'lib/cans/static/application.js' => 'lib/cans/views/application.coffee' do
50
+ system 'coffee -co lib/cans/static lib/cans/views/application.coffee'
51
+ end
data/cans.gemspec CHANGED
@@ -1,87 +1,35 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
- # -*- encoding: utf-8 -*-
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require 'cans/version'
5
3
 
6
4
  Gem::Specification.new do |s|
7
5
  s.name = %q{cans}
8
- s.version = "0.1.2"
6
+ s.version = Cans::VERSION
9
7
 
10
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
9
  s.authors = ["Bryce Kerley"]
12
- s.date = %q{2010-11-12}
10
+ s.date = %q{2011-05-19}
13
11
  s.description = %q{Interactive on-line source browser for rack applications}
14
12
  s.email = %q{bkerley@brycekerley.net}
15
13
  s.extra_rdoc_files = [
16
14
  "LICENSE",
17
15
  "README.rdoc"
18
16
  ]
19
- s.files = [
20
- ".document",
21
- ".gitignore",
22
- "Gemfile",
23
- "Gemfile.lock",
24
- "LICENSE",
25
- "README.rdoc",
26
- "Rakefile",
27
- "VERSION",
28
- "cans.gemspec",
29
- "config.ru",
30
- "lib/cans.rb",
31
- "lib/cans/address.rb",
32
- "lib/cans/application.rb",
33
- "lib/cans/historian.rb",
34
- "lib/cans/views/index.haml",
35
- "lib/cans/views/method.haml",
36
- "lib/cans/views/module.haml",
37
- "test/fixtures/beverage.rb",
38
- "test/helper.rb",
39
- "test/test_address.rb",
40
- "test/test_application.rb",
41
- "test/test_cans.rb",
42
- "test/test_historian.rb"
43
- ]
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
44
19
  s.homepage = %q{http://github.com/bkerley/cans}
45
20
  s.rdoc_options = ["--charset=UTF-8"]
46
21
  s.require_paths = ["lib"]
47
22
  s.required_ruby_version = Gem::Requirement.new("~> 1.9.2")
48
23
  s.rubygems_version = %q{1.3.7}
49
24
  s.summary = %q{Source browser for Rack applications}
50
- s.test_files = [
51
- "test/fixtures/beverage.rb",
52
- "test/helper.rb",
53
- "test/test_address.rb",
54
- "test/test_application.rb",
55
- "test/test_cans.rb",
56
- "test/test_historian.rb"
57
- ]
58
-
59
- if s.respond_to? :specification_version then
60
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
61
- s.specification_version = 3
62
25
 
63
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
64
- s.add_runtime_dependency(%q<sinatra>, ["~> 1.1.0"])
65
- s.add_runtime_dependency(%q<haml>, ["~> 3.0.22"])
66
- s.add_runtime_dependency(%q<method_extensions>, ["~> 0.0.8"])
67
- s.add_development_dependency(%q<shoulda>, ["~> 2.11.3"])
68
- s.add_development_dependency(%q<rack-test>, ["~> 0.5.6"])
69
- s.add_development_dependency(%q<mocha>, ["~> 0.9.9"])
70
- else
71
- s.add_dependency(%q<sinatra>, ["~> 1.1.0"])
72
- s.add_dependency(%q<haml>, ["~> 3.0.22"])
73
- s.add_dependency(%q<method_extensions>, ["~> 0.0.8"])
74
- s.add_dependency(%q<shoulda>, ["~> 2.11.3"])
75
- s.add_dependency(%q<rack-test>, ["~> 0.5.6"])
76
- s.add_dependency(%q<mocha>, ["~> 0.9.9"])
77
- end
78
- else
79
- s.add_dependency(%q<sinatra>, ["~> 1.1.0"])
80
- s.add_dependency(%q<haml>, ["~> 3.0.22"])
81
- s.add_dependency(%q<method_extensions>, ["~> 0.0.8"])
82
- s.add_dependency(%q<shoulda>, ["~> 2.11.3"])
83
- s.add_dependency(%q<rack-test>, ["~> 0.5.6"])
84
- s.add_dependency(%q<mocha>, ["~> 0.9.9"])
85
- end
26
+ s.add_runtime_dependency(%q<sinatra>, ["~> 1.1.0"])
27
+ s.add_runtime_dependency(%q<haml>, ["~> 3.0.22"])
28
+ s.add_runtime_dependency(%q<method_extensions>, ["~> 0.0.8"])
29
+ s.add_development_dependency(%q<shoulda>, ["~> 2.11.3"])
30
+ s.add_development_dependency(%q<rack-test>, ["~> 0.5.6"])
31
+ s.add_development_dependency(%q<coffee-script>, ["~> 1.1.0"])
32
+ s.add_development_dependency(%q<evergreen>, ["~> 0.4.0"])
33
+ s.add_development_dependency(%q<mocha>, ["~> 0.9.12"])
86
34
  end
87
35
 
@@ -0,0 +1,3 @@
1
+ Evergreen.configure do |config|
2
+ config.public_dir = 'lib/cans/static'
3
+ end
@@ -1,6 +1,7 @@
1
1
  module Cans
2
2
  class Application < Sinatra::Base
3
3
  set :views, File.dirname(__FILE__) + '/views'
4
+ set :public, File.dirname(__FILE__) + '/static'
4
5
 
5
6
  get '/' do
6
7
  @constants = Object.constants
@@ -9,6 +10,49 @@ module Cans
9
10
  haml :index
10
11
  end
11
12
 
13
+ get '/browser' do
14
+ haml :frameset
15
+ end
16
+
17
+ post '/browser/image' do
18
+ @constants = Object.constants
19
+ @modules = @constants.map{ |c| Object.const_get c}.select{ |c| c.kind_of? Module}.map(&:name).sort
20
+ content_type :json
21
+ to_json({ :modules=>@modules })
22
+ end
23
+
24
+ post '/browser/class/*' do
25
+ @address = Address.new(params[:splat].first)
26
+ @module = @address.target_module
27
+
28
+ @local_instance_methods = @module.instance_methods false
29
+ @all_instance_methods = @module.instance_methods true
30
+ @super_instance_methods = @all_instance_methods - @local_instance_methods
31
+
32
+ @class_methods = @module.methods.map(&:to_s).sort
33
+
34
+ @ancestors = @module.ancestors
35
+ @child_modules = @module.constants.map{ |c| @module.const_get c}.select{ |c| c.kind_of? Module}.map(&:name).sort
36
+
37
+ content_type :json
38
+ to_json({ :child_modules=>@child_modules,
39
+ :class_methods=>@class_methods,
40
+ :local_instance_methods=>@local_instance_methods.map(&:to_s).sort,
41
+ :inherited_instance_methods=>@super_instance_methods.map(&:to_s).sort
42
+ })
43
+ end
44
+
45
+ post '/browser/method/*' do
46
+ @address = Address.new(params[:splat].first)
47
+ @module = @address.target_module
48
+ @method = @address.target_method
49
+
50
+ @source = @method.source_with_doc
51
+ @location = @method.source_location
52
+
53
+ to_json({ :source=>@source, :source_location=>@location })
54
+ end
55
+
12
56
  get '/module/*' do
13
57
  @address = Address.new(params[:splat].first)
14
58
  @module = @address.target_module
@@ -19,7 +63,7 @@ module Cans
19
63
 
20
64
  @class_methods = @module.methods
21
65
 
22
- @ancestors = @module.ancestors
66
+ @ancestors = @module.ancestors.sort_by(&:name)
23
67
  @child_modules = @module.constants.map{ |c| @module.const_get c}.select{ |c| c.kind_of? Module}.sort_by(&:name)
24
68
 
25
69
  haml :module
@@ -47,6 +91,10 @@ module Cans
47
91
  href = prefix + destination
48
92
  "<a href='#{href}'>#{content}</a>"
49
93
  end
94
+
95
+ def to_json(hash)
96
+ JSON.generate hash
97
+ end
50
98
  end
51
99
  end
52
100
  end
@@ -0,0 +1,253 @@
1
+ (function() {
2
+ var __bind = function(func, context) {
3
+ return function(){ return func.apply(context, arguments); };
4
+ }, __extends = function(child, parent) {
5
+ var ctor = function(){};
6
+ ctor.prototype = parent.prototype;
7
+ child.prototype = new ctor();
8
+ child.prototype.constructor = child;
9
+ if (typeof parent.extended === "function") parent.extended(child);
10
+ child.__super__ = parent.prototype;
11
+ };
12
+ jQuery(function() {
13
+ var Ajax;
14
+ jQuery.ajaxSetup({
15
+ type: 'post',
16
+ dataType: 'json'
17
+ });
18
+ Ajax = function(path, callback) {
19
+ var fullPath;
20
+ fullPath = window.location.pathname + path;
21
+ return jQuery.ajax({
22
+ url: fullPath,
23
+ success: callback
24
+ });
25
+ };
26
+ window.Machine = function() {
27
+ this.load();
28
+ return this;
29
+ };
30
+ window.Machine.prototype.load = function() {
31
+ return Ajax('/image', __bind(function(data) {
32
+ return this.consume(data);
33
+ }, this));
34
+ };
35
+ window.Machine.prototype.consume = function(returned) {
36
+ this.modules = _.map(returned.modules, function(m) {
37
+ return new Module(m);
38
+ });
39
+ return this.view.trigger('loaded');
40
+ };
41
+ window.Module = function(_arg) {
42
+ this.name = _arg;
43
+ this.view = null;
44
+ return this;
45
+ };
46
+ window.Module.prototype.load = function() {
47
+ return Ajax("/class/" + (this.name), __bind(function(data) {
48
+ return this.consume(data);
49
+ }, this));
50
+ };
51
+ window.Module.prototype.consume = function(returned) {
52
+ this.childModules = _.map(returned.child_modules, __bind(function(m) {
53
+ return new Module(m);
54
+ }, this));
55
+ this.classMethods = _.map(returned.class_methods, __bind(function(m) {
56
+ return new ClassMethod(this, m);
57
+ }, this));
58
+ this.localInstanceMethods = _.map(returned.local_instance_methods, __bind(function(m) {
59
+ return new InstanceMethod(this, m);
60
+ }, this));
61
+ this.inheritedInstanceMethods = _.map(returned.inherited_instance_methods, __bind(function(m) {
62
+ return new InstanceMethod(this, m);
63
+ }, this));
64
+ return this.view.trigger('loaded');
65
+ };
66
+ window.Module.prototype.toJSON = function() {
67
+ return {
68
+ name: this.name
69
+ };
70
+ };
71
+ window.Method = function(_arg, _arg2) {
72
+ this.name = _arg2;
73
+ this.module = _arg;
74
+ return this;
75
+ };
76
+ window.Method.prototype.load = function(flavor) {
77
+ return Ajax(this.url(), __bind(function(data) {
78
+ return this.consume(data);
79
+ }, this));
80
+ };
81
+ window.Method.prototype.consume = function(returned) {
82
+ this.source = returned.source;
83
+ if (this.source) {
84
+ this.source_file = returned.source_location[0];
85
+ this.source_line = returned.source_location[1];
86
+ }
87
+ return this.view.trigger('loaded');
88
+ };
89
+ window.Method.prototype.url = function() {
90
+ return "/method/" + (escape(this.module.name)) + "/." + (escape(this.flavor)) + "/" + (escape(this.name));
91
+ };
92
+ window.Method.prototype.toJSON = function() {
93
+ return {
94
+ module: this.module.toJSON(),
95
+ name: this.name,
96
+ source: this.source,
97
+ file: this.source_file,
98
+ line: this.source_line,
99
+ flavor: this.flavor,
100
+ flavorSymbol: this.flavorSymbol
101
+ };
102
+ };
103
+ window.InstanceMethod = function() {
104
+ return window.Method.apply(this, arguments);
105
+ };
106
+ __extends(window.InstanceMethod, window.Method);
107
+ window.InstanceMethod.prototype.flavor = 'i';
108
+ window.InstanceMethod.prototype.flavorSymbol = '#';
109
+ window.ClassMethod = function() {
110
+ return window.Method.apply(this, arguments);
111
+ };
112
+ __extends(window.ClassMethod, window.Method);
113
+ window.ClassMethod.prototype.flavor = 'm';
114
+ window.ClassMethod.prototype.flavorSymbol = '::';
115
+ window.SourceView = Backbone.View.extend({
116
+ tagName: 'div',
117
+ template: _.template($('#source_template').html()),
118
+ errorTemplate: _.template($('#source_error_template').html()),
119
+ render: function() {
120
+ if (this.model.source) {
121
+ return this.renderOkay();
122
+ } else {
123
+ return this.renderError();
124
+ }
125
+ },
126
+ renderOkay: function() {
127
+ var rendered;
128
+ rendered = this.template(this.model.toJSON());
129
+ $(this.el).html(rendered);
130
+ return this;
131
+ },
132
+ renderError: function() {
133
+ var rendered;
134
+ rendered = this.errorTemplate(this.model.toJSON());
135
+ $(this.el).html(rendered);
136
+ return this;
137
+ }
138
+ });
139
+ window.MethodView = Backbone.View.extend({
140
+ tagName: 'li',
141
+ template: _.template($('#method_template').html()),
142
+ events: {
143
+ 'click': 'loadSource'
144
+ },
145
+ initialize: function() {
146
+ this.model.view = this;
147
+ return this.bind('loaded', this.drawSource);
148
+ },
149
+ loadSource: function() {
150
+ $('#content').html('');
151
+ this.model.load();
152
+ return false;
153
+ },
154
+ drawSource: function() {
155
+ var sourceView;
156
+ sourceView = new SourceView({
157
+ model: this.model
158
+ });
159
+ $('#content').html(sourceView.render().el);
160
+ return SyntaxHighlighter.highlight({}, $('#content pre')[0]);
161
+ },
162
+ render: function() {
163
+ var rendered;
164
+ rendered = this.template(this.model.toJSON());
165
+ $(this.el).html(rendered);
166
+ $(this.el).addClass(this.model.flavor);
167
+ return this;
168
+ }
169
+ });
170
+ window.ModuleView = Backbone.View.extend({
171
+ tagName: 'li',
172
+ template: _.template($('#module_template').html()),
173
+ events: {
174
+ 'click': 'loadChildren'
175
+ },
176
+ initialize: function() {
177
+ this.model.view = this;
178
+ return this.bind('loaded', this.drawChildren);
179
+ },
180
+ loadChildren: function() {
181
+ $('#class_method_list').html('');
182
+ $('#instance_method_list').html('');
183
+ $('#content').html('');
184
+ $(this.el).children('.child_modules').html('');
185
+ this.model.load();
186
+ return false;
187
+ },
188
+ drawChildren: function() {
189
+ this.drawChildModules();
190
+ this.drawClassMethods();
191
+ return this.drawInstanceMethods();
192
+ },
193
+ drawClassMethods: function() {
194
+ return _(this.model.classMethods).each(function(m) {
195
+ var view;
196
+ view = new MethodView({
197
+ model: m
198
+ });
199
+ return $('#class_method_list').append(view.render().el);
200
+ });
201
+ },
202
+ drawInstanceMethods: function() {
203
+ _(this.model.localInstanceMethods).each(function(m) {
204
+ var view;
205
+ view = new MethodView({
206
+ model: m
207
+ });
208
+ return $('#instance_method_list').append(view.render().el);
209
+ });
210
+ return _(this.model.inheritedInstanceMethods).each(function(m) {
211
+ var view;
212
+ view = new MethodView({
213
+ model: m
214
+ });
215
+ return $('#instance_method_list').append(view.render().el);
216
+ });
217
+ },
218
+ drawChildModules: function() {
219
+ return _(this.model.childModules).each(__bind(function(m) {
220
+ var view;
221
+ view = new ModuleView({
222
+ model: m
223
+ });
224
+ return $(this.el).children('.child_modules').append(view.render().el);
225
+ }, this));
226
+ },
227
+ render: function() {
228
+ var rendered;
229
+ rendered = this.template(this.model.toJSON());
230
+ $(this.el).html(rendered);
231
+ return this;
232
+ }
233
+ });
234
+ window.MachineView = Backbone.View.extend({
235
+ initialize: function() {
236
+ this.model.view = this;
237
+ return this.bind('loaded', this.drawModules);
238
+ },
239
+ drawModules: function() {
240
+ return _(this.model.modules).each(function(m) {
241
+ var view;
242
+ view = new ModuleView({
243
+ model: m
244
+ });
245
+ return $('#module_list').append(view.render().el);
246
+ });
247
+ }
248
+ });
249
+ return (window.App = new window.MachineView({
250
+ model: new window.Machine()
251
+ }));
252
+ });
253
+ }).call(this);
@@ -0,0 +1,26 @@
1
+ // Backbone.js 0.3.1
2
+ // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
3
+ // Backbone may be freely distributed under the MIT license.
4
+ // For all details and documentation:
5
+ // http://documentcloud.github.com/backbone
6
+ (function(){var e;e=typeof exports!=="undefined"?exports:this.Backbone={};e.VERSION="0.3.1";var f=this._;if(!f&&typeof require!=="undefined")f=require("underscore")._;var h=this.jQuery;e.emulateHTTP=false;e.emulateJSON=false;e.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]||(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,g=c.length;d<g;d++)if(b===c[d]){c.splice(d,1);
7
+ break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,g;if(!(c=this._callbacks))return this;if(b=c[a]){d=0;for(g=b.length;d<g;d++)b[d].apply(this,Array.prototype.slice.call(arguments,1))}if(b=c.all){d=0;for(g=b.length;d<g;d++)b[d].apply(this,arguments)}return this}};e.Model=function(a,b){this.attributes={};this.cid=f.uniqueId("c");this.set(a||{},{silent:true});this._previousAttributes=f.clone(this.attributes);if(b&&b.collection)this.collection=b.collection;this.initialize&&
8
+ this.initialize(a,b)};f.extend(e.Model.prototype,e.Events,{_previousAttributes:null,_changed:false,toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},set:function(a,b){b||(b={});if(!a)return this;if(a.attributes)a=a.attributes;var c=this.attributes;if(!b.silent&&this.validate&&!this._performValidation(a,b))return false;if("id"in a)this.id=a.id;for(var d in a){var g=a[d];if(!f.isEqual(c[d],g)){c[d]=g;if(!b.silent){this._changed=true;this.trigger("change:"+
9
+ d,this,g)}}}!b.silent&&this._changed&&this.change();return this},unset:function(a,b){b||(b={});var c={};c[a]=void 0;if(!b.silent&&this.validate&&!this._performValidation(c,b))return false;delete this.attributes[a];if(!b.silent){this._changed=true;this.trigger("change:"+a,this);this.change()}return this},clear:function(a){a||(a={});var b=this.attributes,c={};for(attr in b)c[attr]=void 0;if(!a.silent&&this.validate&&!this._performValidation(c,a))return false;this.attributes={};if(!a.silent){this._changed=
10
+ true;for(attr in b)this.trigger("change:"+attr,this);this.change()}return this},fetch:function(a){a||(a={});var b=this,c=a.error&&f.bind(a.error,null,b);e.sync("read",this,function(d){if(!b.set(b.parse(d),a))return false;a.success&&a.success(b,d)},c);return this},save:function(a,b){a||(a={});b||(b={});if(!this.set(a,b))return false;var c=this,d=b.error&&f.bind(b.error,null,c),g=this.isNew()?"create":"update";e.sync(g,this,function(i){if(!c.set(c.parse(i),b))return false;b.success&&b.success(c,i)},
11
+ d);return this},destroy:function(a){a||(a={});var b=this,c=a.error&&f.bind(a.error,null,b);e.sync("delete",this,function(d){b.collection&&b.collection.remove(b);a.success&&a.success(b,d)},c);return this},url:function(){var a=j(this.collection);if(this.isNew())return a;return a+"/"+this.id},parse:function(a){return a},clone:function(){return new this.constructor(this)},isNew:function(){return!this.id},change:function(){this.trigger("change",this);this._previousAttributes=f.clone(this.attributes);this._changed=
12
+ false},hasChanged:function(a){if(a)return this._previousAttributes[a]!=this.attributes[a];return this._changed},changedAttributes:function(a){a||(a=this.attributes);var b=this._previousAttributes,c=false,d;for(d in a)if(!f.isEqual(b[d],a[d])){c=c||{};c[d]=a[d]}return c},previous:function(a){if(!a||!this._previousAttributes)return null;return this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},_performValidation:function(a,b){var c=this.validate(a);
13
+ if(c){b.error?b.error(this,c):this.trigger("error",this,c);return false}return true}});e.Collection=function(a,b){b||(b={});if(b.comparator){this.comparator=b.comparator;delete b.comparator}this._boundOnModelEvent=f.bind(this._onModelEvent,this);this._reset();a&&this.refresh(a,{silent:true});this.initialize&&this.initialize(a,b)};f.extend(e.Collection.prototype,e.Events,{model:e.Model,toJSON:function(){return this.map(function(a){return a.toJSON()})},add:function(a,b){if(f.isArray(a))for(var c=0,
14
+ d=a.length;c<d;c++)this._add(a[c],b);else this._add(a,b);return this},remove:function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)this._remove(a[c],b);else this._remove(a,b);return this},get:function(a){return a&&this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");this.models=this.sortBy(this.comparator);a.silent||this.trigger("refresh",
15
+ this);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},refresh:function(a,b){a||(a=[]);b||(b={});this._reset();this.add(a,{silent:true});b.silent||this.trigger("refresh",this);return this},fetch:function(a){a||(a={});var b=this,c=a.error&&f.bind(a.error,null,b);e.sync("read",this,function(d){b.refresh(b.parse(d));a.success&&a.success(b,d)},c);return this},create:function(a,b){var c=this;b||(b={});if(a instanceof e.Model)a.collection=c;else a=new this.model(a,
16
+ {collection:c});return a.save(null,{success:function(d,g){c.add(d);b.success&&b.success(d,g)},error:b.error})},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_add:function(a,b){b||(b={});a instanceof e.Model||(a=new this.model(a,{collection:this}));var c=this.getByCid(a);if(c)throw Error(["Can't add the same model to a set twice",c.id]);this._byId[a.id]=a;this._byCid[a.cid]=a;a.collection=this;
17
+ this.models.splice(this.comparator?this.sortedIndex(a,this.comparator):this.length,0,a);a.bind("all",this._boundOnModelEvent);this.length++;b.silent||a.trigger("add",a,this);return a},_remove:function(a,b){b||(b={});a=this.getByCid(a);if(!a)return null;delete this._byId[a.id];delete this._byCid[a.cid];delete a.collection;this.models.splice(this.indexOf(a),1);this.length--;b.silent||a.trigger("remove",a,this);a.unbind("all",this._boundOnModelEvent);return a},_onModelEvent:function(a,b){if(a==="change:id"){delete this._byId[b.previous("id")];
18
+ this._byId[b.id]=b}this.trigger.apply(this,arguments)}});f.each(["forEach","each","map","reduce","reduceRight","find","detect","filter","select","reject","every","all","some","any","include","invoke","max","min","sortBy","sortedIndex","toArray","size","first","rest","last","without","indexOf","lastIndexOf","isEmpty"],function(a){e.Collection.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});e.Controller=function(a){a||(a={});if(a.routes)this.routes=a.routes;
19
+ this._bindRoutes();this.initialize&&this.initialize(a)};var o=/:([\w\d]+)/g,p=/\*([\w\d]+)/g;f.extend(e.Controller.prototype,e.Events,{route:function(a,b,c){e.history||(e.history=new e.History);f.isRegExp(a)||(a=this._routeToRegExp(a));e.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d))},this))},saveLocation:function(a){e.history.saveLocation(a)},_bindRoutes:function(){if(this.routes)for(var a in this.routes){var b=this.routes[a];
20
+ this.route(a,b,this[b])}},_routeToRegExp:function(a){a=a.replace(o,"([^/]*)").replace(p,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});e.History=function(){this.handlers=[];this.fragment=this.getFragment();f.bindAll(this,"checkUrl")};var k=/^#*/;f.extend(e.History.prototype,{interval:50,getFragment:function(a){return(a||window.location).hash.replace(k,"")},start:function(){var a=document.documentMode;if(a=h.browser.msie&&a<7)this.iframe=h('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow;
21
+ "onhashchange"in window&&!a?h(window).bind("hashchange",this.checkUrl):setInterval(this.checkUrl,this.interval);return this.loadUrl()},route:function(a,b){this.handlers.push({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();if(a==this.fragment&&this.iframe)a=this.getFragment(this.iframe.location);if(a==this.fragment||a==decodeURIComponent(this.fragment))return false;if(this.iframe)window.location.hash=this.iframe.location.hash=a;this.loadUrl()},loadUrl:function(){var a=this.fragment=
22
+ this.getFragment();return f.any(this.handlers,function(b){if(b.route.test(a)){b.callback(a);return true}})},saveLocation:function(a){a=(a||"").replace(k,"");if(this.fragment!=a){window.location.hash=this.fragment=a;if(this.iframe&&a!=this.getFragment(this.iframe.location)){this.iframe.document.open().close();this.iframe.location.hash=a}}}});e.View=function(a){this._configure(a||{});this._ensureElement();this.delegateEvents();this.initialize&&this.initialize(a)};var l=function(a){return h(a,this.el)},
23
+ q=/^(\w+)\s*(.*)$/;f.extend(e.View.prototype,e.Events,{tagName:"div",$:l,jQuery:l,render:function(){return this},remove:function(){h(this.el).remove();return this},make:function(a,b,c){a=document.createElement(a);b&&h(a).attr(b);c&&h(a).html(c);return a},delegateEvents:function(a){if(a||(a=this.events)){h(this.el).unbind();for(var b in a){var c=a[b],d=b.match(q),g=d[1];d=d[2];c=f.bind(this[c],this);d===""?h(this.el).bind(g,c):h(this.el).delegate(d,g,c)}}},_configure:function(a){if(this.options)a=
24
+ f.extend({},this.options,a);if(a.model)this.model=a.model;if(a.collection)this.collection=a.collection;if(a.el)this.el=a.el;if(a.id)this.id=a.id;if(a.className)this.className=a.className;if(a.tagName)this.tagName=a.tagName;this.options=a},_ensureElement:function(){if(!this.el){var a={};if(this.id)a.id=this.id;if(this.className)a.className=this.className;this.el=this.make(this.tagName,a)}}});var m=function(a,b){var c=r(this,a,b);c.extend=m;return c};e.Model.extend=e.Collection.extend=e.Controller.extend=
25
+ e.View.extend=m;var s={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};e.sync=function(a,b,c,d){var g=s[a];a=a==="create"||a==="update"?JSON.stringify(b.toJSON()):null;b={url:j(b),type:g,contentType:"application/json",data:a,dataType:"json",processData:false,success:c,error:d};if(e.emulateJSON){b.contentType="application/x-www-form-urlencoded";b.processData=true;b.data=a?{model:a}:{}}if(e.emulateHTTP)if(g==="PUT"||g==="DELETE"){if(e.emulateJSON)b.data._method=g;b.type="POST";b.beforeSend=
26
+ function(i){i.setRequestHeader("X-HTTP-Method-Override",g)}}h.ajax(b)};var n=function(){},r=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};n.prototype=a.prototype;d.prototype=new n;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},j=function(a){if(!(a&&a.url))throw Error("A 'url' property or function must be specified");return f.isFunction(a.url)?a.url():a.url}})();
@@ -0,0 +1,42 @@
1
+ body {
2
+ padding: 0;
3
+ margin: 0;
4
+ }
5
+ ul#columns {
6
+ list-style-type: none;
7
+ height: 200px;
8
+ margin: 0;
9
+ padding: 0;
10
+ }
11
+ ul#columns>li {
12
+ display: inline-block;
13
+ height: 200px;
14
+ width: 33%;
15
+ overflow: scroll;
16
+ margin: 0;
17
+ padding: 0;
18
+ }
19
+ ul#columns>li:first {
20
+ width: 34%;
21
+ }
22
+
23
+ ul#columns>li>h1 {
24
+ font-size: 12pt;
25
+ margin: 0;
26
+ }
27
+
28
+ ul#columns>li>ul {
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ ul#columns>li>ul>li {
34
+ padding-left: 0.2em;
35
+ }
36
+ #content {
37
+ padding: 0 1em;
38
+ }
39
+ #content h1 {
40
+ margin-top: 0.2em;
41
+ margin-bottom: 0;
42
+ }