diff --git a/.editorconfig b/.editorconfig
index 98a761d..1c6314a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -7,6 +7,6 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
-[{package.json,*.yml}]
+[*.yml]
indent_style = space
indent_size = 2
diff --git a/.gitattributes b/.gitattributes
index 391f0a4..6313b56 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1 @@
-* text=auto
-*.js text eol=lf
+* text=auto eol=lf
diff --git a/.github/funding.yml b/.github/funding.yml
new file mode 100644
index 0000000..6d802f9
--- /dev/null
+++ b/.github/funding.yml
@@ -0,0 +1,2 @@
+github: sindresorhus
+tidelift: npm/stringify-object
diff --git a/.github/security.md b/.github/security.md
new file mode 100644
index 0000000..5358dc5
--- /dev/null
+++ b/.github/security.md
@@ -0,0 +1,3 @@
+# Security Policy
+
+To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..6a82b18
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,21 @@
+name: CI
+on:
+ - push
+ - pull_request
+jobs:
+ test:
+ name: Node.js ${{ matrix.node-version }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node-version:
+ - 18
+ - 16
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: npm install
+ - run: npm test
diff --git a/.gitignore b/.gitignore
index 3c3629e..239ecff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
node_modules
+yarn.lock
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..43c97e7
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b18bae5..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-sudo: false
-language: node_js
-node_js:
- - '6'
- - '4'
diff --git a/contributing.md b/contributing.md
index 0be6f2f..4c91d7b 100644
--- a/contributing.md
+++ b/contributing.md
@@ -1 +1 @@
-See the [contributing docs](https://github.com/yeoman/yeoman/blob/master/contributing.md)
+See the [contributing docs](https://github.com/yeoman/yeoman/blob/main/contributing.md)
diff --git a/index.js b/index.js
index 04a90bc..370d777 100644
--- a/index.js
+++ b/index.js
@@ -1,123 +1,135 @@
-'use strict';
-const isRegexp = require('is-regexp');
-const isObj = require('is-obj');
-const getOwnEnumPropSymbols = require('get-own-enumerable-property-symbols');
+import isRegexp from 'is-regexp';
+import isObject from 'is-obj';
+import getOwnEnumerableKeys from 'get-own-enumerable-keys';
-module.exports = (val, opts, pad) => {
+export default function stringifyObject(input, options, pad) {
const seen = [];
- return (function stringify(val, opts, pad) {
- opts = opts || {};
- opts.indent = opts.indent || '\t';
- pad = pad || '';
+ return (function stringify(input, options = {}, pad = '') {
+ const indent = options.indent || '\t';
let tokens;
-
- if (opts.inlineCharacterLimit === undefined) {
+ if (options.inlineCharacterLimit === undefined) {
tokens = {
- newLine: '\n',
- newLineOrSpace: '\n',
+ newline: '\n',
+ newlineOrSpace: '\n',
pad,
- indent: pad + opts.indent
+ indent: pad + indent,
};
} else {
tokens = {
- newLine: '@@__STRINGIFY_OBJECT_NEW_LINE__@@',
- newLineOrSpace: '@@__STRINGIFY_OBJECT_NEW_LINE_OR_SPACE__@@',
+ newline: '@@__STRINGIFY_OBJECT_NEW_LINE__@@',
+ newlineOrSpace: '@@__STRINGIFY_OBJECT_NEW_LINE_OR_SPACE__@@',
pad: '@@__STRINGIFY_OBJECT_PAD__@@',
- indent: '@@__STRINGIFY_OBJECT_INDENT__@@'
+ indent: '@@__STRINGIFY_OBJECT_INDENT__@@',
};
}
const expandWhiteSpace = string => {
- if (opts.inlineCharacterLimit === undefined) {
+ if (options.inlineCharacterLimit === undefined) {
return string;
}
const oneLined = string
- .replace(new RegExp(tokens.newLine, 'g'), '')
- .replace(new RegExp(tokens.newLineOrSpace, 'g'), ' ')
+ .replace(new RegExp(tokens.newline, 'g'), '')
+ .replace(new RegExp(tokens.newlineOrSpace, 'g'), ' ')
.replace(new RegExp(tokens.pad + '|' + tokens.indent, 'g'), '');
- if (oneLined.length <= opts.inlineCharacterLimit) {
+ if (oneLined.length <= options.inlineCharacterLimit) {
return oneLined;
}
return string
- .replace(new RegExp(tokens.newLine + '|' + tokens.newLineOrSpace, 'g'), '\n')
+ .replace(new RegExp(tokens.newline + '|' + tokens.newlineOrSpace, 'g'), '\n')
.replace(new RegExp(tokens.pad, 'g'), pad)
- .replace(new RegExp(tokens.indent, 'g'), pad + opts.indent);
+ .replace(new RegExp(tokens.indent, 'g'), pad + indent);
};
- if (seen.indexOf(val) !== -1) {
+ if (seen.includes(input)) {
return '"[Circular]"';
}
- if (val === null ||
- val === undefined ||
- typeof val === 'number' ||
- typeof val === 'boolean' ||
- typeof val === 'function' ||
- typeof val === 'symbol' ||
- isRegexp(val)) {
- return String(val);
+ if (
+ input === null
+ || input === undefined
+ || typeof input === 'number'
+ || typeof input === 'boolean'
+ || typeof input === 'function'
+ || typeof input === 'symbol'
+ || isRegexp(input)
+ ) {
+ return String(input);
}
- if (val instanceof Date) {
- return `new Date('${val.toISOString()}')`;
+ if (input instanceof Date) {
+ return `new Date('${input.toISOString()}')`;
}
- if (Array.isArray(val)) {
- if (val.length === 0) {
+ if (Array.isArray(input)) {
+ if (input.length === 0) {
return '[]';
}
- seen.push(val);
+ seen.push(input);
+
+ const returnValue = '[' + tokens.newline + input.map((element, i) => {
+ const eol = input.length - 1 === i ? tokens.newline : ',' + tokens.newlineOrSpace;
+
+ let value = stringify(element, options, pad + indent);
+ if (options.transform) {
+ value = options.transform(input, i, value);
+ }
- const ret = '[' + tokens.newLine + val.map((el, i) => {
- const eol = val.length - 1 === i ? tokens.newLine : ',' + tokens.newLineOrSpace;
- return tokens.indent + stringify(el, opts, pad + opts.indent) + eol;
+ return tokens.indent + value + eol;
}).join('') + tokens.pad + ']';
- seen.pop(val);
+ seen.pop();
- return expandWhiteSpace(ret);
+ return expandWhiteSpace(returnValue);
}
- if (isObj(val)) {
- const objKeys = Object.keys(val).concat(getOwnEnumPropSymbols(val));
+ if (isObject(input)) {
+ let objectKeys = getOwnEnumerableKeys(input);
- if (objKeys.length === 0) {
+ if (options.filter) {
+ // eslint-disable-next-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument
+ objectKeys = objectKeys.filter(element => options.filter(input, element));
+ }
+
+ if (objectKeys.length === 0) {
return '{}';
}
- seen.push(val);
+ seen.push(input);
+
+ const returnValue = '{' + tokens.newline + objectKeys.map((element, index) => {
+ const eol = objectKeys.length - 1 === index ? tokens.newline : ',' + tokens.newlineOrSpace;
+ const isSymbol = typeof element === 'symbol';
+ const isClassic = !isSymbol && /^[a-z$_][$\w]*$/i.test(element);
+ const key = isSymbol || isClassic ? element : stringify(element, options);
- const ret = '{' + tokens.newLine + objKeys.map((el, i) => {
- if (opts.filter && !opts.filter(val, el)) {
- return '';
+ let value = stringify(input[element], options, pad + indent);
+ if (options.transform) {
+ value = options.transform(input, element, value);
}
- const eol = objKeys.length - 1 === i ? tokens.newLine : ',' + tokens.newLineOrSpace;
- const isSymbol = typeof el === 'symbol';
- const isClassic = !isSymbol && /^[a-z$_][a-z$_0-9]*$/i.test(el);
- const key = isSymbol || isClassic ? el : stringify(el, opts);
- return tokens.indent + String(key) + ': ' + stringify(val[el], opts, pad + opts.indent) + eol;
+ return tokens.indent + String(key) + ': ' + value + eol;
}).join('') + tokens.pad + '}';
- seen.pop(val);
+ seen.pop();
- return expandWhiteSpace(ret);
+ return expandWhiteSpace(returnValue);
}
- val = String(val).replace(/[\r\n]/g, x => x === '\n' ? '\\n' : '\\r');
+ input = input.replace(/\\/g, '\\\\');
+ input = String(input).replace(/[\r\n]/g, x => x === '\n' ? '\\n' : '\\r');
- if (opts.singleQuotes === false) {
- val = val.replace(/"/g, '\\"');
- return `"${val}"`;
+ if (options.singleQuotes === false) {
+ input = input.replace(/"/g, '\\"');
+ return `"${input}"`;
}
- val = val.replace(/\\?'/g, '\\\'');
- return `'${val}'`;
- })(val, opts, pad);
-};
+ input = input.replace(/'/g, '\\\'');
+ return `'${input}'`;
+ })(input, options, pad);
+}
diff --git a/package.json b/package.json
index 5d87311..64c49b2 100644
--- a/package.json
+++ b/package.json
@@ -1,43 +1,48 @@
{
- "name": "stringify-object",
- "version": "3.1.1",
- "description": "Stringify an object/array like JSON.stringify just without all the double-quotes",
- "license": "BSD-2-Clause",
- "repository": "yeoman/stringify-object",
- "author": {
- "name": "Sindre Sorhus",
- "email": "sindresorhus@gmail.com",
- "url": "sindresorhus.com"
- },
- "engines": {
- "node": ">=4"
- },
- "scripts": {
- "test": "xo && mocha"
- },
- "files": [
- "index.js"
- ],
- "keywords": [
- "object",
- "stringify",
- "pretty",
- "print",
- "dump",
- "format",
- "type",
- "json"
- ],
- "dependencies": {
- "get-own-enumerable-property-symbols": "^1.0.1",
- "is-obj": "^1.0.1",
- "is-regexp": "^1.0.0"
- },
- "devDependencies": {
- "mocha": "*",
- "xo": "*"
- },
- "xo": {
- "esnext": true
- }
+ "name": "stringify-object",
+ "version": "5.0.0",
+ "description": "Stringify an object/array like JSON.stringify just without all the double-quotes",
+ "license": "BSD-2-Clause",
+ "repository": "yeoman/stringify-object",
+ "funding": "https://github.com/yeoman/stringify-object?sponsor=1",
+ "author": {
+ "name": "Sindre Sorhus",
+ "email": "sindresorhus@gmail.com",
+ "url": "https://sindresorhus.com"
+ },
+ "type": "module",
+ "exports": "./index.js",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "scripts": {
+ "test": "xo && ava"
+ },
+ "files": [
+ "index.js"
+ ],
+ "keywords": [
+ "object",
+ "stringify",
+ "pretty",
+ "print",
+ "dump",
+ "format",
+ "type",
+ "json"
+ ],
+ "dependencies": {
+ "get-own-enumerable-keys": "^1.0.0",
+ "is-obj": "^3.0.0",
+ "is-regexp": "^3.1.0"
+ },
+ "devDependencies": {
+ "ava": "^5.1.1",
+ "xo": "^0.53.1"
+ },
+ "xo": {
+ "ignores": [
+ "test/fixtures/*.js"
+ ]
+ }
}
diff --git a/readme.md b/readme.md
index 284129f..82b5969 100644
--- a/readme.md
+++ b/readme.md
@@ -1,4 +1,4 @@
-# stringify-object [![Build Status](https://secure.travis-ci.org/yeoman/stringify-object.svg?branch=master)](http://travis-ci.org/yeoman/stringify-object)
+# stringify-object
> Stringify an object/array like JSON.stringify just without all the double-quotes
@@ -6,26 +6,26 @@ Useful for when you want to get the string representation of an object in a form
It also handles circular references and lets you specify quote type.
-
## Install
+```sh
+npm install stringify-object
```
-$ npm install --save stringify-object
-```
-
## Usage
```js
-const stringifyObject = require('stringify-object');
+import stringifyObject from 'stringify-object';
-const obj = {
+const object = {
foo: 'bar',
'arr': [1, 2, 3],
- nested: { hello: "world" }
+ nested: {
+ hello: "world"
+ }
};
-const pretty = stringifyObject(obj, {
+const pretty = stringifyObject(object, {
indent: ' ',
singleQuotes: false
});
@@ -33,51 +33,90 @@ const pretty = stringifyObject(obj, {
console.log(pretty);
/*
{
- foo: "bar",
- arr: [
- 1,
- 2,
- 3
- ],
- nested: {
- hello: "world"
- }
+ foo: "bar",
+ arr: [
+ 1,
+ 2,
+ 3
+ ],
+ nested: {
+ hello: "world"
+ }
}
*/
```
-
## API
-### stringifyObject(input, [options])
+### stringifyObject(input, options?)
Circular references will be replaced with `"[Circular]"`.
+Object keys are only quoted when necessary, for example, `{'foo-bar': true}`.
+
#### input
-Type: `Object` `Array`
+Type: `object | Array`
#### options
+Type: `object`
+
##### indent
-Type: `string`
-Default: `'\t'`
+Type: `string`\
+Default: `\t`
Preferred indentation.
##### singleQuotes
-Type: `boolean`
+Type: `boolean`\
Default: `true`
Set to false to get double-quoted strings.
-##### filter(obj, prop)
+##### filter(object, property)
Type: `Function`
-Expected to return a `boolean` of whether to keep the object.
+Expected to return a `boolean` of whether to include the property `property` of the object `object` in the output.
+
+##### transform(object, property, originalResult)
+
+Type: `Function`\
+Default: `undefined`
+
+Expected to return a `string` that transforms the string that resulted from stringifying `object[property]`. This can be used to detect special types of objects that need to be stringified in a particular way. The `transform` function might return an alternate string in this case, otherwise returning the `originalResult`.
+
+Here's an example that uses the `transform` option to mask fields named "password":
+
+```js
+import stringifyObject from 'stringify-object';
+
+const object = {
+ user: 'becky',
+ password: 'secret'
+};
+
+const pretty = stringifyObject(object, {
+ transform: (object, property, originalResult) => {
+ if (property === 'password') {
+ return originalResult.replace(/\w/g, '*');
+ }
+
+ return originalResult;
+ }
+});
+
+console.log(pretty);
+/*
+{
+ user: 'becky',
+ password: '******'
+}
+*/
+```
##### inlineCharacterLimit
@@ -88,13 +127,17 @@ When set, will inline values up to `inlineCharacterLimit` length for the sake of
For example, given the example at the top of the README:
```js
-const obj = {
+import stringifyObject from 'stringify-object';
+
+const object = {
foo: 'bar',
'arr': [1, 2, 3],
- nested: { hello: "world" }
+ nested: {
+ hello: "world"
+ }
};
-const pretty = stringifyObject(obj, {
+const pretty = stringifyObject(object, {
indent: ' ',
singleQuotes: false,
inlineCharacterLimit: 12
@@ -103,18 +146,13 @@ const pretty = stringifyObject(obj, {
console.log(pretty);
/*
{
- foo: "bar",
- arr: [1, 2, 3],
- nested: {
- hello: "world"
- }
+ foo: "bar",
+ arr: [1, 2, 3],
+ nested: {
+ hello: "world"
+ }
}
*/
```
As you can see, `arr` was printed as a one-liner because its string was shorter than 12 characters.
-
-
-## License
-
-[BSD license](http://opensource.org/licenses/bsd-license.php) © Yeoman Team
diff --git a/test.js b/test.js
deleted file mode 100644
index 804ed97..0000000
--- a/test.js
+++ /dev/null
@@ -1,146 +0,0 @@
-/* eslint-env mocha */
-'use strict';
-const fs = require('fs');
-const assert = require('assert');
-const stringifyObject = require('./');
-
-it('should stringify an object', () => {
- /* eslint-disable quotes, object-shorthand */
- const obj = {
- foo: 'bar \'bar\'',
- foo2: [
- 'foo',
- 'bar',
- {
- foo: "bar 'bar'"
- }
- ],
- 'foo-foo': 'bar',
- '2foo': 'bar',
- '@#': "bar",
- $el: 'bar',
- _private: 'bar',
- number: 1,
- boolean: true,
- date: new Date("2014-01-29T22:41:05.665Z"),
- escapedString: "\"\"",
- null: null,
- undefined: undefined,
- function: function () {},
- regexp: /./,
- NaN: NaN,
- Infinity: Infinity,
- newlines: "foo\nbar\r\nbaz",
- [Symbol()]: Symbol(), // eslint-disable-line symbol-description
- [Symbol('foo')]: Symbol('foo'),
- [Symbol.for('foo')]: Symbol.for('foo')
- };
- /* eslint-enable */
-
- obj.circular = obj;
-
- const actual = stringifyObject(obj, {
- indent: ' ',
- singleQuotes: false
- });
-
- assert.equal(actual + '\n', fs.readFileSync('fixture.js', 'utf8'));
- assert.equal(
- stringifyObject({foo: 'a \' b \' c \\\' d'}, {singleQuotes: true}),
- '{\n\tfoo: \'a \\\' b \\\' c \\\' d\'\n}'
- );
-});
-
-it('should not detect reused object values as circular reference', () => {
- const val = {val: 10};
- const obj = {foo: val, bar: val};
- assert.equal(stringifyObject(obj), '{\n\tfoo: {\n\t\tval: 10\n\t},\n\tbar: {\n\t\tval: 10\n\t}\n}');
-});
-
-it('should not detect reused array values as false circular references', () => {
- const val = [10];
- const obj = {foo: val, bar: val};
- assert.equal(stringifyObject(obj), '{\n\tfoo: [\n\t\t10\n\t],\n\tbar: [\n\t\t10\n\t]\n}');
-});
-
-it('considering filter option to stringify an object', () => {
- const val = {val: 10};
- const obj = {foo: val, bar: val};
- const actual = stringifyObject(obj, {
- filter: (obj, prop) => prop !== 'foo'
- });
- assert.equal(actual, '{\n\tbar: {\n\t\tval: 10\n\t}\n}');
-});
-
-it('should not crash with circular references in arrays', () => {
- const array = [];
- array.push(array);
- assert.doesNotThrow(() => {
- stringifyObject(array);
- });
-
- const nestedArray = [[]];
- nestedArray[0][0] = nestedArray;
- assert.doesNotThrow(() => {
- stringifyObject(nestedArray);
- });
-});
-
-it('should handle circular references in arrays', () => {
- const array2 = [];
- const array = [array2];
- array2[0] = array2;
-
- assert.doesNotThrow(() => {
- stringifyObject(array);
- });
-});
-
-it('should stringify complex circular arrays', () => {
- const array = [[[]]];
- array[0].push(array);
- array[0][0].push(array);
- array[0][0].push(10);
- array[0][0][0] = array;
- assert.equal(stringifyObject(array), '[\n\t[\n\t\t[\n\t\t\t"[Circular]",\n\t\t\t10\n\t\t],\n\t\t"[Circular]"\n\t]\n]');
-});
-
-it('allows short objects to be one-lined', () => {
- const object = {id: 8, name: 'Jane'};
-
- assert.equal(stringifyObject(object), '{\n\tid: 8,\n\tname: \'Jane\'\n}');
- assert.equal(stringifyObject(object, {inlineCharacterLimit: 21}), '{id: 8, name: \'Jane\'}');
- assert.equal(stringifyObject(object, {inlineCharacterLimit: 20}), '{\n\tid: 8,\n\tname: \'Jane\'\n}');
-});
-
-it('allows short arrays to be one-lined', () => {
- const array = ['foo', {id: 8, name: 'Jane'}, 42];
-
- assert.equal(stringifyObject(array), '[\n\t\'foo\',\n\t{\n\t\tid: 8,\n\t\tname: \'Jane\'\n\t},\n\t42\n]');
- assert.equal(stringifyObject(array, {inlineCharacterLimit: 34}), '[\'foo\', {id: 8, name: \'Jane\'}, 42]');
- assert.equal(stringifyObject(array, {inlineCharacterLimit: 33}), '[\n\t\'foo\',\n\t{id: 8, name: \'Jane\'},\n\t42\n]');
-});
-
-it('does not mess up indents for complex objects', () => {
- const object = {
- arr: [1, 2, 3],
- nested: {hello: 'world'}
- };
-
- assert.equal(stringifyObject(object), '{\n\tarr: [\n\t\t1,\n\t\t2,\n\t\t3\n\t],\n\tnested: {\n\t\thello: \'world\'\n\t}\n}');
- assert.equal(stringifyObject(object, {inlineCharacterLimit: 12}), '{\n\tarr: [1, 2, 3],\n\tnested: {\n\t\thello: \'world\'\n\t}\n}');
-});
-
-it('handles non-plain object', () => {
- assert.notStrictEqual(stringifyObject(fs.statSync(__filename)), '[object Object]');
-});
-
-it('should not stringify non-enumerable symbols', () => {
- const obj = {
- [Symbol('for enumerable key')]: undefined
- };
- const symbol = Symbol('for non-enumerable key');
- Object.defineProperty(obj, symbol, {enumerable: false});
-
- assert.equal(stringifyObject(obj), '{\n\tSymbol(for enumerable key): undefined\n}');
-});
diff --git a/fixture.js b/test/fixtures/object.js
similarity index 94%
rename from fixture.js
rename to test/fixtures/object.js
index a34acb4..4586040 100644
--- a/fixture.js
+++ b/test/fixtures/object.js
@@ -18,7 +18,7 @@
escapedString: "\"\"",
null: null,
undefined: undefined,
- function: function () {},
+ fn: function fn() {},
regexp: /./,
NaN: NaN,
Infinity: Infinity,
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 0000000..c2116f6
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,203 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
+import test from 'ava';
+import stringifyObject from '../index.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+test('stringify an object', t => {
+ /* eslint-disable quotes, object-shorthand */
+ const object = {
+ foo: 'bar \'bar\'',
+ foo2: [
+ 'foo',
+ 'bar',
+ {
+ foo: "bar 'bar'",
+ },
+ ],
+ 'foo-foo': 'bar',
+ '2foo': 'bar',
+ '@#': "bar",
+ $el: 'bar',
+ _private: 'bar',
+ number: 1,
+ boolean: true,
+ date: new Date("2014-01-29T22:41:05.665Z"),
+ escapedString: "\"\"",
+ null: null,
+ undefined: undefined,
+ fn: function fn() {}, // eslint-disable-line func-names
+ regexp: /./,
+ NaN: Number.NaN,
+ Infinity: Number.POSITIVE_INFINITY,
+ newlines: "foo\nbar\r\nbaz",
+ [Symbol()]: Symbol(), // eslint-disable-line symbol-description
+ [Symbol('foo')]: Symbol('foo'),
+ [Symbol.for('foo')]: Symbol.for('foo'),
+ };
+ /* eslint-enable */
+
+ object.circular = object;
+
+ const actual = stringifyObject(object, {
+ indent: ' ',
+ singleQuotes: false,
+ });
+
+ t.is(actual + '\n', fs.readFileSync(path.resolve(__dirname, 'fixtures/object.js'), 'utf8'));
+ t.is(
+ stringifyObject({foo: 'a \' b \' c \\\' d'}, {singleQuotes: true}),
+ '{\n\tfoo: \'a \\\' b \\\' c \\\\\\\' d\'\n}',
+ );
+});
+
+test('string escaping works properly', t => {
+ t.is(stringifyObject('\\', {singleQuotes: true}), '\'\\\\\''); // \
+ t.is(stringifyObject('\\\'', {singleQuotes: true}), '\'\\\\\\\'\''); // \'
+ t.is(stringifyObject('\\"', {singleQuotes: true}), '\'\\\\"\''); // \"
+ t.is(stringifyObject('\\', {singleQuotes: false}), '"\\\\"'); // \
+ t.is(stringifyObject('\\\'', {singleQuotes: false}), '"\\\\\'"'); // \'
+ t.is(stringifyObject('\\"', {singleQuotes: false}), '"\\\\\\""'); // \"
+ /* eslint-disable no-eval */
+ t.is(eval(stringifyObject('\\\'')), '\\\'');
+ t.is(eval(stringifyObject('\\\'', {singleQuotes: false})), '\\\'');
+ /* eslint-enable */
+ // Regression test for #40
+ t.is(stringifyObject("a'a"), '\'a\\\'a\''); // eslint-disable-line quotes
+});
+
+test('detect reused object values as circular reference', t => {
+ const value = {val: 10};
+ const object = {foo: value, bar: value};
+ t.is(stringifyObject(object), '{\n\tfoo: {\n\t\tval: 10\n\t},\n\tbar: {\n\t\tval: 10\n\t}\n}');
+});
+
+test('detect reused array values as false circular references', t => {
+ const value = [10];
+ const object = {foo: value, bar: value};
+ t.is(stringifyObject(object), '{\n\tfoo: [\n\t\t10\n\t],\n\tbar: [\n\t\t10\n\t]\n}');
+});
+
+test('considering filter option to stringify an object', t => {
+ const value = {val: 10};
+ const object = {foo: value, bar: value};
+ const actual = stringifyObject(object, {
+ filter: (object, prop) => prop !== 'foo',
+ });
+ t.is(actual, '{\n\tbar: {\n\t\tval: 10\n\t}\n}');
+
+ const actual2 = stringifyObject(object, {
+ filter: (object, prop) => prop !== 'bar',
+ });
+ t.is(actual2, '{\n\tfoo: {\n\t\tval: 10\n\t}\n}');
+
+ const actual3 = stringifyObject(object, {
+ filter: (object, prop) => prop !== 'val' && prop !== 'bar',
+ });
+ t.is(actual3, '{\n\tfoo: {}\n}');
+});
+
+test('allows an object to be transformed', t => {
+ const object = {
+ foo: {
+ val: 10,
+ },
+ bar: 9,
+ baz: [8],
+ };
+
+ const actual = stringifyObject(object, {
+ transform(object, prop, result) {
+ if (prop === 'val') {
+ return String(object[prop] + 1);
+ }
+
+ if (prop === 'bar') {
+ return '\'' + result + 'L\'';
+ }
+
+ if (object[prop] === 8) {
+ return 'LOL';
+ }
+
+ return result;
+ },
+ });
+
+ t.is(actual, '{\n\tfoo: {\n\t\tval: 11\n\t},\n\tbar: \'9L\',\n\tbaz: [\n\t\tLOL\n\t]\n}');
+});
+
+test('doesn\'t crash with circular references in arrays', t => {
+ const array = [];
+ array.push(array);
+ t.notThrows(() => {
+ stringifyObject(array);
+ });
+
+ const nestedArray = [[]];
+ nestedArray[0][0] = nestedArray;
+ t.notThrows(() => {
+ stringifyObject(nestedArray);
+ });
+});
+
+test('handle circular references in arrays', t => {
+ const array2 = [];
+ const array = [array2];
+ array2[0] = array2;
+
+ t.notThrows(() => {
+ stringifyObject(array);
+ });
+});
+
+test('stringify complex circular arrays', t => {
+ const array = [[[]]];
+ array[0].push(array);
+ array[0][0].push(array, 10);
+ array[0][0][0] = array;
+ t.is(stringifyObject(array), '[\n\t[\n\t\t[\n\t\t\t"[Circular]",\n\t\t\t10\n\t\t],\n\t\t"[Circular]"\n\t]\n]');
+});
+
+test('allows short objects to be one-lined', t => {
+ const object = {id: 8, name: 'Jane'};
+
+ t.is(stringifyObject(object), '{\n\tid: 8,\n\tname: \'Jane\'\n}');
+ t.is(stringifyObject(object, {inlineCharacterLimit: 21}), '{id: 8, name: \'Jane\'}');
+ t.is(stringifyObject(object, {inlineCharacterLimit: 20}), '{\n\tid: 8,\n\tname: \'Jane\'\n}');
+});
+
+test('allows short arrays to be one-lined', t => {
+ const array = ['foo', {id: 8, name: 'Jane'}, 42];
+
+ t.is(stringifyObject(array), '[\n\t\'foo\',\n\t{\n\t\tid: 8,\n\t\tname: \'Jane\'\n\t},\n\t42\n]');
+ t.is(stringifyObject(array, {inlineCharacterLimit: 34}), '[\'foo\', {id: 8, name: \'Jane\'}, 42]');
+ t.is(stringifyObject(array, {inlineCharacterLimit: 33}), '[\n\t\'foo\',\n\t{id: 8, name: \'Jane\'},\n\t42\n]');
+});
+
+test('does not mess up indents for complex objects', t => {
+ const object = {
+ arr: [1, 2, 3],
+ nested: {hello: 'world'},
+ };
+
+ t.is(stringifyObject(object), '{\n\tarr: [\n\t\t1,\n\t\t2,\n\t\t3\n\t],\n\tnested: {\n\t\thello: \'world\'\n\t}\n}');
+ t.is(stringifyObject(object, {inlineCharacterLimit: 12}), '{\n\tarr: [1, 2, 3],\n\tnested: {\n\t\thello: \'world\'\n\t}\n}');
+});
+
+test('handles non-plain object', t => {
+ // TODO: It should work without `fileURLToPath` but currently it throws for an unknown reason.
+ t.not(stringifyObject(fs.statSync(fileURLToPath(import.meta.url))), '[object Object]');
+});
+
+test('don\'t stringify non-enumerable symbols', t => {
+ const object = {
+ [Symbol('for enumerable key')]: undefined,
+ };
+ const symbol = Symbol('for non-enumerable key');
+ Object.defineProperty(object, symbol, {enumerable: false});
+
+ t.is(stringifyObject(object), '{\n\tSymbol(for enumerable key): undefined\n}');
+});