Permalink
Browse files

Make Prototype's JSON implementation EcmaScript 5 compliant.

  • Loading branch information...
tobie committed Feb 21, 2010
1 parent 732eb3d commit 038a2985a70593c1a86c230fadbdfe2e4898a48c
View
@@ -1,3 +1,5 @@
+* Make Prototype's JSON implementation EcmaScript 5 compliant. [#453 state:resolved] (Tobie Langel)
+
* Also detect embedded (UIWebView) mobile Safari. (Thomas Fuchs)
* Avoid object creation and an unnecessary function call in `Class#addMethods`, when working around JScript DontEnum bug. Replace with feature test and a simple boolean check at runtime. (kangax)
View
@@ -334,25 +334,6 @@ Array.from = $A;
return '[' + this.map(Object.inspect).join(', ') + ']';
}
- /** related to: Object.toJSON
- * Array#toJSON() -> String
- *
- * Returns a JSON string representation of the array.
- *
- * <h5>Example</h5>
- *
- * ['a', {b: null}].toJSON();
- * //-> '["a", {"b": null}]'
- **/
- function toJSON() {
- var results = [];
- this.each(function(object) {
- var value = Object.toJSON(object);
- if (!Object.isUndefined(value)) results.push(value);
- });
- return '[' + results.join(', ') + ']';
- }
-
/**
* Array#indexOf(item[, offset = 0]) -> Number
* - item (?): A value that may or may not be in the array.
@@ -432,8 +413,7 @@ Array.from = $A;
clone: clone,
toArray: clone,
size: size,
- inspect: inspect,
- toJSON: toJSON
+ inspect: inspect
});
// fix for opera
View
@@ -4,26 +4,53 @@
* Extensions to the built-in `Date` object.
**/
-/**
- * Date#toJSON() -> String
- *
- * Produces a string representation of the date in ISO 8601 format.
- * The time zone is always UTC, as denoted by the suffix "Z".
- *
- * <h5>Example</h5>
- *
- * var d = new Date(1969, 11, 31, 19);
- * d.getTimezoneOffset();
- * //-> -180 (time offest is given in minutes.)
- * d.toJSON();
- * //-> '"1969-12-31T16:00:00Z"'
-**/
-Date.prototype.toJSON = function() {
- return '"' + this.getUTCFullYear() + '-' +
- (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
- this.getUTCDate().toPaddedString(2) + 'T' +
- this.getUTCHours().toPaddedString(2) + ':' +
- this.getUTCMinutes().toPaddedString(2) + ':' +
- this.getUTCSeconds().toPaddedString(2) + 'Z"';
-};
+
+(function(proto) {
+
+ /**
+ * Date#toISOString() -> String
+ *
+ * Produces a string representation of the date in ISO 8601 format.
+ * The time zone is always UTC, as denoted by the suffix "Z".
+ *
+ * <h5>Example</h5>
+ *
+ * var d = new Date(1969, 11, 31, 19);
+ * d.getTimezoneOffset();
+ * //-> -180 (time offest is given in minutes.)
+ * d.toISOString();
+ * //-> '1969-12-31T16:00:00Z'
+ **/
+
+ function toISOString() {
+ return this.getUTCFullYear() + '-' +
+ (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+ this.getUTCDate().toPaddedString(2) + 'T' +
+ this.getUTCHours().toPaddedString(2) + ':' +
+ this.getUTCMinutes().toPaddedString(2) + ':' +
+ this.getUTCSeconds().toPaddedString(2) + 'Z';
+ }
+
+ /**
+ * Date#toJSON() -> String
+ *
+ * Internally calls [[Date#toISOString]].
+ *
+ * <h5>Example</h5>
+ *
+ * var d = new Date(1969, 11, 31, 19);
+ * d.getTimezoneOffset();
+ * //-> -180 (time offest is given in minutes.)
+ * d.toJSON();
+ * //-> '1969-12-31T16:00:00Z'
+ **/
+
+ function toJSON() {
+ return this.toISOString();
+ }
+
+ if (!proto.toISOString) proto.toISOString = toISOString;
+ if (!proto.toJSON) proto.toJSON = toJSON;
+
+})(Date.prototype);
View
@@ -168,6 +168,14 @@ var Hash = Class.create(Enumerable, (function() {
return Object.clone(this._object);
}
+ /** related to: Object.toJSON, alias of: Hash#toObject
+ * Hash#toJSON() -> Object
+ **/
+
+ /** alias of: Hash#toObject
+ * Hash#toTemplateReplacements() -> Object
+ **/
+
/**
* Hash#keys() -> [String...]
*
@@ -331,22 +339,7 @@ var Hash = Class.create(Enumerable, (function() {
return pair.map(Object.inspect).join(': ');
}).join(', ') + '}>';
}
-
- /** related to: Object.toJSON
- * Hash#toJSON() -> String
- *
- * Returns a JSON string containing the keys and values in this hash.
- *
- * <h5>Example</h5>
- *
- * var h = $H({'a': 'apple', 'b': 23, 'c': false});
- * h.toJSON();
- * // -> {"a": "apple", "b": 23, "c": false}
- **/
- function toJSON() {
- return Object.toJSON(this.toObject());
- }
-
+
/**
* Hash#clone() -> Hash
*
@@ -371,7 +364,7 @@ var Hash = Class.create(Enumerable, (function() {
update: update,
toQueryString: toQueryString,
inspect: inspect,
- toJSON: toJSON,
+ toJSON: toObject,
clone: clone
};
})());
View
@@ -103,15 +103,6 @@ Object.extend(Number.prototype, (function() {
return '0'.times(length - string.length) + string;
}
- /** related to: Object.toJSON
- * Number#toJSON() -> String
- *
- * Returns a JSON string representation of the number.
- **/
- function toJSON() {
- return isFinite(this) ? this.toString() : 'null';
- }
-
/**
* Number#abs() -> Number
*
@@ -159,7 +150,6 @@ Object.extend(Number.prototype, (function() {
succ: succ,
times: times,
toPaddedString: toPaddedString,
- toJSON: toJSON,
abs: abs,
round: round,
ceil: ceil,
View
@@ -10,8 +10,12 @@
**/
(function() {
- var _toString = Object.prototype.toString;
-
+ var _toString = Object.prototype.toString,
+ NATIVE_JSON_STRINGIFY_SUPPORT = window.JSON &&
+ typeof JSON.stringify === 'function' &&
+ JSON.stringify(0) === '0' &&
+ typeof JSON.stringify(Prototype.K) === 'undefined';
+
/**
* Object.extend(destination, source) -> Object
* - destination (Object): The object to receive the new properties.
@@ -67,26 +71,55 @@
* generic `Object`.
**/
function toJSON(object) {
- var type = typeof object;
+ var results, type = typeof object;
+
switch (type) {
case 'undefined':
case 'function':
case 'unknown': return;
case 'boolean': return object.toString();
+ case 'string': return object.inspect(true);
+ case 'number': return _numberToJSON(object);
}
if (object === null) return 'null';
- if (object.toJSON) return object.toJSON();
+
+ type = _toString.call(object);
+
+ switch (type) {
+ case '[object Boolean]': return object.toString();
+ case '[object String]': return object.inspect(true);
+ case '[object Number]': return _numberToJSON(object);
+ case '[object Array]':
+ results = [];
+ object.each(function(item) {
+ item = toJSON(item);
+ if (isUndefined(item)) item = 'null';
+ results.push(item);
+ });
+ return '[' + results.join(',') + ']';
+ }
+
+ if (typeof object.toJSON === 'function')
+ return toJSON(object.toJSON());
if (isElement(object)) return;
- var results = [];
+ results = [];
for (var property in object) {
var value = toJSON(object[property]);
if (!isUndefined(value))
- results.push(property.toJSON() + ': ' + value);
+ results.push(property.inspect(true) + ':' + value);
}
- return '{' + results.join(', ') + '}';
+ return '{' + results.join(',') + '}';
+ }
+
+ function _numberToJSON(number) {
+ return isFinite(number) ? String(number) : 'null';
+ }
+
+ function stringify(object) {
+ return JSON.stringify(object);
}
/**
@@ -279,7 +312,7 @@
extend(Object, {
extend: extend,
inspect: inspect,
- toJSON: toJSON,
+ toJSON: NATIVE_JSON_STRINGIFY_SUPPORT ? stringify : toJSON,
toQueryString: toQueryString,
toHTML: toHTML,
keys: keys,
View
@@ -29,6 +29,9 @@ Object.extend(String, {
});
Object.extend(String.prototype, (function() {
+ var NATIVE_JSON_PARSE_SUPPORT = window.JSON &&
+ typeof JSON.parse === 'function' &&
+ JSON.parse('{"test": true}').test;
function prepareReplacement(replacement) {
if (Object.isFunction(replacement)) return replacement;
@@ -376,15 +379,6 @@ Object.extend(String.prototype, (function() {
return "'" + escapedString.replace(/'/g, '\\\'') + "'";
}
- /** related to: Object.toJSON
- * String#toJSON() -> String
- *
- * Returns a JSON string.
- **/
- function toJSON() {
- return this.inspect(true);
- }
-
/**
* String#unfilterJSON([filter = Prototype.JSONFilter]) -> String
*
@@ -404,8 +398,10 @@ Object.extend(String.prototype, (function() {
function isJSON() {
var str = this;
if (str.blank()) return false;
- str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
- return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+ str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@');
+ str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
+ str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
+ return (/^[\],:{}\s]*$/).test(str);
}
/**
@@ -418,12 +414,23 @@ Object.extend(String.prototype, (function() {
* is _not called_.
**/
function evalJSON(sanitize) {
- var json = this.unfilterJSON();
+ var json = this.unfilterJSON(),
+ cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
+ if (cx.test(json)) {
+ json = json.replace(cx, function (a) {
+ return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
try {
if (!sanitize || json.isJSON()) return eval('(' + json + ')');
} catch (e) { }
throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
}
+
+ function parseJSON() {
+ var json = this.unfilterJSON();
+ return JSON.parse(json);
+ }
/**
* String#include(substring) -> Boolean
@@ -510,10 +517,9 @@ Object.extend(String.prototype, (function() {
underscore: underscore,
dasherize: dasherize,
inspect: inspect,
- toJSON: toJSON,
unfilterJSON: unfilterJSON,
isJSON: isJSON,
- evalJSON: evalJSON,
+ evalJSON: NATIVE_JSON_PARSE_SUPPORT ? parseJSON : evalJSON,
include: include,
startsWith: startsWith,
endsWith: endsWith,
View
@@ -2,7 +2,7 @@ var extendDefault = function(options) {
return Object.extend({
asynchronous: false,
method: 'get',
- onException: function(e) { throw e }
+ onException: function(r, e) { throw e; }
}, options);
};
@@ -274,8 +274,7 @@ new Test.Unit.Runner({
sanitizeJSON: true,
parameters: Fixtures.invalidJson,
onException: function(request, error) {
- this.assert(error.message.include('Badly formed JSON string'));
- this.assertInstanceOf(Ajax.Request, request);
+ this.assertEqual('SyntaxError', error.name);
}.bind(this)
}));
} else {
@@ -360,14 +359,14 @@ new Test.Unit.Runner({
new Ajax.Request("/response", extendDefault({
parameters: Fixtures.invalidJson,
onException: function(request, error) {
- this.assert(error.message.include('Badly formed JSON string'));
+ this.assertEqual('SyntaxError', error.name);
}.bind(this)
}));
new Ajax.Request("/response", extendDefault({
parameters: { 'X-JSON': '{});window.attacked = true;({}' },
onException: function(request, error) {
- this.assert(error.message.include('Badly formed JSON string'));
+ this.assertEqual('SyntaxError', error.name);
}.bind(this)
}));
Oops, something went wrong.

3 comments on commit 038a298

Tobie, thanks so much for making this happen!

-Bob

Collaborator

tobie replied Feb 22, 2010

I'm pushing a couple more changes after going through the specs.

Hey, I'm just glad to see movement on the issue. Thanks again!

Please sign in to comment.