Skip to content

Commit

Permalink
Well‑formed JSON.stringify
Browse files Browse the repository at this point in the history
  • Loading branch information
zloirock committed Nov 6, 2019
1 parent a1bc971 commit b306541
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 29 deletions.
5 changes: 5 additions & 0 deletions packages/core-js-compat/src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,11 @@ const data = {
firefox: '65',
safari: '12.1',
},
'es.json.stringify': {
chrome: '72',
firefox: '64',
safari: '12.1',
},
'es.json.to-string-tag': {
edge: '15',
chrome: '50',
Expand Down
3 changes: 3 additions & 0 deletions packages/core-js-compat/src/modules-by-versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ module.exports = {
'esnext.map.upsert',
'esnext.weak-map.upsert',
],
3.4: [
'es.json.stringify',
],
};
9 changes: 6 additions & 3 deletions packages/core-js/es/json/stringify.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
require('../../modules/es.json.stringify');
var core = require('../../internals/path');
var $JSON = core.JSON || (core.JSON = { stringify: JSON.stringify });

module.exports = function stringify(it) { // eslint-disable-line no-unused-vars
return $JSON.stringify.apply($JSON, arguments);
if (!core.JSON) core.JSON = { stringify: JSON.stringify };

// eslint-disable-next-line no-unused-vars
module.exports = function stringify(it, replacer, space) {
return core.JSON.stringify.apply(null, arguments);
};
45 changes: 45 additions & 0 deletions packages/core-js/modules/es.json.stringify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
var $ = require('../internals/export');
var getBuiltIn = require('../internals/get-built-in');
var fails = require('../internals/fails');
var IndexedObject = require('../internals/indexed-object');

var $stringify = getBuiltIn('JSON', 'stringify');
var reg = /[\uD800-\uDFFF]/g;
var re = /^[\uD800-\uDFFF]$/;
var low = /^[\uD800-\uDBFF]$/;
var hi = /^[\uDC00-\uDFFF]$/;

var fix = function (string) {
var indexed = IndexedObject(string);
var length = indexed.length;
var result = '';
var i = 0;
var point, prev, next;
for (; i < length; i++) {
point = indexed[i];
if (re.test(point)) {
prev = i === 0 ? '' : indexed[i - 1];
next = i === length - 1 ? '' : indexed[i + 1];
if ((low.test(point) && !hi.test(next)) || (hi.test(point) && !low.test(prev))) {
result += '\\u' + point.charCodeAt(0).toString(16);
continue;
}
} result += point;
} return result;
};

var FORCED = fails(function () {
return $stringify('\uDF06\uD834') !== '"\\udf06\\ud834"'
|| $stringify('\uDEAD') !== '"\\udead"';
});

if ($stringify) {
// https://github.com/tc39/proposal-well-formed-stringify
$({ target: 'JSON', stat: true, forced: FORCED }, {
// eslint-disable-next-line no-unused-vars
stringify: function stringify(it, replacer, space) {
var result = $stringify.apply(null, arguments);
return typeof result == 'string' && reg.test(result) ? fix(result) : result;
}
});
}
57 changes: 31 additions & 26 deletions packages/core-js/modules/es.symbol.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
var $ = require('../internals/export');
var global = require('../internals/global');
var getBuiltIn = require('../internals/get-built-in');
var IS_PURE = require('../internals/is-pure');
var DESCRIPTORS = require('../internals/descriptors');
var NATIVE_SYMBOL = require('../internals/native-symbol');
Expand Down Expand Up @@ -42,8 +43,7 @@ var setInternalState = InternalStateModule.set;
var getInternalState = InternalStateModule.getterFor(SYMBOL);
var ObjectPrototype = Object[PROTOTYPE];
var $Symbol = global.Symbol;
var JSON = global.JSON;
var nativeJSONStringify = JSON && JSON.stringify;
var $stringify = getBuiltIn('JSON', 'stringify');
var nativeGetOwnPropertyDescriptor = getOwnPropertyDescriptorModule.f;
var nativeDefineProperty = definePropertyModule.f;
var nativeGetOwnPropertyNames = getOwnPropertyNamesExternal.f;
Expand Down Expand Up @@ -264,30 +264,35 @@ $({ target: 'Object', stat: true, forced: fails(function () { getOwnPropertySymb

// `JSON.stringify` method behavior with symbols
// https://tc39.github.io/ecma262/#sec-json.stringify
JSON && $({ target: 'JSON', stat: true, forced: !NATIVE_SYMBOL || fails(function () {
var symbol = $Symbol();
// MS Edge converts symbol values to JSON as {}
return nativeJSONStringify([symbol]) != '[null]'
// WebKit converts symbol values to JSON as null
|| nativeJSONStringify({ a: symbol }) != '{}'
// V8 throws on boxed symbols
|| nativeJSONStringify(Object(symbol)) != '{}';
}) }, {
stringify: function stringify(it) {
var args = [it];
var index = 1;
var replacer, $replacer;
while (arguments.length > index) args.push(arguments[index++]);
$replacer = replacer = args[1];
if (!isObject(replacer) && it === undefined || isSymbol(it)) return; // IE8 returns string on undefined
if (!isArray(replacer)) replacer = function (key, value) {
if (typeof $replacer == 'function') value = $replacer.call(this, key, value);
if (!isSymbol(value)) return value;
};
args[1] = replacer;
return nativeJSONStringify.apply(JSON, args);
}
});
if ($stringify) {
var FORCED_JSON_STRINGIFY = !NATIVE_SYMBOL || fails(function () {
var symbol = $Symbol();
// MS Edge converts symbol values to JSON as {}
return $stringify([symbol]) != '[null]'
// WebKit converts symbol values to JSON as null
|| $stringify({ a: symbol }) != '{}'
// V8 throws on boxed symbols
|| $stringify(Object(symbol)) != '{}';
});

$({ target: 'JSON', stat: true, forced: FORCED_JSON_STRINGIFY }, {
// eslint-disable-next-line no-unused-vars
stringify: function stringify(it, replacer, space) {
var args = [it];
var index = 1;
var $replacer;
while (arguments.length > index) args.push(arguments[index++]);
$replacer = replacer;
if (!isObject(replacer) && it === undefined || isSymbol(it)) return; // IE8 returns string on undefined
if (!isArray(replacer)) replacer = function (key, value) {
if (typeof $replacer == 'function') value = $replacer.call(this, key, value);
if (!isSymbol(value)) return value;
};
args[1] = replacer;
return $stringify.apply(null, args);
}
});
}

// `Symbol.prototype[@@toPrimitive]` method
// https://tc39.github.io/ecma262/#sec-symbol.prototype-@@toprimitive
Expand Down
4 changes: 4 additions & 0 deletions tests/compat/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,10 @@ GLOBAL.tests = {
'es.global-this': function () {
return globalThis;
},
'es.json.stringify': function () {
return JSON.stringify('\uDF06\uD834') === '"\\udf06\\ud834"'
&& JSON.stringify('\uDEAD') === '"\\udead"';
},
'es.json.to-string-tag': [SYMBOLS_SUPPORT, function () {
return JSON[Symbol.toStringTag];
}],
Expand Down
16 changes: 16 additions & 0 deletions tests/pure/es.json.stringify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { GLOBAL } from '../helpers/constants';
import stringify from 'core-js/es/json/stringify';

if (GLOBAL.JSON) {
QUnit.test('Well‑formed JSON.stringify', assert => {
assert.isFunction(stringify);
assert.arity(stringify, 3);
assert.name(stringify, 'stringify');

assert.same(stringify({ foo: 'bar' }), '{"foo":"bar"}', 'basic');
assert.same(stringify('\uDEAD'), '"\\udead"', 'r1');
assert.same(stringify('\uDF06\uD834'), '"\\udf06\\ud834"', 'r2');
assert.same(stringify('\uDF06ab\uD834'), '"\\udf06ab\\ud834"', 'r3');
assert.same(stringify('𠮷'), '"𠮷"', 'r4');
});
}
1 change: 1 addition & 0 deletions tests/pure/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import './es.date.to-json';
import './es.function.bind';
import './es.function.has-instance';
import './es.global-this';
import './es.json.stringify';
import './es.map';
import './es.math.acosh';
import './es.math.asinh';
Expand Down
17 changes: 17 additions & 0 deletions tests/tests/es.json.stringify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { GLOBAL } from '../helpers/constants';

if (GLOBAL.JSON) {
QUnit.test('Well‑formed JSON.stringify', assert => {
const { stringify } = JSON;
assert.isFunction(stringify);
assert.arity(stringify, 3);
assert.name(stringify, 'stringify');
assert.looksNative(stringify);

assert.same(stringify({ foo: 'bar' }), '{"foo":"bar"}', 'basic');
assert.same(stringify('\uDEAD'), '"\\udead"', 'r1');
assert.same(stringify('\uDF06\uD834'), '"\\udf06\\ud834"', 'r2');
assert.same(stringify('\uDF06ab\uD834'), '"\\udf06ab\\ud834"', 'r3');
assert.same(stringify('𠮷'), '"𠮷"', 'r4');
});
}
1 change: 1 addition & 0 deletions tests/tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import './es.function.bind';
import './es.function.has-instance';
import './es.function.name';
import './es.global-this';
import './es.json.stringify';
import './es.map';
import './es.math.acosh';
import './es.math.asinh';
Expand Down

0 comments on commit b306541

Please sign in to comment.