jipe 1.0.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/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Add dependencies to develop your gem here.
4
+ # Include everything needed to run rake, tests, features, etc.
5
+ group :development do
6
+ gem "bundler", "~> 1.0.0"
7
+ gem "jeweler", "~> 1.5.2"
8
+ end
@@ -0,0 +1,16 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ git (1.2.5)
5
+ jeweler (1.5.2)
6
+ bundler (~> 1.0.0)
7
+ git (>= 1.2.5)
8
+ rake
9
+ rake (0.8.7)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ bundler (~> 1.0.0)
16
+ jeweler (~> 1.5.2)
data/README ADDED
@@ -0,0 +1,47 @@
1
+ JIPE: In-Place Editing Components for Jester
2
+ ============================================
3
+
4
+ JIPE stands for Jester In-Place Editing. It started out as a replacement for
5
+ Rails' built-in in_place_editor helper function using Eric Mill's Jester
6
+ library, but is beginning to add additional editing widgets as well.
7
+
8
+ The upshot of this is that if you have RESTful controllers in your Rails app,
9
+ then JIPE can very quickly help you set up in-place editing for your RESTful
10
+ objects.
11
+
12
+ Setup
13
+ =====
14
+
15
+ To begin using JIPE in your project, run:
16
+
17
+ script/generate jipe
18
+
19
+ This will copy the JIPE icons into your public/images directory.
20
+
21
+ Usage
22
+ =====
23
+
24
+ To use JIPE, you'll need to include the Javascripts in your layout:
25
+
26
+ <%= javascript_include_tag 'jipe/jester' %>
27
+ <%= javascript_include_tag 'jipe/jipe' %>
28
+
29
+ This requires Rails 2.3, since it makes use of the Engines-like features. For
30
+ older versions of Rails, you will need to either install the Engines plugin,
31
+ or copy the Javascripts into your public/javascript directory yourself.
32
+
33
+ You'll also need to tell Jester how to access your REST resources, like so:
34
+
35
+ <script type="text/javascript">
36
+ Resource.model("Post");
37
+ </script>
38
+
39
+ Now that you've done that, creating in-place editing fields is easy:
40
+
41
+ Title: <%= jipe_editor @post, "title" %><br/>
42
+ <%= jipe_editor @post, "body", :rows => 4 %>
43
+
44
+ In addition to textbox-style in-place editors, JIPE lets you do clickable image
45
+ toggles for boolean fields. For example:
46
+
47
+ Mood: <%= jipe_image_toggle @post, "is_happy", "smiley.png", "frowny.png" %>
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
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
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "jipe"
16
+ gem.homepage = "http://github.com/nbudin/jipe"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{RESTful In-place editors for Rails using Jester}
19
+ gem.email = "natbudin@gmail.com"
20
+ gem.authors = ["Nat Budin"]
21
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
22
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
23
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
24
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ require 'rake/rdoctask'
30
+ desc 'Default: run unit tests.'
31
+ task :default => :test
32
+
33
+ desc 'Test the jipe plugin.'
34
+ Rake::TestTask.new(:test) do |t|
35
+ t.libs << 'lib'
36
+ t.pattern = 'test/**/*_test.rb'
37
+ t.verbose = true
38
+ end
39
+
40
+ desc 'Generate documentation for the jipe plugin.'
41
+ Rake::RDocTask.new(:rdoc) do |rdoc|
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = 'Jipe'
44
+ rdoc.options << '--line-numbers' << '--inline-source'
45
+ rdoc.rdoc_files.include('README')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,17 @@
1
+ class JipeController < ApplicationController
2
+ unloadable
3
+ layout nil
4
+ caches_page :jester, :jipe
5
+
6
+ def jester
7
+ respond_to do |format|
8
+ format.js {}
9
+ end
10
+ end
11
+
12
+ def jipe
13
+ respond_to do |format|
14
+ format.js {}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,860 @@
1
+ // Jester version 1.5
2
+ // Released October 25th, 2007
3
+
4
+ // Compatible, tested with Prototype 1.6.0.2
5
+
6
+ // Copyright 2007, thoughtbot, inc.
7
+ // Released under the MIT License.
8
+
9
+ Jester = {}
10
+ Jester.Resource = function(){};
11
+
12
+ // Doing it this way forces the validation of the syntax but gives flexibility enough to rename the new class.
13
+ Jester.Constructor = function(model){
14
+ return (function CONSTRUCTOR() {
15
+ this.klass = CONSTRUCTOR;
16
+ this.initialize.apply(this, arguments);
17
+ this.after_initialization.apply(this, arguments);
18
+ }).toString().replace(/CONSTRUCTOR/g, model);
19
+ }
20
+
21
+ // universal Jester callback holder for remote JSON loading
22
+ var jesterCallback = null;
23
+
24
+ Object.extend(Jester.Resource, {
25
+ model: function(model, options)
26
+ {
27
+ var new_model = null;
28
+ new_model = eval(model + " = " + Jester.Constructor(model));
29
+ new_model.prototype = new Jester.Resource();
30
+ Object.extend(new_model, Jester.Resource);
31
+
32
+ // We delay instantiating XML.ObjTree() so that it can be listed at the end of this file instead of the beginning
33
+ if (!Jester.Tree) {
34
+ Jester.Tree = new XML.ObjTree();
35
+ Jester.Tree.attr_prefix = "@";
36
+ }
37
+ if (!options) options = {};
38
+
39
+ var default_options = {
40
+ format: "xml",
41
+ singular: model.underscore(),
42
+ name: model
43
+ }
44
+ options = Object.extend(default_options, options);
45
+ options.format = options.format.toLowerCase();
46
+ options.plural = options.singular.pluralize(options.plural);
47
+ options.singular_xml = options.singular.replace(/_/g, "-");
48
+ options.plural_xml = options.plural.replace(/_/g, "-");
49
+ options.remote = false;
50
+
51
+ // Establish prefix
52
+ var default_prefix = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "");
53
+ if (options.prefix && options.prefix.match(/^https?:/))
54
+ options.remote = true;
55
+
56
+ if (!options.prefix)
57
+ options.prefix = default_prefix;
58
+
59
+ if (!options.prefix.match(/^(https?|file):/))
60
+ options.prefix = default_prefix + (options.prefix.match(/^\//) ? "" : "/") + options.prefix;
61
+
62
+ options.prefix = options.prefix.replace(/\b\/+$/,"");
63
+
64
+ // Establish custom URLs
65
+ options.urls = Object.extend(this._default_urls(options), options.urls);
66
+
67
+ // Assign options to model
68
+ new_model.name = model;
69
+ new_model.options = options;
70
+ for(var opt in options)
71
+ new_model["_" + opt] = options[opt];
72
+
73
+ // Establish custom URL helpers
74
+ for (var url in options.urls)
75
+ eval('new_model._' + url + '_url = function(params) {return this._url_for("' + url + '", params);}');
76
+
77
+ if (options.checkNew)
78
+ this.buildAttributes(new_model, options);
79
+
80
+ if (window)
81
+ window[model] = new_model;
82
+
83
+ return new_model;
84
+ },
85
+
86
+ buildAttributes: function(model, options) {
87
+ model = model || this;
88
+ var async = options.asynchronous;
89
+
90
+ if (async == null)
91
+ async = true;
92
+
93
+ var buildWork = bind(model, function(doc) {
94
+ if (this._format == "json")
95
+ this._attributes = this._attributesFromJSON(doc);
96
+ else
97
+ this._attributes = this._attributesFromTree(doc[this._singular_xml]);
98
+ });
99
+ model.requestAndParse(options.format, buildWork, model._new_url(), {asynchronous: async});
100
+ },
101
+
102
+ loadRemoteJSON : function(url, callback, user_callback) {
103
+ // tack on user_callback if there is one, and only if it's really a function
104
+ if (typeof(user_callback) == "function")
105
+ jesterCallback = function(doc) {user_callback(callback(doc));}
106
+ else
107
+ jesterCallback = callback;
108
+
109
+ var script = document.createElement("script");
110
+ script.type = "text/javascript";
111
+
112
+ if (url.indexOf("?") == -1)
113
+ url += "?";
114
+ else
115
+ url += "&";
116
+ url += "callback=jesterCallback";
117
+ script.src = url;
118
+
119
+ document.firstChild.appendChild(script);
120
+ },
121
+
122
+ requestAndParse : function(format, callback, url, options, user_callback, remote) {
123
+ if (remote && format == "json" && user_callback)
124
+ return this.loadRemoteJSON(url, callback, user_callback)
125
+
126
+ parse_and_callback = null;
127
+ if (format.toLowerCase() == "json") {
128
+ parse_and_callback = function(transport) {
129
+ if (transport.status == 500) return callback(null);
130
+ eval("var attributes = " + transport.responseText); // hashes need this kind of eval
131
+ return callback(attributes);
132
+ }
133
+ } else {
134
+ parse_and_callback = function(transport) {
135
+ if (transport.status == 500) return callback(null);
136
+ return callback(Jester.Tree.parseXML(transport.responseText));
137
+ }
138
+ }
139
+
140
+ // most parse requests are going to be a GET
141
+ if (!(options.postBody || options.parameters || options.postbody || options.method == "post")) {
142
+ options.method = "get";
143
+ }
144
+
145
+ return this.request(parse_and_callback, url, options, user_callback);
146
+ },
147
+
148
+ // Helper to aid in handling either async or synchronous requests
149
+ request : function(callback, url, options, user_callback) {
150
+ if (user_callback) {
151
+ options.asynchronous = true;
152
+ // if an options hash was given instead of a callback
153
+ if (typeof(user_callback) == "object") {
154
+ for (var x in user_callback)
155
+ options[x] = user_callback[x];
156
+ user_callback = options.onComplete;
157
+ }
158
+ }
159
+ else
160
+ user_callback = function(arg){return arg;}
161
+
162
+ if (options.asynchronous) {
163
+ options.onComplete = function(transport, json) {user_callback(callback(transport), json);}
164
+ return new Ajax.Request(url, options).transport;
165
+ }
166
+ else
167
+ {
168
+ options.asynchronous = false; // Make sure it's set, to avoid being overridden.
169
+ return callback(new Ajax.Request(url, options).transport);
170
+ }
171
+ },
172
+
173
+ find : function(id, params, callback) {
174
+ // allow a params hash to be omitted and a callback function given directly
175
+ if (!callback && typeof(params) == "function") {
176
+ callback = params;
177
+ params = null;
178
+ }
179
+
180
+ var findAllWork = bind(this, function(doc) {
181
+ if (!doc) return null;
182
+
183
+ var collection = this._loadCollection(doc);
184
+
185
+ if (!collection) return null;
186
+
187
+ // This is better than requiring the controller to support a "limit" parameter
188
+ if (id == "first")
189
+ return collection[0];
190
+
191
+ return collection;
192
+ });
193
+
194
+ var findOneWork = bind(this, function(doc) {
195
+ if (!doc) return null;
196
+
197
+ var base = this._loadSingle(doc);
198
+
199
+ // if there were no properties, it was probably not actually loaded
200
+ if (!base || base._properties.length == 0) return null;
201
+
202
+ // even if the ID didn't come back, we obviously knew the ID to search with, so set it
203
+ if (!base._properties.include("id")) base._setAttribute("id", parseInt(id))
204
+
205
+ return base;
206
+ });
207
+
208
+ if (id == "first" || id == "all") {
209
+ var url = this._list_url(params);
210
+ return this.requestAndParse(this._format, findAllWork, url, {}, callback, this._remote);
211
+ }
212
+ else {
213
+ if (isNaN(parseInt(id))) return null;
214
+ if (!params) params = {};
215
+ params.id = id;
216
+
217
+ var url = this._show_url(params);
218
+ return this.requestAndParse(this._format, findOneWork, url, {}, callback, this._remote);
219
+ }
220
+ },
221
+
222
+ build : function(attributes) {
223
+ return new this(attributes);
224
+ },
225
+
226
+ create : function(attributes, callback) {
227
+ var base = new this(attributes);
228
+
229
+ createWork = bind(this, function(saved) {
230
+ return callback(base);
231
+ });
232
+
233
+ if (callback) {
234
+ return base.save(createWork);
235
+ }
236
+ else {
237
+ base.save();
238
+ return base;
239
+ }
240
+ },
241
+
242
+ // Destroys a REST object. Can be used as follows:
243
+ // object.destroy() - when called on an instance of a model, destroys that instance
244
+ // Model.destroy(1) - destroys the Model object with ID 1
245
+ // Model.destroy({parent: 3, id: 1}) - destroys the Model object with Parent ID 3 and ID 1
246
+ //
247
+ // Any of these forms can also be passed a callback function as an additional parameter and it works as you expect.
248
+ destroy : function(params, callback) {
249
+ if (typeof(params) == "function") {
250
+ callback = params;
251
+ params = null;
252
+ }
253
+ if (typeof(params) == "number") {
254
+ params = {id: params};
255
+ }
256
+ params.id = params.id || this.id;
257
+ if (!params.id) return false;
258
+
259
+ var destroyWork = bind(this, function(transport) {
260
+ if (transport.status == 200) {
261
+ if (!params.id || this.id == params.id)
262
+ this.id = null;
263
+ return this;
264
+ }
265
+ else
266
+ return false;
267
+ });
268
+
269
+ return this.request(destroyWork, this._destroy_url(params), {method: "delete"}, callback);
270
+ },
271
+
272
+ _interpolate: function(string, params) {
273
+ if (!params) return string;
274
+
275
+ var result = string;
276
+ params.each(function(pair) {
277
+ var re = new RegExp(":" + pair.key, "g");
278
+ if (result.match(re)) {
279
+ result = result.replace(re, pair.value);
280
+ params.unset(pair.key);
281
+ }
282
+ });
283
+ return result;
284
+ },
285
+
286
+ _url_for : function(action, params) {
287
+ if (!this._urls[action]) return "";
288
+ // if an integer is sent, it's assumed just the ID is a parameter
289
+ if (typeof(params) == "number") params = {id: params}
290
+
291
+ if (params) params = $H(params);
292
+
293
+ var url = this._interpolate(this._prefix + this._urls[action], params)
294
+ return url + (params && params.any() ? "?" + params.toQueryString() : "");
295
+ },
296
+
297
+ _default_urls : function(options) {
298
+ urls = {
299
+ 'show' : "/" + options.plural + "/:id." + options.format,
300
+ 'list' : "/" + options.plural + "." + options.format,
301
+ 'new' : "/" + options.plural + "/new." + options.format
302
+ }
303
+ urls.create = urls.list;
304
+ urls.destroy = urls.update = urls.show;
305
+
306
+ return urls;
307
+ },
308
+
309
+ // Converts a JSON hash returns from ActiveRecord::Base#to_json into a hash of attribute values
310
+ // Does not handle associations, as AR's #to_json doesn't either
311
+ // Also, JSON doesn't include room to store types, so little auto-transforming is done here (just on 'id')
312
+ _attributesFromJSON : function(json) {
313
+ if (!json || json.constructor != Object) return false;
314
+ if (json.attributes) json = json.attributes;
315
+
316
+ var attributes = {};
317
+ var i = 0;
318
+ for (var attr in json) {
319
+ var value = json[attr];
320
+ if (attr == "id")
321
+ value = parseInt(value);
322
+ else if (attr.match(/(created_at|created_on|updated_at|updated_on)/)) {
323
+ var date = Date.parse(value);
324
+ if (date && !isNaN(date)) value = date;
325
+ }
326
+ attributes[attr] = value;
327
+ i += 1;
328
+ }
329
+ if (i == 0) return false; // empty hashes should just return false
330
+
331
+ return attributes;
332
+ },
333
+
334
+ // Converts the XML tree returned from a single object into a hash of attribute values
335
+ _attributesFromTree : function(elements) {
336
+ var attributes = {}
337
+ for (var attr in elements) {
338
+ // pull out the value
339
+ var value = elements[attr];
340
+ if (elements[attr] && elements[attr]["@type"]) {
341
+ if (elements[attr]["#text"])
342
+ value = elements[attr]["#text"];
343
+ else
344
+ value = undefined;
345
+ }
346
+
347
+ // handle empty value (pass it through)
348
+ if (!value) {}
349
+
350
+ // handle scalars
351
+ else if (typeof(value) == "string") {
352
+ // perform any useful type transformations
353
+ if (elements[attr]["@type"] == "integer") {
354
+ var num = parseInt(value);
355
+ if (!isNaN(num)) value = num;
356
+ }
357
+ else if (elements[attr]["@type"] == "boolean")
358
+ value = (value == "true");
359
+ else if (elements[attr]["@type"] == "datetime") {
360
+ var date = Date.parse(value);
361
+ if (!isNaN(date)) value = date;
362
+ }
363
+ }
364
+ // handle arrays (associations)
365
+ else {
366
+ var relation = value; // rename for clarity in the context of an association
367
+
368
+ // first, detect if it's has_one/belongs_to, or has_many
369
+ var i = 0;
370
+ var singular = null;
371
+ var has_many = false;
372
+ for (var val in relation) {
373
+ if (i == 0)
374
+ singular = val;
375
+ i += 1;
376
+ }
377
+
378
+ // has_many
379
+ if (relation[singular] && typeof(relation[singular]) == "object" && i == 1) {
380
+ var value = [];
381
+ var plural = attr;
382
+ var name = singular.camelize().capitalize();
383
+
384
+ // force array
385
+ if (!(elements[plural][singular].length > 0))
386
+ elements[plural][singular] = [elements[plural][singular]];
387
+
388
+ elements[plural][singular].each( bind(this, function(single) {
389
+ // if the association hasn't been modeled, do a default modeling here
390
+ // hosted object's prefix and format are inherited, singular and plural are set
391
+ // from the XML
392
+ if (eval("typeof(" + name + ")") == "undefined") {
393
+ Jester.Resource.model(name, {prefix: this._prefix, singular: singular, plural: plural, format: this._format});
394
+ }
395
+ var base = eval(name + ".build(this._attributesFromTree(single))");
396
+ value.push(base);
397
+ }));
398
+ }
399
+ // has_one or belongs_to
400
+ else {
401
+ singular = attr;
402
+ var name = singular.capitalize();
403
+
404
+ // if the association hasn't been modeled, do a default modeling here
405
+ // hosted object's prefix and format are inherited, singular is set from the XML
406
+ if (eval("typeof(" + name + ")") == "undefined") {
407
+ Jester.Resource.model(name, {prefix: this._prefix, singular: singular, format: this._format});
408
+ }
409
+ value = eval(name + ".build(this._attributesFromTree(value))");
410
+ }
411
+ }
412
+
413
+ // transform attribute name if needed
414
+ attribute = attr.replace(/-/g, "_");
415
+ attributes[attribute] = value;
416
+ }
417
+
418
+ return attributes;
419
+ },
420
+
421
+ _loadSingle : function(doc) {
422
+ var attributes;
423
+ if (this._format == "json")
424
+ attributes = this._attributesFromJSON(doc);
425
+ else
426
+ attributes = this._attributesFromTree(doc[this._singular_xml]);
427
+
428
+ return this.build(attributes);
429
+ },
430
+
431
+ _loadCollection : function(doc) {
432
+ var collection;
433
+ if (this._format == "json") {
434
+ collection = doc.map( bind(this, function(item) {
435
+ return this.build(this._attributesFromJSON(item));
436
+ }));
437
+ }
438
+ else {
439
+ // if only one result, wrap it in an array
440
+ if (!Jester.Resource.elementHasMany(doc[this._plural_xml]))
441
+ doc[this._plural_xml][this._singular_xml] = [doc[this._plural_xml][this._singular_xml]];
442
+
443
+ collection = doc[this._plural_xml][this._singular_xml].map( bind(this, function(elem) {
444
+ return this.build(this._attributesFromTree(elem));
445
+ }));
446
+ }
447
+ return collection;
448
+ }
449
+
450
+ });
451
+
452
+ Object.extend(Jester.Resource.prototype, {
453
+ initialize : function(attributes) {
454
+ // Initialize no attributes, no associations
455
+ this._properties = [];
456
+ this._associations = [];
457
+
458
+ this.setAttributes(this.klass._attributes || {});
459
+ this.setAttributes(attributes);
460
+
461
+ // Initialize with no errors
462
+ this.errors = [];
463
+
464
+ // Establish custom URL helpers
465
+ for (var url in this.klass._urls)
466
+ eval('this._' + url + '_url = function(params) {return this._url_for("' + url + '", params);}');
467
+ },
468
+ after_initialization: function(){},
469
+
470
+ new_record : function() {return !(this.id);},
471
+ valid : function() {return ! this.errors.any();},
472
+
473
+ reload : function(callback) {
474
+ var reloadWork = bind(this, function(copy) {
475
+ this._resetAttributes(copy.attributes(true));
476
+
477
+ if (callback)
478
+ return callback(this);
479
+ else
480
+ return this;
481
+ });
482
+
483
+ if (this.id) {
484
+ if (callback)
485
+ return this.klass.find(this.id, {}, reloadWork);
486
+ else
487
+ return reloadWork(this.klass.find(this.id));
488
+ }
489
+ else
490
+ return this;
491
+ },
492
+
493
+ // Destroys a REST object. Can be used as follows:
494
+ // object.destroy() - when called on an instance of a model, destroys that instance
495
+ // Model.destroy(1) - destroys the Model object with ID 1
496
+ // Model.destroy({parent: 3, id: 1}) - destroys the Model object with Parent ID 3 and ID 1
497
+ //
498
+ // Any of these forms can also be passed a callback function as an additional parameter and it works as you expect.
499
+ destroy : function(params, callback) {
500
+ if (params === undefined) {
501
+ params = {};
502
+ }
503
+ if (typeof(params) == "function") {
504
+ callback = params;
505
+ params = {};
506
+ }
507
+ if (typeof(params) == "number") {
508
+ params = {id: params};
509
+ }
510
+ if (!params.id) {
511
+ params.id = this.id;
512
+ }
513
+ if (!params.id) return false;
514
+
515
+ // collect params from instance if we're being called as an instance method
516
+ if (this._properties !== undefined) {
517
+ (this._properties).each( bind(this, function(value, i) {
518
+ if (params[value] === undefined) {
519
+ params[value] = this[value];
520
+ }
521
+ }));
522
+ }
523
+
524
+ var destroyWork = bind(this, function(transport) {
525
+ if (transport.status == 200) {
526
+ if (!params.id || this.id == params.id)
527
+ this.id = null;
528
+ return this;
529
+ }
530
+ else
531
+ return false;
532
+ });
533
+
534
+ return this.klass.request(destroyWork, this._destroy_url(params), {method: "delete"}, callback);
535
+ },
536
+
537
+ save : function(callback) {
538
+ var saveWork = bind(this, function(transport) {
539
+ var saved = false;
540
+
541
+ if (transport.responseText && (transport.responseText.strip() != "")) {
542
+ var errors = this._errorsFrom(transport.responseText);
543
+ if (errors)
544
+ this._setErrors(errors);
545
+ else {
546
+ var attributes;
547
+ if (this.klass._format == "json") {
548
+ attributes = this._attributesFromJSON(transport.responseText);
549
+ }
550
+ else {
551
+ var doc = Jester.Tree.parseXML(transport.responseText);
552
+ if (doc[this.klass._singular_xml])
553
+ attributes = this._attributesFromTree(doc[this.klass._singular_xml]);
554
+ }
555
+ if (attributes)
556
+ this._resetAttributes(attributes);
557
+ }
558
+ }
559
+
560
+ // Get ID from the location header if it's there
561
+ if (this.new_record() && transport.status == 201) {
562
+ loc = transport.getResponseHeader("location");
563
+ if (loc) {
564
+ id = parseInt(loc.match(/\/([^\/]*?)(\.\w+)?$/)[1]);
565
+ if (!isNaN(id))
566
+ this._setProperty("id", id)
567
+ }
568
+ }
569
+
570
+ return (transport.status >= 200 && transport.status < 300 && this.errors.length == 0);
571
+ });
572
+
573
+ // reset errors
574
+ this._setErrors([]);
575
+
576
+ var url = null;
577
+ var method = null;
578
+
579
+ // collect params
580
+ var params = {};
581
+ var urlParams = {};
582
+ (this._properties).each( bind(this, function(value, i) {
583
+ params[this.klass._singular + "[" + value + "]"] = this[value];
584
+ urlParams[value] = this[value];
585
+ }));
586
+
587
+ // distinguish between create and update
588
+ if (this.new_record()) {
589
+ url = this._create_url(urlParams);
590
+ method = "post";
591
+ }
592
+ else {
593
+ url = this._update_url(urlParams);
594
+ method = "put";
595
+ }
596
+
597
+
598
+ // send the request
599
+ return this.klass.request(saveWork, url, {parameters: params, method: method}, callback);
600
+ },
601
+
602
+ setAttributes : function(attributes)
603
+ {
604
+ $H(attributes).each(bind(this, function(attr){ this._setAttribute(attr.key, attr.value) }));
605
+ return attributes;
606
+ },
607
+
608
+ updateAttributes : function(attributes, callback)
609
+ {
610
+ this.setAttributes(attributes);
611
+ return this.save(callback);
612
+ },
613
+
614
+ // mimics ActiveRecord's behavior of omitting associations, but keeping foreign keys
615
+ attributes : function(include_associations) {
616
+ var attributes = {}
617
+ for (var i=0; i<this._properties.length; i++)
618
+ attributes[this._properties[i]] = this[this._properties[i]];
619
+ if (include_associations) {
620
+ for (var i=0; i<this._associations.length; i++)
621
+ attributes[this._associations[i]] = this[this._associations[i]];
622
+ }
623
+ return attributes;
624
+ },
625
+
626
+ /*
627
+ Internal methods.
628
+ */
629
+
630
+ _attributesFromJSON: function()
631
+ {
632
+ return this.klass._attributesFromJSON.apply(this.klass, arguments);
633
+ },
634
+
635
+ _attributesFromTree: function()
636
+ {
637
+ return this.klass._attributesFromTree.apply(this.klass, arguments);
638
+ },
639
+
640
+ _errorsFrom : function(raw) {
641
+ if (this.klass._format == "json")
642
+ return this._errorsFromJSON(raw);
643
+ else
644
+ return this._errorsFromXML(raw);
645
+ },
646
+
647
+ // Pulls errors from JSON
648
+ _errorsFromJSON : function(json) {
649
+ try {
650
+ json = eval(json); // okay for arrays
651
+ } catch(e) {
652
+ return false;
653
+ }
654
+
655
+ if (!(json && json.constructor == Array && json[0] && json[0].constructor == Array)) return false;
656
+
657
+ return json.map(function(pair) {
658
+ return pair[0].capitalize() + " " + pair[1];
659
+ });
660
+ },
661
+
662
+ // Pulls errors from XML
663
+ _errorsFromXML : function(xml) {
664
+ if (!xml) return false;
665
+ var doc = Jester.Tree.parseXML(xml);
666
+
667
+ if (doc && doc.errors) {
668
+ var errors = [];
669
+ if (typeof(doc.errors.error) == "string")
670
+ doc.errors.error = [doc.errors.error];
671
+
672
+ doc.errors.error.each(function(value, index) {
673
+ errors.push(value);
674
+ });
675
+
676
+ return errors;
677
+ }
678
+ else return false;
679
+ },
680
+
681
+ // Sets errors with an array. Could be extended at some point to include breaking error messages into pairs (attribute, msg).
682
+ _setErrors : function(errors) {
683
+ this.errors = errors;
684
+ },
685
+
686
+
687
+ // Sets all attributes and associations at once
688
+ // Deciding between the two on whether the attribute is a complex object or a scalar
689
+ _resetAttributes : function(attributes) {
690
+ this._clear();
691
+ for (var attr in attributes)
692
+ this._setAttribute(attr, attributes[attr]);
693
+ },
694
+
695
+ _setAttribute : function(attribute, value) {
696
+ if (value && typeof(value) == "object" && value.constructor != Date)
697
+ this._setAssociation(attribute, value);
698
+ else
699
+ this._setProperty(attribute, value);
700
+ },
701
+
702
+ _setProperties : function(properties) {
703
+ this._clearProperties();
704
+ for (var prop in properties)
705
+ this._setProperty(prop, properties[prop])
706
+ },
707
+
708
+ _setAssociations : function(associations) {
709
+ this._clearAssociations();
710
+ for (var assoc in associations)
711
+ this._setAssociation(assoc, associations[assoc])
712
+ },
713
+
714
+ _setProperty : function(property, value) {
715
+ this[property] = value;
716
+ if (!(this._properties.include(property)))
717
+ this._properties.push(property);
718
+ },
719
+
720
+ _setAssociation : function(association, value) {
721
+ this[association] = value;
722
+ if (!(this._associations.include(association)))
723
+ this._associations.push(association);
724
+ },
725
+
726
+ _clear : function() {
727
+ this._clearProperties();
728
+ this._clearAssociations();
729
+ },
730
+
731
+ _clearProperties : function() {
732
+ for (var i=0; i<this._properties.length; i++)
733
+ this[this._properties[i]] = null;
734
+ this._properties = [];
735
+ },
736
+
737
+ _clearAssociations : function() {
738
+ for (var i=0; i<this._associations.length; i++)
739
+ this[this._associations[i]] = null;
740
+ this._associations = [];
741
+ },
742
+
743
+ // helper URLs
744
+ _url_for : function(action, params) {
745
+ if (!params) params = this.id;
746
+ if (typeof(params) == "object" && !params.id)
747
+ params.id = this.id;
748
+
749
+ return this.klass._url_for(action, params);
750
+ }
751
+
752
+ });
753
+
754
+ // Returns true if the element has more objects beneath it, or just 1 or more attributes.
755
+ // It's not perfect, this would mess up if an object had only one attribute, and it was an array.
756
+ // For now, this is just one of the difficulties of dealing with ObjTree.
757
+ Jester.Resource.elementHasMany = function(element) {
758
+ var i = 0;
759
+ var singular = null;
760
+ var has_many = false;
761
+ for (var val in element) {
762
+ if (i == 0)
763
+ singular = val;
764
+ i += 1;
765
+ }
766
+
767
+ return (element[singular] && typeof(element[singular]) == "object" && element[singular].length != null && i == 1);
768
+ }
769
+
770
+ // This bind function is a modification of the standard Prototype bind function.
771
+ // Use this instead of Prototype's when running in XULRunner due to a longstanding
772
+ // bug in the javascript interpreter.
773
+
774
+ function bind(context, func) {
775
+ var __method = func, args = $A(func.arguments), object = context;
776
+
777
+ return function() {
778
+ return __method.apply(object, args.concat($A(arguments)));
779
+ }
780
+ }
781
+
782
+ // If there is no object already called Resource, we define one to make things a little cleaner for us.
783
+ if(typeof(Resource) == "undefined")
784
+ Resource = Jester.Resource;
785
+
786
+
787
+
788
+
789
+ /*
790
+ Inflector library, contributed graciously to Jester by Ryan Schuft.
791
+ The library in full is a complete port of Rails' Inflector, though Jester only uses its pluralization.
792
+ Its home page can be found at: http://code.google.com/p/inflection-js/
793
+ */
794
+
795
+ if (!String.prototype.pluralize) String.prototype.pluralize = function(plural) {
796
+ var str=this;
797
+ if(plural)str=plural;
798
+ else {
799
+ var uncountable_words=['equipment','information','rice','money','species','series','fish','sheep','moose'];
800
+ var uncountable=false;
801
+ for(var x=0;!uncountable&&x<uncountable_words.length;x++)uncountable=(uncountable_words[x].toLowerCase()==str.toLowerCase());
802
+ if(!uncountable) {
803
+ var rules=[
804
+ [new RegExp('(m)an$','gi'),'$1en'],
805
+ [new RegExp('(pe)rson$','gi'),'$1ople'],
806
+ [new RegExp('(child)$','gi'),'$1ren'],
807
+ [new RegExp('(ax|test)is$','gi'),'$1es'],
808
+ [new RegExp('(octop|vir)us$','gi'),'$1i'],
809
+ [new RegExp('(alias|status)$','gi'),'$1es'],
810
+ [new RegExp('(bu)s$','gi'),'$1ses'],
811
+ [new RegExp('(buffal|tomat)o$','gi'),'$1oes'],
812
+ [new RegExp('([ti])um$','gi'),'$1a'],
813
+ [new RegExp('sis$','gi'),'ses'],
814
+ [new RegExp('(?:([^f])fe|([lr])f)$','gi'),'$1$2ves'],
815
+ [new RegExp('(hive)$','gi'),'$1s'],
816
+ [new RegExp('([^aeiouy]|qu)y$','gi'),'$1ies'],
817
+ [new RegExp('(x|ch|ss|sh)$','gi'),'$1es'],
818
+ [new RegExp('(matr|vert|ind)ix|ex$','gi'),'$1ices'],
819
+ [new RegExp('([m|l])ouse$','gi'),'$1ice'],
820
+ [new RegExp('^(ox)$','gi'),'$1en'],
821
+ [new RegExp('(quiz)$','gi'),'$1zes'],
822
+ [new RegExp('s$','gi'),'s'],
823
+ [new RegExp('$','gi'),'s']
824
+ ];
825
+ var matched=false;
826
+ for(var x=0;!matched&&x<=rules.length;x++) {
827
+ matched=str.match(rules[x][0]);
828
+ if(matched)str=str.replace(rules[x][0],rules[x][1]);
829
+ }
830
+ }
831
+ }
832
+ return str;
833
+ };
834
+
835
+ /*
836
+
837
+ This is a lighter form of ObjTree, with parts Jester doesn't use removed.
838
+ Compressed using http://dean.edwards.name/packer/.
839
+ Homepage: http://www.kawa.net/works/js/xml/objtree-e.html
840
+
841
+ XML.ObjTree -- XML source code from/to JavaScript object like E4X
842
+
843
+ Copyright (c) 2005-2006 Yusuke Kawasaki. All rights reserved.
844
+ This program is free software; you can redistribute it and/or
845
+ modify it under the Artistic license. Or whatever license I choose,
846
+ which I will do instead of keeping this documentation like it is.
847
+
848
+ */
849
+
850
+ eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('5(p(o)==\'w\')o=v(){};o.r=v(){m 9};o.r.1i="0.1b";o.r.u.14=\'<?L 1s="1.0" 1o="1n-8" ?>\\n\';o.r.u.Y=\'-\';o.r.u.1c=\'1a/L\';o.r.u.N=v(a){6 b;5(W.U){6 c=K U();6 d=c.1r(a,"1p/L");5(!d)m;b=d.A}q 5(W.10){c=K 10(\'1k.1h\');c.1g=z;c.1e(a);b=c.A}5(!b)m;m 9.E(b)};o.r.u.1d=v(c,d,e){6 f={};y(6 g 19 d){f[g]=d[g]}5(!f.M){5(p(f.18)=="w"&&p(f.17)=="w"&&p(f.16)=="w"){f.M="15"}q{f.M="13"}}5(e){f.X=V;6 h=9;6 i=e;6 j=f.T;f.T=v(a){6 b;5(a&&a.x&&a.x.A){b=h.E(a.x.A)}q 5(a&&a.J){b=h.N(a.J)}i(b,a);5(j)j(a)}}q{f.X=z}6 k;5(p(S)!="w"&&S.I){f.1q=c;6 l=K S.I(f);5(l)k=l.12}q 5(p(Q)!="w"&&Q.I){6 l=K Q.I(c,f);5(l)k=l.12}5(e)m k;5(k&&k.x&&k.x.A){m 9.E(k.x.A)}q 5(k&&k.J){m 9.N(k.J)}};o.r.u.E=v(a){5(!a)m;9.H={};5(9.P){y(6 i=0;i<9.P.t;i++){9.H[9.P[i]]=1}}6 b=9.O(a);5(9.H[a.F]){b=[b]}5(a.B!=11){6 c={};c[a.F]=b;b=c}m b};o.r.u.O=v(a){5(a.B==7){m}5(a.B==3||a.B==4){6 b=a.G.1j(/[^\\1f-\\1l]/);5(b==1m)m z;m a.G}6 c;6 d={};5(a.D&&a.D.t){c={};y(6 i=0;i<a.D.t;i++){6 e=a.D[i].F;5(p(e)!="Z")C;6 f=a.D[i].G;5(!f)C;e=9.Y+e;5(p(d[e])=="w")d[e]=0;d[e]++;9.R(c,e,d[e],f)}}5(a.s&&a.s.t){6 g=V;5(c)g=z;y(6 i=0;i<a.s.t&&g;i++){6 h=a.s[i].B;5(h==3||h==4)C;g=z}5(g){5(!c)c="";y(6 i=0;i<a.s.t;i++){c+=a.s[i].G}}q{5(!c)c={};y(6 i=0;i<a.s.t;i++){6 e=a.s[i].F;5(p(e)!="Z")C;6 f=9.O(a.s[i]);5(f==z)C;5(p(d[e])=="w")d[e]=0;d[e]++;9.R(c,e,d[e],f)}}}m c};o.r.u.R=v(a,b,c,d){5(9.H[b]){5(c==1)a[b]=[];a[b][a[b].t]=d}q 5(c==1){a[b]=d}q 5(c==2){a[b]=[a[b],d]}q{a[b][a[b].t]=d}};',62,91,'|||||if|var|||this|||||||||||||return||XML|typeof|else|ObjTree|childNodes|length|prototype|function|undefined|responseXML|for|false|documentElement|nodeType|continue|attributes|parseDOM|nodeName|nodeValue|__force_array|Request|responseText|new|xml|method|parseXML|parseElement|force_array|Ajax|addNode|HTTP|onComplete|DOMParser|true|window|asynchronous|attr_prefix|string|ActiveXObject||transport|post|xmlDecl|get|parameters|postbody|postBody|in|text|24|overrideMimeType|parseHTTP|loadXML|x00|async|XMLDOM|VERSION|match|Microsoft|x20|null|UTF|encoding|application|uri|parseFromString|version'.split('|'),0,{}))
851
+
852
+ /*
853
+
854
+ This is a Date parsing library by Nicholas Barthelemy, packed to keep jester.js light.
855
+ Homepage: https://svn.nbarthelemy.com/date-js/
856
+ Compressed using http://dean.edwards.name/packer/.
857
+
858
+ */
859
+
860
+ eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('N.q.F||(N.q.F=t(a){o u.1d().F(a)});O.q.F||(O.q.F=t(a){o\'0\'.1H(a-u.K)+u});O.q.1H||(O.q.1H=t(a){v s=\'\',i=0;2k(i++<a){s+=u}o s});N.q.1j||(N.q.1j=t(){o u.1d().1j()});O.q.1j||(O.q.1j=t(){v n=u,l=n.K,i=-1;2k(i++<l){u.20(i,i+1)==0?n=n.20(1,n.K):i=l}o n});k.1m="2H 2F 2z 2y 2x 2u 2r 3q 3n 3k 3i 3d".1x(" ");k.1o="38 35 2Y 2U 2Q 2O 2M".1x(" ");k.2K="31 28 31 30 31 30 31 31 30 31 30 31".1x(" ");k.1A={2G:"%Y-%m-%d %H:%M:%S",2w:"%Y-%m-%2v%H:%M:%S%T",2s:"%a, %d %b %Y %H:%M:%S %Z",3p:"%d %b %H:%M",3o:"%B %d, %Y %H:%M"};k.3l=-1;k.3j=-2;(t(){v d=k;d["3h"]=1;d["2i"]=1t;d["2h"]=d["2i"]*19;d["2e"]=d["2h"]*19;d["P"]=d["2e"]*24;d["37"]=d["P"]*7;d["34"]=d["P"]*31;d["1q"]=d["P"]*2X;d["2W"]=d["1q"]*10;d["2R"]=d["1q"]*23;d["2P"]=d["1q"]*1t})();k.q.1D||(k.q.1D=t(){o D k(u.1k())});k.q.26||(k.q.26=t(a,b){u.1F(u.1k()+((a||k.P)*(b||1)));o u});k.q.2a||(k.q.2a=t(a,b){u.1F(u.1k()-((a||k.P)*(b||1)));o u});k.q.1Z||(k.q.1Z=t(){u.1Y(0);u.1X(0);u.1U(0);u.1T(0);o u});k.q.1I||(k.q.1I=t(a,b){C(1i a==\'1p\')a=k.1J(a);o 18.2l((u.1k()-a.1k())/(b|k.P))});k.q.1N||(k.q.1N=k.q.1I);k.q.2n||(k.q.2n=t(){d=O(u);o d.1f(-(18.1y(d.K,2)))>3&&d.1f(-(18.1y(d.K,2)))<21?"V":["V","17","16","1a","V"][18.1y(N(d)%10,4)]});k.q.1w||(k.q.1w=t(){v f=(D k(u.1h(),0,1)).1e();o 18.2t((u.1n()+(f>3?f-4:f+3))/7)});k.q.1M=t(){o u.1d().1v(/^.*? ([A-Z]{3}) [0-9]{4}.*$/,"$1").1v(/^.*?\\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\\)$/,"$1$2$3")};k.q.2p=t(){o(u.1u()>0?"-":"+")+O(18.2l(u.1u()/19)).F(2)+O(u.1u()%19,2,"0").F(2)};k.q.1n||(k.q.1n=t(){o((k.2o(u.1h(),u.1c(),u.1b()+1,0,0,0)-k.2o(u.1h(),0,1,0,0,0))/k.P)});k.q.2m||(k.q.2m=t(){v a=u.1D();a.15(a.1c()+1);a.L(0);o a.1b()});k.2j||(k.2j=t(a,b){a=(a+12)%12;C(k.1K(b)&&a==1)o 29;o k.3g.3f[a]});k.1K||(k.1K=t(a){o(((a%4)==0)&&((a%23)!=0)||((a%3e)==0))});k.q.1B||(k.q.1B=t(c){C(!u.3c())o\'&3b;\';v d=u;C(k.1A[c.2g()])c=k.1A[c.2g()];o c.1v(/\\%([3a])/g,t(a,b){39(b){E\'a\':o k.1l(d.1e()).1f(0,3);E\'A\':o k.1l(d.1e());E\'b\':o k.13(d.1c()).1f(0,3);E\'B\':o k.13(d.1c());E\'c\':o d.1d();E\'d\':o d.1b().F(2);E\'H\':o d.1G().F(2);E\'I\':o((h=d.1G()%12)?h:12).F(2);E\'j\':o d.1n().F(3);E\'m\':o(d.1c()+1).F(2);E\'M\':o d.36().F(2);E\'p\':o d.1G()<12?\'33\':\'32\';E\'S\':o d.2Z().F(2);E\'U\':o d.1w().F(2);E\'W\':R Q("%W 2V 2T 2S 25");E\'w\':o d.1e();E\'x\':o d.1r("%m/%d/%Y");E\'X\':o d.1r("%I:%M%p");E\'y\':o d.1h().1d().1f(2);E\'Y\':o d.1h();E\'T\':o d.2p();E\'Z\':o d.1M()}})});k.q.1r||(k.q.1r=k.q.1B);k.22=k.1J;k.1J=t(a){C(1i a!=\'1p\')o a;C(a.K==0||(/^\\s+$/).1E(a))o;2N(v i=0;i<k.1g.K;i++){v r=k.1g[i].J.2L(a);C(r)o k.1g[i].G(r)}o D k(k.22(a))};k.13||(k.13=t(c){v d=-1;C(1i c==\'2J\'){o k.1m[c.1c()]}2I C(1i c==\'27\'){d=c-1;C(d<0||d>11)R D Q("1s 1C 2b 2q 1W 1V 2d 1 2c 12:"+d);o k.1m[d]}v m=k.1m.1S(t(a,b){C(D 1O("^"+c,"i").1E(a)){d=b;o 1R}o 2f});C(m.K==0)R D Q("1s 1C 1p");C(m.K>1)R D Q("1Q 1C");o k.1m[d]});k.1l||(k.1l=t(c){v d=-1;C(1i c==\'27\'){d=c-1;C(d<0||d>6)R D Q("1s 1z 2b 2q 1W 1V 2d 1 2c 7");o k.1o[d]}v m=k.1o.1S(t(a,b){C(D 1O("^"+c,"i").1E(a)){d=b;o 1R}o 2f});C(m.K==0)R D Q("1s 1z 1p");C(m.K>1)R D Q("1Q 1z");o k.1o[d]});k.1g||(k.1g=[{J:/(\\d{1,2})\\/(\\d{1,2})\\/(\\d{2,4})/,G:t(a){v d=D k();d.1L(a[3]);d.L(14(a[2],10));d.15(14(a[1],10)-1);o d}},{J:/(\\d{4})(?:-?(\\d{2})(?:-?(\\d{2})(?:[T ](\\d{2})(?::?(\\d{2})(?::?(\\d{2})(?:\\.(\\d+))?)?)?(?:Z|(?:([-+])(\\d{2})(?::?(\\d{2}))?)?)?)?)?)?/,G:t(a){v b=0;v d=D k(a[1],0,1);C(a[2])d.15(a[2]-1);C(a[3])d.L(a[3]);C(a[4])d.1Y(a[4]);C(a[5])d.1X(a[5]);C(a[6])d.1U(a[6]);C(a[7])d.1T(N("0."+a[7])*1t);C(a[9]){b=(N(a[9])*19)+N(a[10]);b*=((a[8]==\'-\')?1:-1)}b-=d.1u();1P=(N(d)+(b*19*1t));d.1F(N(1P));o d}},{J:/^2E/i,G:t(){o D k()}},{J:/^2D/i,G:t(){v d=D k();d.L(d.1b()+1);o d}},{J:/^2C/i,G:t(){v d=D k();d.L(d.1b()-1);o d}},{J:/^(\\d{1,2})(17|16|1a|V)?$/i,G:t(a){v d=D k();d.L(14(a[1],10));o d}},{J:/^(\\d{1,2})(?:17|16|1a|V)? (\\w+)$/i,G:t(a){v d=D k();d.L(14(a[1],10));d.15(k.13(a[2]));o d}},{J:/^(\\d{1,2})(?:17|16|1a|V)? (\\w+),? (\\d{4})$/i,G:t(a){v d=D k();d.L(14(a[1],10));d.15(k.13(a[2]));d.1L(a[3]);o d}},{J:/^(\\w+) (\\d{1,2})(?:17|16|1a|V)?$/i,G:t(a){v d=D k();d.L(14(a[2],10));d.15(k.13(a[1]));o d}},{J:/^(\\w+) (\\d{1,2})(?:17|16|1a|V)?,? (\\d{4})$/i,G:t(a){v d=D k();d.L(14(a[2],10));d.15(k.13(a[1]));d.1L(a[3]);o d}},{J:/^3m (\\w+)$/i,G:t(a){v d=D k();v b=d.1e();v c=k.1l(a[1]);v e=c-b;C(c<=b){e+=7}d.L(d.1b()+e);o d}},{J:/^2B (\\w+)$/i,G:t(a){R D Q("2A 25 3r");}}]);',62,214,'||||||||||||||||||||Date||||return||prototype|||function|this|var|||||||if|new|case|zf|handler|||re|length|setDate||Number|String|DAY|Error|throw||||th||||||||parseMonth|parseInt|setMonth|nd|st|Math|60|rd|getDate|getMonth|toString|getDay|substr|__PARSE_PATTERNS|getFullYear|typeof|rz|getTime|parseDay|MONTH_NAMES|getDayOfYear|DAY_NAMES|string|YEAR|format|Invalid|1000|getTimezoneOffset|replace|getWeek|split|min|day|FORMATS|strftime|month|clone|test|setTime|getHours|str|diff|parse|isLeapYear|setYear|getTimezone|compare|RegExp|time|Ambiguous|true|findAll|setMilliseconds|setSeconds|be|must|setMinutes|setHours|clearTime|substring||__native_parse|100||yet|increment|number|||decrement|index|and|between|HOUR|false|toLowerCase|MINUTE|SECOND|daysInMonth|while|floor|lastDayOfMonth|getOrdinal|UTC|getGMTOffset|value|July|rfc822|round|June|dT|iso8601|May|April|March|Not|last|yes|tom|tod|February|db|January|else|object|DAYS_PER_MONTH|exec|Saturday|for|Friday|MILLENNIUM|Thursday|CENTURY|supported|not|Wednesday|is|DECADE|365|Tuesday|getSeconds|||PM|AM|MONTH|Monday|getMinutes|WEEK|Sunday|switch|aAbBcdHIjmMpSUWwxXyYTZ|nbsp|valueOf|December|400|DAYS_IN_MONTH|Convensions|MILLISECOND|November|ERA|October|EPOCH|next|September|long|short|August|implemented'.split('|'),0,{}))