code_buddy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +4 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +44 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +10 -0
  8. data/bin/code_buddy +5 -0
  9. data/code_buddy.gemspec +31 -0
  10. data/lib/code_buddy.rb +12 -0
  11. data/lib/code_buddy/app.rb +50 -0
  12. data/lib/code_buddy/middleware.rb +45 -0
  13. data/lib/code_buddy/public/images/buddy.jpeg +0 -0
  14. data/lib/code_buddy/public/javascripts/backbone.js +966 -0
  15. data/lib/code_buddy/public/javascripts/code_buddy.js +130 -0
  16. data/lib/code_buddy/public/javascripts/jquery.js +7179 -0
  17. data/lib/code_buddy/public/javascripts/underscore.js +722 -0
  18. data/lib/code_buddy/public/stylesheets/code_buddy.css +106 -0
  19. data/lib/code_buddy/public/stylesheets/coderay.css +102 -0
  20. data/lib/code_buddy/stack.rb +19 -0
  21. data/lib/code_buddy/stack_frame.rb +43 -0
  22. data/lib/code_buddy/templates/rescues/_request_and_response.erb +31 -0
  23. data/lib/code_buddy/templates/rescues/_trace.erb +26 -0
  24. data/lib/code_buddy/templates/rescues/diagnostics.erb +10 -0
  25. data/lib/code_buddy/templates/rescues/layout.erb +29 -0
  26. data/lib/code_buddy/templates/rescues/missing_template.erb +2 -0
  27. data/lib/code_buddy/templates/rescues/routing_error.erb +10 -0
  28. data/lib/code_buddy/templates/rescues/template_error.erb +21 -0
  29. data/lib/code_buddy/templates/rescues/unknown_action.erb +2 -0
  30. data/lib/code_buddy/version.rb +3 -0
  31. data/lib/code_buddy/views/banner.erb +6 -0
  32. data/lib/code_buddy/views/form.erb +13 -0
  33. data/lib/code_buddy/views/header.erb +8 -0
  34. data/lib/code_buddy/views/index.erb +25 -0
  35. data/lib/code_buddy/views/orig.erb +53 -0
  36. data/spec/app_spec.rb +6 -0
  37. data/spec/middleware_spec.rb +37 -0
  38. data/spec/spec_helper.rb +26 -0
  39. data/spec/stack_frame_spec.rb +105 -0
  40. data/spec/stack_spec.rb +26 -0
  41. metadata +203 -0
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ *.swp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use @code_buddy
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in code_buddy.gemspec
4
+ gemspec
@@ -0,0 +1,44 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ code_buddy (0.0.1)
5
+ coderay (~> 0.9.6)
6
+ json_pure (~> 1.4.6)
7
+ rack (~> 1.2.0)
8
+ sinatra (~> 1.1.0)
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ coderay (0.9.6)
14
+ diff-lcs (1.1.2)
15
+ json_pure (1.4.6)
16
+ mocha (0.9.10)
17
+ rake
18
+ rack (1.2.1)
19
+ rake (0.8.7)
20
+ rspec (2.2.0)
21
+ rspec-core (~> 2.2)
22
+ rspec-expectations (~> 2.2)
23
+ rspec-mocks (~> 2.2)
24
+ rspec-core (2.2.1)
25
+ rspec-expectations (2.2.0)
26
+ diff-lcs (~> 1.1.2)
27
+ rspec-mocks (2.2.0)
28
+ sinatra (1.1.0)
29
+ rack (~> 1.1)
30
+ tilt (~> 1.1)
31
+ tilt (1.1)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ code_buddy!
38
+ coderay (~> 0.9.6)
39
+ json_pure (~> 1.4.6)
40
+ mocha (~> 0.9.10)
41
+ rack (~> 1.2.0)
42
+ rake (~> 0.8.7)
43
+ rspec (~> 2.2.0)
44
+ sinatra (~> 1.1.0)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Pat Shaughnessy, Alex Rothenberg and Daniel Higginbotham
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ =Code Buddy
2
+
3
+ Code Buddy will display a Ruby stack in a browser, along with code snippets from each file in the stack.
4
+
5
+ == Run as a Sinatra app
6
+
7
+ $ gem install code_buddy
8
+ $ code_buddy
9
+ == Sinatra/1.1.0 has taken the stage on 4567 for development with backup from Mongrel
10
+
11
+ Now open http://localhost:4567 and paste your stack.
12
+
13
+ == Run as Rack middleware
14
+
15
+ Just add code_buddy to your Gemfile:
16
+
17
+ gem 'code_buddy'
18
+
19
+ Now if config.consider_all_requests_local=true whenever you see a exception page in your Rails app you'll be able to click on the stack and see it inside Code Buddy with the corresponding code snippets.
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require 'rubygems'
4
+ require 'code_buddy'
5
+ CodeBuddy::App.run! :host => 'localhost'
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "code_buddy/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "code_buddy"
7
+ s.version = CodeBuddy::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Pat Shaughnessy, Alex Rothenberg, Daniel Higginbotham']
10
+ s.email = ['pat@patshaughnessy.net, alex@alexrothenberg.com, daniel@flyingmachinestudios.com']
11
+ s.homepage = "http://github.com/patshaughnessy/code_buddy"
12
+ s.summary = %q{See the Ruby code running in your app.}
13
+ s.description = %q{See the Ruby code running in your app.}
14
+
15
+ s.rubyforge_project = "code_buddy"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency 'rack', '~> 1.2.0'
23
+ s.add_dependency 'sinatra', '~> 1.1.0'
24
+ s.add_dependency 'json_pure', '~> 1.4.6'
25
+ s.add_dependency 'coderay', '~> 0.9.6'
26
+
27
+ s.add_development_dependency 'rake', '~> 0.8.7'
28
+ s.add_development_dependency 'rspec', '~> 2.2.0'
29
+ s.add_development_dependency 'mocha', '~> 0.9.10'
30
+
31
+ end
@@ -0,0 +1,12 @@
1
+ require 'sinatra'
2
+ require 'coderay'
3
+ require 'json'
4
+ require 'json/add/rails'
5
+
6
+ require 'code_buddy/middleware'
7
+ require 'code_buddy/app'
8
+ require 'code_buddy/stack'
9
+ require 'code_buddy/stack_frame'
10
+ require 'coderay'
11
+ require 'json'
12
+ require 'json/add/rails'
@@ -0,0 +1,50 @@
1
+ module CodeBuddy
2
+ class App < Sinatra::Base
3
+ set :views, File.dirname(__FILE__) + '/views'
4
+ set :public, File.dirname(__FILE__) + '/public'
5
+
6
+ class << self
7
+ attr_reader :stack
8
+
9
+ def exception=(exception)
10
+ @stack = Stack.new(exception)
11
+ end
12
+
13
+ def stack_string=(stack_string)
14
+ @stack = Stack.new(stack_string)
15
+ end
16
+ end
17
+
18
+ get '/' do
19
+ display_stack(0)
20
+ end
21
+
22
+ get '/new' do
23
+ erb :form
24
+ end
25
+
26
+ post '/' do
27
+ self.class.stack_string = params[:stack]
28
+ redirect '/'
29
+ end
30
+
31
+ get '/stack' do
32
+ display_stack(0)
33
+ end
34
+
35
+ get '/:selected' do
36
+ display_stack(params[:selected].to_i)
37
+ end
38
+
39
+ def display_stack(selected_param)
40
+ @stack = self.class.stack
41
+ if @stack
42
+ @stack.selected = selected_param
43
+ erb :index
44
+ else
45
+ redirect '/new'
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,45 @@
1
+ def rails_loaded
2
+ Module.const_get('Rails')
3
+ return true
4
+ rescue NameError
5
+ return false
6
+ end
7
+
8
+ if rails_loaded
9
+ module CodeBuddy
10
+ class Railtie < Rails::Railtie
11
+ initializer "code_buddy.add_middleware" do |app|
12
+ app.middleware.swap ActionDispatch::ShowExceptions, CodeBuddy::ShowExceptions
13
+ end
14
+ end
15
+
16
+ class ShowExceptions < ActionDispatch::ShowExceptions
17
+
18
+ CODEBUDDY_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')
19
+
20
+ def call(env)
21
+ if env['PATH_INFO'] =~ /^\/code_buddy(.*)/
22
+ env['PATH_INFO'] = $1
23
+ App.new.call(env)
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def rescue_action_locally(request, exception)
30
+ template = ActionView::Base.new([CODEBUDDY_TEMPLATE_PATH],
31
+ :request => request,
32
+ :exception => exception,
33
+ :application_trace => application_trace(exception),
34
+ :framework_trace => framework_trace(exception),
35
+ :full_trace => full_trace(exception)
36
+ )
37
+ file = "rescues/#{@@rescue_templates[exception.class.name]}.erb"
38
+ body = template.render(:file => file, :layout => 'rescues/layout.erb')
39
+ App.exception = exception
40
+ render(status_code(exception), body)
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,966 @@
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
+
7
+ (function(){
8
+
9
+ // Initial Setup
10
+ // -------------
11
+
12
+ // The top-level namespace. All public Backbone classes and modules will
13
+ // be attached to this. Exported for both CommonJS and the browser.
14
+ var Backbone;
15
+ if (typeof exports !== 'undefined') {
16
+ Backbone = exports;
17
+ } else {
18
+ Backbone = this.Backbone = {};
19
+ }
20
+
21
+ // Current version of the library. Keep in sync with `package.json`.
22
+ Backbone.VERSION = '0.3.1';
23
+
24
+ // Require Underscore, if we're on the server, and it's not already present.
25
+ var _ = this._;
26
+ if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._;
27
+
28
+ // For Backbone's purposes, jQuery owns the `$` variable.
29
+ var $ = this.jQuery;
30
+
31
+ // Turn on `emulateHTTP` to use support legacy HTTP servers. Setting this option will
32
+ // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
33
+ // `X-Http-Method-Override` header.
34
+ Backbone.emulateHTTP = false;
35
+
36
+ // Turn on `emulateJSON` to support legacy servers that can't deal with direct
37
+ // `application/json` requests ... will encode the body as
38
+ // `application/x-www-form-urlencoded` instead and will send the model in a
39
+ // form param named `model`.
40
+ Backbone.emulateJSON = false;
41
+
42
+ // Backbone.Events
43
+ // -----------------
44
+
45
+ // A module that can be mixed in to *any object* in order to provide it with
46
+ // custom events. You may `bind` or `unbind` a callback function to an event;
47
+ // `trigger`-ing an event fires all callbacks in succession.
48
+ //
49
+ // var object = {};
50
+ // _.extend(object, Backbone.Events);
51
+ // object.bind('expand', function(){ alert('expanded'); });
52
+ // object.trigger('expand');
53
+ //
54
+ Backbone.Events = {
55
+
56
+ // Bind an event, specified by a string name, `ev`, to a `callback` function.
57
+ // Passing `"all"` will bind the callback to all events fired.
58
+ bind : function(ev, callback) {
59
+ var calls = this._callbacks || (this._callbacks = {});
60
+ var list = this._callbacks[ev] || (this._callbacks[ev] = []);
61
+ list.push(callback);
62
+ return this;
63
+ },
64
+
65
+ // Remove one or many callbacks. If `callback` is null, removes all
66
+ // callbacks for the event. If `ev` is null, removes all bound callbacks
67
+ // for all events.
68
+ unbind : function(ev, callback) {
69
+ var calls;
70
+ if (!ev) {
71
+ this._callbacks = {};
72
+ } else if (calls = this._callbacks) {
73
+ if (!callback) {
74
+ calls[ev] = [];
75
+ } else {
76
+ var list = calls[ev];
77
+ if (!list) return this;
78
+ for (var i = 0, l = list.length; i < l; i++) {
79
+ if (callback === list[i]) {
80
+ list.splice(i, 1);
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return this;
87
+ },
88
+
89
+ // Trigger an event, firing all bound callbacks. Callbacks are passed the
90
+ // same arguments as `trigger` is, apart from the event name.
91
+ // Listening for `"all"` passes the true event name as the first argument.
92
+ trigger : function(ev) {
93
+ var list, calls, i, l;
94
+ if (!(calls = this._callbacks)) return this;
95
+ if (list = calls[ev]) {
96
+ for (i = 0, l = list.length; i < l; i++) {
97
+ list[i].apply(this, Array.prototype.slice.call(arguments, 1));
98
+ }
99
+ }
100
+ if (list = calls['all']) {
101
+ for (i = 0, l = list.length; i < l; i++) {
102
+ list[i].apply(this, arguments);
103
+ }
104
+ }
105
+ return this;
106
+ }
107
+
108
+ };
109
+
110
+ // Backbone.Model
111
+ // --------------
112
+
113
+ // Create a new model, with defined attributes. A client id (`cid`)
114
+ // is automatically generated and assigned for you.
115
+ Backbone.Model = function(attributes, options) {
116
+ this.attributes = {};
117
+ this.cid = _.uniqueId('c');
118
+ this.set(attributes || {}, {silent : true});
119
+ this._previousAttributes = _.clone(this.attributes);
120
+ if (options && options.collection) this.collection = options.collection;
121
+ if (this.initialize) this.initialize(attributes, options);
122
+ };
123
+
124
+ // Attach all inheritable methods to the Model prototype.
125
+ _.extend(Backbone.Model.prototype, Backbone.Events, {
126
+
127
+ // A snapshot of the model's previous attributes, taken immediately
128
+ // after the last `"change"` event was fired.
129
+ _previousAttributes : null,
130
+
131
+ // Has the item been changed since the last `"change"` event?
132
+ _changed : false,
133
+
134
+ // Return a copy of the model's `attributes` object.
135
+ toJSON : function() {
136
+ return _.clone(this.attributes);
137
+ },
138
+
139
+ // Get the value of an attribute.
140
+ get : function(attr) {
141
+ return this.attributes[attr];
142
+ },
143
+
144
+ // Set a hash of model attributes on the object, firing `"change"` unless you
145
+ // choose to silence it.
146
+ set : function(attrs, options) {
147
+
148
+ // Extract attributes and options.
149
+ options || (options = {});
150
+ if (!attrs) return this;
151
+ if (attrs.attributes) attrs = attrs.attributes;
152
+ var now = this.attributes;
153
+
154
+ // Run validation.
155
+ if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
156
+
157
+ // Check for changes of `id`.
158
+ if ('id' in attrs) this.id = attrs.id;
159
+
160
+ // Update attributes.
161
+ for (var attr in attrs) {
162
+ var val = attrs[attr];
163
+ if (!_.isEqual(now[attr], val)) {
164
+ now[attr] = val;
165
+ if (!options.silent) {
166
+ this._changed = true;
167
+ this.trigger('change:' + attr, this, val);
168
+ }
169
+ }
170
+ }
171
+
172
+ // Fire the `"change"` event, if the model has been changed.
173
+ if (!options.silent && this._changed) this.change();
174
+ return this;
175
+ },
176
+
177
+ // Remove an attribute from the model, firing `"change"` unless you choose
178
+ // to silence it.
179
+ unset : function(attr, options) {
180
+ options || (options = {});
181
+ var value = this.attributes[attr];
182
+
183
+ // Run validation.
184
+ var validObj = {};
185
+ validObj[attr] = void 0;
186
+ if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
187
+
188
+ // Remove the attribute.
189
+ delete this.attributes[attr];
190
+ if (!options.silent) {
191
+ this._changed = true;
192
+ this.trigger('change:' + attr, this);
193
+ this.change();
194
+ }
195
+ return this;
196
+ },
197
+
198
+ // Clear all attributes on the model, firing `"change"` unless you choose
199
+ // to silence it.
200
+ clear : function(options) {
201
+ options || (options = {});
202
+ var old = this.attributes;
203
+
204
+ // Run validation.
205
+ var validObj = {};
206
+ for (attr in old) validObj[attr] = void 0;
207
+ if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
208
+
209
+ this.attributes = {};
210
+ if (!options.silent) {
211
+ this._changed = true;
212
+ for (attr in old) {
213
+ this.trigger('change:' + attr, this);
214
+ }
215
+ this.change();
216
+ }
217
+ return this;
218
+ },
219
+
220
+ // Fetch the model from the server. If the server's representation of the
221
+ // model differs from its current attributes, they will be overriden,
222
+ // triggering a `"change"` event.
223
+ fetch : function(options) {
224
+ options || (options = {});
225
+ var model = this;
226
+ var success = function(resp) {
227
+ if (!model.set(model.parse(resp), options)) return false;
228
+ if (options.success) options.success(model, resp);
229
+ };
230
+ var error = options.error && _.bind(options.error, null, model);
231
+ Backbone.sync('read', this, success, error);
232
+ return this;
233
+ },
234
+
235
+ // Set a hash of model attributes, and sync the model to the server.
236
+ // If the server returns an attributes hash that differs, the model's
237
+ // state will be `set` again.
238
+ save : function(attrs, options) {
239
+ attrs || (attrs = {});
240
+ options || (options = {});
241
+ if (!this.set(attrs, options)) return false;
242
+ var model = this;
243
+ var success = function(resp) {
244
+ if (!model.set(model.parse(resp), options)) return false;
245
+ if (options.success) options.success(model, resp);
246
+ };
247
+ var error = options.error && _.bind(options.error, null, model);
248
+ var method = this.isNew() ? 'create' : 'update';
249
+ Backbone.sync(method, this, success, error);
250
+ return this;
251
+ },
252
+
253
+ // Destroy this model on the server. Upon success, the model is removed
254
+ // from its collection, if it has one.
255
+ destroy : function(options) {
256
+ options || (options = {});
257
+ var model = this;
258
+ var success = function(resp) {
259
+ if (model.collection) model.collection.remove(model);
260
+ if (options.success) options.success(model, resp);
261
+ };
262
+ var error = options.error && _.bind(options.error, null, model);
263
+ Backbone.sync('delete', this, success, error);
264
+ return this;
265
+ },
266
+
267
+ // Default URL for the model's representation on the server -- if you're
268
+ // using Backbone's restful methods, override this to change the endpoint
269
+ // that will be called.
270
+ url : function() {
271
+ var base = getUrl(this.collection);
272
+ if (this.isNew()) return base;
273
+ return base + '/' + this.id;
274
+ },
275
+
276
+ // **parse** converts a response into the hash of attributes to be `set` on
277
+ // the model. The default implementation is just to pass the response along.
278
+ parse : function(resp) {
279
+ return resp;
280
+ },
281
+
282
+ // Create a new model with identical attributes to this one.
283
+ clone : function() {
284
+ return new this.constructor(this);
285
+ },
286
+
287
+ // A model is new if it has never been saved to the server, and has a negative
288
+ // ID.
289
+ isNew : function() {
290
+ return !this.id;
291
+ },
292
+
293
+ // Call this method to manually fire a `change` event for this model.
294
+ // Calling this will cause all objects observing the model to update.
295
+ change : function() {
296
+ this.trigger('change', this);
297
+ this._previousAttributes = _.clone(this.attributes);
298
+ this._changed = false;
299
+ },
300
+
301
+ // Determine if the model has changed since the last `"change"` event.
302
+ // If you specify an attribute name, determine if that attribute has changed.
303
+ hasChanged : function(attr) {
304
+ if (attr) return this._previousAttributes[attr] != this.attributes[attr];
305
+ return this._changed;
306
+ },
307
+
308
+ // Return an object containing all the attributes that have changed, or false
309
+ // if there are no changed attributes. Useful for determining what parts of a
310
+ // view need to be updated and/or what attributes need to be persisted to
311
+ // the server.
312
+ changedAttributes : function(now) {
313
+ now || (now = this.attributes);
314
+ var old = this._previousAttributes;
315
+ var changed = false;
316
+ for (var attr in now) {
317
+ if (!_.isEqual(old[attr], now[attr])) {
318
+ changed = changed || {};
319
+ changed[attr] = now[attr];
320
+ }
321
+ }
322
+ return changed;
323
+ },
324
+
325
+ // Get the previous value of an attribute, recorded at the time the last
326
+ // `"change"` event was fired.
327
+ previous : function(attr) {
328
+ if (!attr || !this._previousAttributes) return null;
329
+ return this._previousAttributes[attr];
330
+ },
331
+
332
+ // Get all of the attributes of the model at the time of the previous
333
+ // `"change"` event.
334
+ previousAttributes : function() {
335
+ return _.clone(this._previousAttributes);
336
+ },
337
+
338
+ // Run validation against a set of incoming attributes, returning `true`
339
+ // if all is well. If a specific `error` callback has been passed,
340
+ // call that instead of firing the general `"error"` event.
341
+ _performValidation : function(attrs, options) {
342
+ var error = this.validate(attrs);
343
+ if (error) {
344
+ if (options.error) {
345
+ options.error(this, error);
346
+ } else {
347
+ this.trigger('error', this, error);
348
+ }
349
+ return false;
350
+ }
351
+ return true;
352
+ }
353
+
354
+ });
355
+
356
+ // Backbone.Collection
357
+ // -------------------
358
+
359
+ // Provides a standard collection class for our sets of models, ordered
360
+ // or unordered. If a `comparator` is specified, the Collection will maintain
361
+ // its models in sort order, as they're added and removed.
362
+ Backbone.Collection = function(models, options) {
363
+ options || (options = {});
364
+ if (options.comparator) {
365
+ this.comparator = options.comparator;
366
+ delete options.comparator;
367
+ }
368
+ this._boundOnModelEvent = _.bind(this._onModelEvent, this);
369
+ this._reset();
370
+ if (models) this.refresh(models, {silent: true});
371
+ if (this.initialize) this.initialize(models, options);
372
+ };
373
+
374
+ // Define the Collection's inheritable methods.
375
+ _.extend(Backbone.Collection.prototype, Backbone.Events, {
376
+
377
+ // The default model for a collection is just a **Backbone.Model**.
378
+ // This should be overridden in most cases.
379
+ model : Backbone.Model,
380
+
381
+ // The JSON representation of a Collection is an array of the
382
+ // models' attributes.
383
+ toJSON : function() {
384
+ return this.map(function(model){ return model.toJSON(); });
385
+ },
386
+
387
+ // Add a model, or list of models to the set. Pass **silent** to avoid
388
+ // firing the `added` event for every new model.
389
+ add : function(models, options) {
390
+ if (_.isArray(models)) {
391
+ for (var i = 0, l = models.length; i < l; i++) {
392
+ this._add(models[i], options);
393
+ }
394
+ } else {
395
+ this._add(models, options);
396
+ }
397
+ return this;
398
+ },
399
+
400
+ // Remove a model, or a list of models from the set. Pass silent to avoid
401
+ // firing the `removed` event for every model removed.
402
+ remove : function(models, options) {
403
+ if (_.isArray(models)) {
404
+ for (var i = 0, l = models.length; i < l; i++) {
405
+ this._remove(models[i], options);
406
+ }
407
+ } else {
408
+ this._remove(models, options);
409
+ }
410
+ return this;
411
+ },
412
+
413
+ // Get a model from the set by id.
414
+ get : function(id) {
415
+ return id && this._byId[id.id != null ? id.id : id];
416
+ },
417
+
418
+ // Get a model from the set by client id.
419
+ getByCid : function(cid) {
420
+ return cid && this._byCid[cid.cid || cid];
421
+ },
422
+
423
+ // Get the model at the given index.
424
+ at: function(index) {
425
+ return this.models[index];
426
+ },
427
+
428
+ // Force the collection to re-sort itself. You don't need to call this under normal
429
+ // circumstances, as the set will maintain sort order as each item is added.
430
+ sort : function(options) {
431
+ options || (options = {});
432
+ if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
433
+ this.models = this.sortBy(this.comparator);
434
+ if (!options.silent) this.trigger('refresh', this);
435
+ return this;
436
+ },
437
+
438
+ // Pluck an attribute from each model in the collection.
439
+ pluck : function(attr) {
440
+ return _.map(this.models, function(model){ return model.get(attr); });
441
+ },
442
+
443
+ // When you have more items than you want to add or remove individually,
444
+ // you can refresh the entire set with a new list of models, without firing
445
+ // any `added` or `removed` events. Fires `refresh` when finished.
446
+ refresh : function(models, options) {
447
+ models || (models = []);
448
+ options || (options = {});
449
+ this._reset();
450
+ this.add(models, {silent: true});
451
+ if (!options.silent) this.trigger('refresh', this);
452
+ return this;
453
+ },
454
+
455
+ // Fetch the default set of models for this collection, refreshing the
456
+ // collection when they arrive.
457
+ fetch : function(options) {
458
+ options || (options = {});
459
+ var collection = this;
460
+ var success = function(resp) {
461
+ collection.refresh(collection.parse(resp));
462
+ if (options.success) options.success(collection, resp);
463
+ };
464
+ var error = options.error && _.bind(options.error, null, collection);
465
+ Backbone.sync('read', this, success, error);
466
+ return this;
467
+ },
468
+
469
+ // Create a new instance of a model in this collection. After the model
470
+ // has been created on the server, it will be added to the collection.
471
+ create : function(model, options) {
472
+ var coll = this;
473
+ options || (options = {});
474
+ if (!(model instanceof Backbone.Model)) {
475
+ model = new this.model(model, {collection: coll});
476
+ } else {
477
+ model.collection = coll;
478
+ }
479
+ var success = function(nextModel, resp) {
480
+ coll.add(nextModel);
481
+ if (options.success) options.success(nextModel, resp);
482
+ };
483
+ return model.save(null, {success : success, error : options.error});
484
+ },
485
+
486
+ // **parse** converts a response into a list of models to be added to the
487
+ // collection. The default implementation is just to pass it through.
488
+ parse : function(resp) {
489
+ return resp;
490
+ },
491
+
492
+ // Proxy to _'s chain. Can't be proxied the same way the rest of the
493
+ // underscore methods are proxied because it relies on the underscore
494
+ // constructor.
495
+ chain: function () {
496
+ return _(this.models).chain();
497
+ },
498
+
499
+ // Reset all internal state. Called when the collection is refreshed.
500
+ _reset : function(options) {
501
+ this.length = 0;
502
+ this.models = [];
503
+ this._byId = {};
504
+ this._byCid = {};
505
+ },
506
+
507
+ // Internal implementation of adding a single model to the set, updating
508
+ // hash indexes for `id` and `cid` lookups.
509
+ _add : function(model, options) {
510
+ options || (options = {});
511
+ if (!(model instanceof Backbone.Model)) {
512
+ model = new this.model(model, {collection: this});
513
+ }
514
+ var already = this.getByCid(model);
515
+ if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
516
+ this._byId[model.id] = model;
517
+ this._byCid[model.cid] = model;
518
+ model.collection = this;
519
+ var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length;
520
+ this.models.splice(index, 0, model);
521
+ model.bind('all', this._boundOnModelEvent);
522
+ this.length++;
523
+ if (!options.silent) model.trigger('add', model, this);
524
+ return model;
525
+ },
526
+
527
+ // Internal implementation of removing a single model from the set, updating
528
+ // hash indexes for `id` and `cid` lookups.
529
+ _remove : function(model, options) {
530
+ options || (options = {});
531
+ model = this.getByCid(model);
532
+ if (!model) return null;
533
+ delete this._byId[model.id];
534
+ delete this._byCid[model.cid];
535
+ delete model.collection;
536
+ this.models.splice(this.indexOf(model), 1);
537
+ this.length--;
538
+ if (!options.silent) model.trigger('remove', model, this);
539
+ model.unbind('all', this._boundOnModelEvent);
540
+ return model;
541
+ },
542
+
543
+ // Internal method called every time a model in the set fires an event.
544
+ // Sets need to update their indexes when models change ids. All other
545
+ // events simply proxy through.
546
+ _onModelEvent : function(ev, model) {
547
+ if (ev === 'change:id') {
548
+ delete this._byId[model.previous('id')];
549
+ this._byId[model.id] = model;
550
+ }
551
+ this.trigger.apply(this, arguments);
552
+ }
553
+
554
+ });
555
+
556
+ // Underscore methods that we want to implement on the Collection.
557
+ var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
558
+ 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
559
+ 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
560
+ 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty'];
561
+
562
+ // Mix in each Underscore method as a proxy to `Collection#models`.
563
+ _.each(methods, function(method) {
564
+ Backbone.Collection.prototype[method] = function() {
565
+ return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
566
+ };
567
+ });
568
+
569
+ // Backbone.Controller
570
+ // -------------------
571
+
572
+ // Controllers map faux-URLs to actions, and fire events when routes are
573
+ // matched. Creating a new one sets its `routes` hash, if not set statically.
574
+ Backbone.Controller = function(options) {
575
+ options || (options = {});
576
+ if (options.routes) this.routes = options.routes;
577
+ this._bindRoutes();
578
+ if (this.initialize) this.initialize(options);
579
+ };
580
+
581
+ // Cached regular expressions for matching named param parts and splatted
582
+ // parts of route strings.
583
+ var namedParam = /:([\w\d]+)/g;
584
+ var splatParam = /\*([\w\d]+)/g;
585
+
586
+ // Set up all inheritable **Backbone.Controller** properties and methods.
587
+ _.extend(Backbone.Controller.prototype, Backbone.Events, {
588
+
589
+ // Manually bind a single named route to a callback. For example:
590
+ //
591
+ // this.route('search/:query/p:num', 'search', function(query, num) {
592
+ // ...
593
+ // });
594
+ //
595
+ route : function(route, name, callback) {
596
+ Backbone.history || (Backbone.history = new Backbone.History);
597
+ if (!_.isRegExp(route)) route = this._routeToRegExp(route);
598
+ Backbone.history.route(route, _.bind(function(fragment) {
599
+ var args = this._extractParameters(route, fragment);
600
+ callback.apply(this, args);
601
+ this.trigger.apply(this, ['route:' + name].concat(args));
602
+ }, this));
603
+ },
604
+
605
+ // Simple proxy to `Backbone.history` to save a fragment into the history,
606
+ // without triggering routes.
607
+ saveLocation : function(fragment) {
608
+ Backbone.history.saveLocation(fragment);
609
+ },
610
+
611
+ // Bind all defined routes to `Backbone.history`.
612
+ _bindRoutes : function() {
613
+ if (!this.routes) return;
614
+ for (var route in this.routes) {
615
+ var name = this.routes[route];
616
+ this.route(route, name, this[name]);
617
+ }
618
+ },
619
+
620
+ // Convert a route string into a regular expression, suitable for matching
621
+ // against the current location fragment.
622
+ _routeToRegExp : function(route) {
623
+ route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)");
624
+ return new RegExp('^' + route + '$');
625
+ },
626
+
627
+ // Given a route, and a URL fragment that it matches, return the array of
628
+ // extracted parameters.
629
+ _extractParameters : function(route, fragment) {
630
+ return route.exec(fragment).slice(1);
631
+ }
632
+
633
+ });
634
+
635
+ // Backbone.History
636
+ // ----------------
637
+
638
+ // Handles cross-browser history management, based on URL hashes. If the
639
+ // browser does not support `onhashchange`, falls back to polling.
640
+ Backbone.History = function() {
641
+ this.handlers = [];
642
+ this.fragment = this.getFragment();
643
+ _.bindAll(this, 'checkUrl');
644
+ };
645
+
646
+ // Cached regex for cleaning hashes.
647
+ var hashStrip = /^#*/;
648
+
649
+ // Set up all inheritable **Backbone.History** properties and methods.
650
+ _.extend(Backbone.History.prototype, {
651
+
652
+ // The default interval to poll for hash changes, if necessary, is
653
+ // twenty times a second.
654
+ interval: 50,
655
+
656
+ // Get the cross-browser normalized URL fragment.
657
+ getFragment : function(loc) {
658
+ return (loc || window.location).hash.replace(hashStrip, '');
659
+ },
660
+
661
+ // Start the hash change handling, returning `true` if the current URL matches
662
+ // an existing route, and `false` otherwise.
663
+ start : function() {
664
+ var docMode = document.documentMode;
665
+ var oldIE = ($.browser.msie && docMode < 7);
666
+ if (oldIE) {
667
+ this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
668
+ }
669
+ if ('onhashchange' in window && !oldIE) {
670
+ $(window).bind('hashchange', this.checkUrl);
671
+ } else {
672
+ setInterval(this.checkUrl, this.interval);
673
+ }
674
+ return this.loadUrl();
675
+ },
676
+
677
+ // Add a route to be tested when the hash changes. Routes are matched in the
678
+ // order they are added.
679
+ route : function(route, callback) {
680
+ this.handlers.push({route : route, callback : callback});
681
+ },
682
+
683
+ // Checks the current URL to see if it has changed, and if it has,
684
+ // calls `loadUrl`, normalizing across the hidden iframe.
685
+ checkUrl : function() {
686
+ var current = this.getFragment();
687
+ if (current == this.fragment && this.iframe) {
688
+ current = this.getFragment(this.iframe.location);
689
+ }
690
+ if (current == this.fragment ||
691
+ current == decodeURIComponent(this.fragment)) return false;
692
+ if (this.iframe) {
693
+ window.location.hash = this.iframe.location.hash = current;
694
+ }
695
+ this.loadUrl();
696
+ },
697
+
698
+ // Attempt to load the current URL fragment. If a route succeeds with a
699
+ // match, returns `true`. If no defined routes matches the fragment,
700
+ // returns `false`.
701
+ loadUrl : function() {
702
+ var fragment = this.fragment = this.getFragment();
703
+ var matched = _.any(this.handlers, function(handler) {
704
+ if (handler.route.test(fragment)) {
705
+ handler.callback(fragment);
706
+ return true;
707
+ }
708
+ });
709
+ return matched;
710
+ },
711
+
712
+ // Save a fragment into the hash history. You are responsible for properly
713
+ // URL-encoding the fragment in advance. This does not trigger
714
+ // a `hashchange` event.
715
+ saveLocation : function(fragment) {
716
+ fragment = (fragment || '').replace(hashStrip, '');
717
+ if (this.fragment == fragment) return;
718
+ window.location.hash = this.fragment = fragment;
719
+ if (this.iframe && (fragment != this.getFragment(this.iframe.location))) {
720
+ this.iframe.document.open().close();
721
+ this.iframe.location.hash = fragment;
722
+ }
723
+ }
724
+
725
+ });
726
+
727
+ // Backbone.View
728
+ // -------------
729
+
730
+ // Creating a Backbone.View creates its initial element outside of the DOM,
731
+ // if an existing element is not provided...
732
+ Backbone.View = function(options) {
733
+ this._configure(options || {});
734
+ this._ensureElement();
735
+ this.delegateEvents();
736
+ if (this.initialize) this.initialize(options);
737
+ };
738
+
739
+ // jQuery lookup, scoped to DOM elements within the current view.
740
+ // This should be prefered to global jQuery lookups, if you're dealing with
741
+ // a specific view.
742
+ var jQueryDelegate = function(selector) {
743
+ return $(selector, this.el);
744
+ };
745
+
746
+ // Cached regex to split keys for `delegate`.
747
+ var eventSplitter = /^(\w+)\s*(.*)$/;
748
+
749
+ // Set up all inheritable **Backbone.View** properties and methods.
750
+ _.extend(Backbone.View.prototype, Backbone.Events, {
751
+
752
+ // The default `tagName` of a View's element is `"div"`.
753
+ tagName : 'div',
754
+
755
+ // Attach the jQuery function as the `$` and `jQuery` properties.
756
+ $ : jQueryDelegate,
757
+ jQuery : jQueryDelegate,
758
+
759
+ // **render** is the core function that your view should override, in order
760
+ // to populate its element (`this.el`), with the appropriate HTML. The
761
+ // convention is for **render** to always return `this`.
762
+ render : function() {
763
+ return this;
764
+ },
765
+
766
+ // Remove this view from the DOM. Note that the view isn't present in the
767
+ // DOM by default, so calling this method may be a no-op.
768
+ remove : function() {
769
+ $(this.el).remove();
770
+ return this;
771
+ },
772
+
773
+ // For small amounts of DOM Elements, where a full-blown template isn't
774
+ // needed, use **make** to manufacture elements, one at a time.
775
+ //
776
+ // var el = this.make('li', {'class': 'row'}, this.model.get('title'));
777
+ //
778
+ make : function(tagName, attributes, content) {
779
+ var el = document.createElement(tagName);
780
+ if (attributes) $(el).attr(attributes);
781
+ if (content) $(el).html(content);
782
+ return el;
783
+ },
784
+
785
+ // Set callbacks, where `this.callbacks` is a hash of
786
+ //
787
+ // *{"event selector": "callback"}*
788
+ //
789
+ // {
790
+ // 'mousedown .title': 'edit',
791
+ // 'click .button': 'save'
792
+ // }
793
+ //
794
+ // pairs. Callbacks will be bound to the view, with `this` set properly.
795
+ // Uses jQuery event delegation for efficiency.
796
+ // Omitting the selector binds the event to `this.el`.
797
+ // This only works for delegate-able events: not `focus`, `blur`, and
798
+ // not `change`, `submit`, and `reset` in Internet Explorer.
799
+ delegateEvents : function(events) {
800
+ if (!(events || (events = this.events))) return;
801
+ $(this.el).unbind();
802
+ for (var key in events) {
803
+ var methodName = events[key];
804
+ var match = key.match(eventSplitter);
805
+ var eventName = match[1], selector = match[2];
806
+ var method = _.bind(this[methodName], this);
807
+ if (selector === '') {
808
+ $(this.el).bind(eventName, method);
809
+ } else {
810
+ $(this.el).delegate(selector, eventName, method);
811
+ }
812
+ }
813
+ },
814
+
815
+ // Performs the initial configuration of a View with a set of options.
816
+ // Keys with special meaning *(model, collection, id, className)*, are
817
+ // attached directly to the view.
818
+ _configure : function(options) {
819
+ if (this.options) options = _.extend({}, this.options, options);
820
+ if (options.model) this.model = options.model;
821
+ if (options.collection) this.collection = options.collection;
822
+ if (options.el) this.el = options.el;
823
+ if (options.id) this.id = options.id;
824
+ if (options.className) this.className = options.className;
825
+ if (options.tagName) this.tagName = options.tagName;
826
+ this.options = options;
827
+ },
828
+
829
+ // Ensure that the View has a DOM element to render into.
830
+ _ensureElement : function() {
831
+ if (this.el) return;
832
+ var attrs = {};
833
+ if (this.id) attrs.id = this.id;
834
+ if (this.className) attrs.className = this.className;
835
+ this.el = this.make(this.tagName, attrs);
836
+ }
837
+
838
+ });
839
+
840
+ // The self-propagating extend function that Backbone classes use.
841
+ var extend = function (protoProps, classProps) {
842
+ var child = inherits(this, protoProps, classProps);
843
+ child.extend = extend;
844
+ return child;
845
+ };
846
+
847
+ // Set up inheritance for the model, collection, and view.
848
+ Backbone.Model.extend = Backbone.Collection.extend =
849
+ Backbone.Controller.extend = Backbone.View.extend = extend;
850
+
851
+ // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
852
+ var methodMap = {
853
+ 'create': 'POST',
854
+ 'update': 'PUT',
855
+ 'delete': 'DELETE',
856
+ 'read' : 'GET'
857
+ };
858
+
859
+ // Backbone.sync
860
+ // -------------
861
+
862
+ // Override this function to change the manner in which Backbone persists
863
+ // models to the server. You will be passed the type of request, and the
864
+ // model in question. By default, uses jQuery to make a RESTful Ajax request
865
+ // to the model's `url()`. Some possible customizations could be:
866
+ //
867
+ // * Use `setTimeout` to batch rapid-fire updates into a single request.
868
+ // * Send up the models as XML instead of JSON.
869
+ // * Persist models via WebSockets instead of Ajax.
870
+ //
871
+ // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
872
+ // as `POST`, with a `_method` parameter containing the true HTTP method,
873
+ // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
874
+ // `application/json` with the model in a param named `model`.
875
+ // Useful when interfacing with server-side languages like **PHP** that make
876
+ // it difficult to read the body of `PUT` requests.
877
+ Backbone.sync = function(method, model, success, error) {
878
+ var type = methodMap[method];
879
+ var modelJSON = (method === 'create' || method === 'update') ?
880
+ JSON.stringify(model.toJSON()) : null;
881
+
882
+ // Default JSON-request options.
883
+ var params = {
884
+ url: getUrl(model),
885
+ type: type,
886
+ contentType: 'application/json',
887
+ data: modelJSON,
888
+ dataType: 'json',
889
+ processData: false,
890
+ success: success,
891
+ error: error
892
+ };
893
+
894
+ // For older servers, emulate JSON by encoding the request into an HTML-form.
895
+ if (Backbone.emulateJSON) {
896
+ params.contentType = 'application/x-www-form-urlencoded';
897
+ params.processData = true;
898
+ params.data = modelJSON ? {model : modelJSON} : {};
899
+ }
900
+
901
+ // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
902
+ // And an `X-HTTP-Method-Override` header.
903
+ if (Backbone.emulateHTTP) {
904
+ if (type === 'PUT' || type === 'DELETE') {
905
+ if (Backbone.emulateJSON) params.data._method = type;
906
+ params.type = 'POST';
907
+ params.beforeSend = function(xhr) {
908
+ xhr.setRequestHeader("X-HTTP-Method-Override", type);
909
+ };
910
+ }
911
+ }
912
+
913
+ // Make the request.
914
+ $.ajax(params);
915
+ };
916
+
917
+ // Helpers
918
+ // -------
919
+
920
+ // Shared empty constructor function to aid in prototype-chain creation.
921
+ var ctor = function(){};
922
+
923
+ // Helper function to correctly set up the prototype chain, for subclasses.
924
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and
925
+ // class properties to be extended.
926
+ var inherits = function(parent, protoProps, staticProps) {
927
+ var child;
928
+
929
+ // The constructor function for the new subclass is either defined by you
930
+ // (the "constructor" property in your `extend` definition), or defaulted
931
+ // by us to simply call `super()`.
932
+ if (protoProps && protoProps.hasOwnProperty('constructor')) {
933
+ child = protoProps.constructor;
934
+ } else {
935
+ child = function(){ return parent.apply(this, arguments); };
936
+ }
937
+
938
+ // Set the prototype chain to inherit from `parent`, without calling
939
+ // `parent`'s constructor function.
940
+ ctor.prototype = parent.prototype;
941
+ child.prototype = new ctor();
942
+
943
+ // Add prototype properties (instance properties) to the subclass,
944
+ // if supplied.
945
+ if (protoProps) _.extend(child.prototype, protoProps);
946
+
947
+ // Add static properties to the constructor function, if supplied.
948
+ if (staticProps) _.extend(child, staticProps);
949
+
950
+ // Correctly set child's `prototype.constructor`, for `instanceof`.
951
+ child.prototype.constructor = child;
952
+
953
+ // Set a convenience property in case the parent's prototype is needed later.
954
+ child.__super__ = parent.prototype;
955
+
956
+ return child;
957
+ };
958
+
959
+ // Helper function to get a URL from a Model or Collection as a property
960
+ // or as a function.
961
+ var getUrl = function(object) {
962
+ if (!(object && object.url)) throw new Error("A 'url' property or function must be specified");
963
+ return _.isFunction(object.url) ? object.url() : object.url;
964
+ };
965
+
966
+ })();