prototype: Add support for JSON encoding and decoding. Closes #7427.

This commit is contained in:
Sam Stephenson 2007-03-09 03:23:24 +00:00
parent f281192758
commit f160bc4d4d
9 changed files with 217 additions and 15 deletions

View File

@ -1,5 +1,7 @@
*SVN*
* Add support for JSON encoding and decoding. Closes #7427. [Tobie Langel]
* Fix double escaping of query parameters in Hash.prototype.toQueryString, and prevent Safari from iterating over shadowed properties when creating hashes. Closes #7421. [Tobie Langel, Mislav Marohnić]
* Fix simulated attribute reading for IE for "href", "src" and boolean attributes. [Mislav Marohnić, Thomas Fuchs]

View File

@ -86,6 +86,15 @@ Object.extend(Array.prototype, {
inspect: function() {
return '[' + this.map(Object.inspect).join(', ') + ']';
},
toJSON: function() {
var results = [];
this.each(function(object) {
var value = Object.toJSON(object);
if (value !== undefined) results.push(value);
});
return '[' + results.join(',') + ']';
}
});

View File

@ -27,6 +27,26 @@ Object.extend(Object, {
}
},
toJSON: function(object) {
var type = typeof object;
switch(type) {
case 'undefined':
case 'function':
case 'unknown': return;
case 'boolean': return object.toString();
}
if (object === null) return 'null';
if (object.toJSON) return object.toJSON();
if (object.ownerDocument === document) return;
var results = [];
for (var property in object) {
var value = Object.toJSON(object[property]);
if (value !== undefined)
results.push(property.toJSON() + ':' + value);
}
return '{' + results.join(',') + '}';
},
keys: function(object) {
var keys = [];
for (var property in object)
@ -62,9 +82,7 @@ Function.prototype.bindAsEventListener = function(object) {
Object.extend(Number.prototype, {
toColorPart: function() {
var digits = this.toString(16);
if (this < 16) return '0' + digits;
return digits;
return this.toPaddedString(2, 16);
},
succ: function() {
@ -74,9 +92,27 @@ Object.extend(Number.prototype, {
times: function(iterator) {
$R(0, this, true).each(iterator);
return this;
},
toPaddedString: function(length, radix) {
var string = this.toString(radix || 10);
return '0'.times(length - string.length) + string;
},
toJSON: function(){
return isFinite(this) ? this.toString() : 'null';
}
});
Date.prototype.toJSON = function() {
return '"' + this.getFullYear() + '-' +
(this.getMonth() + 1).toPaddedString(2) + '-' +
this.getDate().toPaddedString(2) + 'T' +
this.getHours().toPaddedString(2) + ':' +
this.getMinutes().toPaddedString(2) + ':' +
this.getSeconds().toPaddedString(2) + '"';
};
var Try = {
these: function() {
var returnValue;

View File

@ -22,6 +22,15 @@ Object.extend(Hash, {
});
return parts.join('&');
},
toJSON: function(object) {
var results = [];
this.prototype._each.call(object, function(pair){
var value = Object.toJSON(pair.value);
if (value !== undefined) results.push(pair.key.toJSON() + ':' + value);
});
return '{' + results.join(',') + '}';
}
});
@ -84,6 +93,10 @@ Object.extend(Hash.prototype, {
return '#<Hash:{' + this.map(function(pair) {
return pair.map(Object.inspect).join(': ');
}).join(', ') + '}>';
},
toJSON: function() {
return Hash.toJSON(this);
}
});

View File

@ -1,6 +1,16 @@
String.interpret = function(value) {
return value == null ? '' : String(value);
}
Object.extend(String, {
interpret: function(value){
return value == null ? '' : String(value);
},
specialChar: {
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'\\': '\\\\'
}
});
Object.extend(String.prototype, {
gsub: function(pattern, replacement) {
@ -107,7 +117,13 @@ Object.extend(String.prototype, {
return this.slice(0, this.length - 1) +
String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
},
times: function(count) {
var result = '';
for (var i = 0; i < count; i++) result += this;
return result;
},
camelize: function() {
var parts = this.split('-'), len = parts.length;
if (len == 1) return parts[0];
@ -135,13 +151,26 @@ Object.extend(String.prototype, {
},
inspect: function(useDoubleQuotes) {
var escapedString = this.replace(/\\/g, '\\\\');
if (useDoubleQuotes)
return '"' + escapedString.replace(/"/g, '\\"') + '"';
else
return "'" + escapedString.replace(/'/g, '\\\'') + "'";
var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
var character = String.specialChar[match[0]];
return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
});
if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
return "'" + escapedString.replace(/'/g, '\\\'') + "'";
},
toJSON: function(){
return this.inspect(true);
},
evalJSON: function(sanitize){
try {
if (!sanitize || (/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/.test(this)))
return eval('(' + this + ')');
} catch(e) {}
throw new SyntaxError('Badly formated JSON string: ' + this.inspect());
},
include: function(pattern) {
return this.indexOf(pattern) > -1;
},

View File

@ -102,6 +102,13 @@
assertEqual('[\'a\', 1]',['a',1].inspect());
}},
testToJSON: function(){ with(this) {
assertEqual('[]', Object.toJSON([]));
assertEqual('[\"a\"]', Object.toJSON(['a']));
assertEqual('[\"a\",1]', Object.toJSON(['a', 1]));
assertEqual('[\"a\",{\"b\":null}]', Object.toJSON(['a', {'b': null}]));
}},
testReduce: function(){ with(this) {
assertUndefined([].reduce());
assertNull([null].reduce());

View File

@ -22,11 +22,17 @@
<!-- Log output -->
<div id="testlog"> </div>
<div id="test"></div>
<!-- Tests follow -->
<script type="text/javascript" language="javascript" charset="utf-8">
// <![CDATA[
var Person = function(name){
this.name = name;
};
Person.prototype.toJSON = function() {
return '-' + this.name;
};
var peEventCount = 0;
// peEventFired will stop the PeriodicalExecuter after 3 callbacks
@ -87,6 +93,33 @@
assertEqual('[]', Object.inspect([]));
}},
testObjectToJSON: function() { with(this) {
assertUndefined(Object.toJSON(undefined));
assertUndefined(Object.toJSON(Prototype.K));
assertEqual('\"\"', Object.toJSON(''));
assertEqual('[]', Object.toJSON([]));
assertEqual('[\"a\"]', Object.toJSON(['a']));
assertEqual('[\"a\",1]', Object.toJSON(['a', 1]));
assertEqual('[\"a\",{\"b\":null}]', Object.toJSON(['a', {'b': null}]));
assertEqual('{\"a\":\"hello!\"}', Object.toJSON({a: 'hello!'}));
assertEqual('{}', Object.toJSON({}));
assertEqual('{}', Object.toJSON({a: undefined, b: undefined, c: Prototype.K}));
assertEqual('{\"b\":[false,true],\"c\":{\"a\":\"hello!\"}}',
Object.toJSON({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}));
assertEqual('{\"b\":[false,true],\"c\":{\"a\":\"hello!\"}}',
Object.toJSON($H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}})));
assertEqual('true', Object.toJSON(true));
assertEqual('false', Object.toJSON(false));
assertEqual('null', Object.toJSON(null));
var sam = new Person('sam');
assertEqual('-sam', Object.toJSON(sam));
assertEqual('-sam', sam.toJSON());
var element = $('test');
assertUndefined(Object.toJSON(element));
element.toJSON = function(){return 'I\'m a div with id test'};
assertEqual('I\'m a div with id test', Object.toJSON(element));
}},
// sanity check
testDoesntExtendObjectPrototype: function() {with(this) {
// for-in is supported with objects
@ -125,6 +158,32 @@
}
},
testNumberToColorPart: function() {with(this) {
assertEqual('00', (0).toColorPart());
assertEqual('0a', (10).toColorPart());
assertEqual('ff', (255).toColorPart());
}},
testNumberToPaddedString: function() {with(this) {
assertEqual('00', (0).toPaddedString(2, 16));
assertEqual('0a', (10).toPaddedString(2, 16));
assertEqual('ff', (255).toPaddedString(2, 16));
assertEqual('000', (0).toPaddedString(3));
assertEqual('010', (10).toPaddedString(3));
assertEqual('100', (100).toPaddedString(3));
assertEqual('1000', (1000).toPaddedString(3));
}},
testNumberToJSON: function() {with(this) {
assertEqual('null', Number.NaN.toJSON());
assertEqual('0', (0).toJSON());
assertEqual('-293', (-293).toJSON());
}},
testDateToJSON: function() {with(this) {
assertEqual('\"1969-12-31T19:00:00\"', new Date(1969, 11, 31, 19).toJSON());
}},
testBrowserDetection: function() {with(this) {
var results = $H(Prototype.Browser).map(function(engine){
return engine;

View File

@ -126,6 +126,13 @@
assertEqual('#<Hash:{}>', $H({}).inspect());
assertEqual("#<Hash:{'a': 'A#'}>", $H(Fixtures.one).inspect());
assertEqual("#<Hash:{'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D#'}>", $H(Fixtures.many).inspect());
}},
testToJSON: function(){ with(this) {
assertEqual('{\"b\":[false,true],\"c\":{\"a\":\"hello!\"}}',
$H({'b': [undefined, false, true, undefined], c: {a: 'hello!'}}).toJSON());
assertEqual('E', eval('(' + $H(Fixtures.dangerous).toJSON() + ')')._each);
}}
}, 'testlog');
// ]]>

View File

@ -26,6 +26,7 @@
<!-- Tests follow -->
<script type="text/javascript" language="javascript" charset="utf-8">
// <![CDATA[
var attackTarget;
var evalScriptsCounter = 0,
largeTextEscaped = '&lt;span&gt;test&lt;/span&gt;',
largeTextUnescaped = '<span>test</span>';
@ -320,6 +321,11 @@
assertEqual('\'\'', ''.inspect());
assertEqual('\'test\'', 'test'.inspect());
assertEqual('\'test \\\'test\\\' "test"\'', 'test \'test\' "test"'.inspect());
assertEqual('\"test \'test\' \\"test\\"\"', 'test \'test\' "test"'.inspect(true));
assertEqual('\'\\b\\t\\n\\f\\r"\\\\\'', '\b\t\n\f\r"\\'.inspect());
assertEqual('\"\\b\\t\\n\\f\\r\\"\\\\\"', '\b\t\n\f\r"\\'.inspect(true));
assertEqual('\'\\b\\t\\n\\f\\r\'', '\x08\x09\x0a\x0c\x0d'.inspect());
assertEqual('\'\\u001a\'', '\x1a'.inspect());
}},
testInclude: function() {with(this) {
@ -369,7 +375,41 @@
assertEqual('abce', 'abcd'.succ());
assertEqual('{', 'z'.succ());
assertEqual(':', '9'.succ());
}}
}},
testTimes: function() {with(this) {
assertEqual('', ''.times(0));
assertEqual('', ''.times(5));
assertEqual('', 'a'.times(0));
assertEqual('a', 'a'.times(1));
assertEqual('aaaaa', 'a'.times(5));
assertEqual('foofoofoofoofoo', 'foo'.times(5));
assertEqual('', 'foo'.times(-5));
}},
testToJSON: function() {with(this) {
assertEqual('\"\"', ''.toJSON());
assertEqual('\"test\"', 'test'.toJSON());
}},
testEvalJSON: function() {with(this) {
var valid = '{test: "hello world!"}';
var invalid = '{test: "hello world!"';
var dangerous = '{});attackTarget = "attack succeeded!";({}';
assertEqual('hello world!', valid.evalJSON().test);
assertEqual('hello world!', valid.evalJSON(true).test);
assertRaise('SyntaxError', function(){invalid.evalJSON();});
assertRaise('SyntaxError', function(){invalid.evalJSON(true);});
attackTarget = "scared";
dangerous.evalJSON();
assertEqual("attack succeeded!", attackTarget);
attackTarget = "Not scared!";
assertRaise('SyntaxError', function(){dangerous.evalJSON(true)});
assertEqual("Not scared!", attackTarget);
}}
}, 'testlog');
// ]]>
</script>