cans 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }