Skip to content

Commit 7c46e07

Browse files
committed
fix: improve copy on BigInt / Map / Set
1 parent edfe2f3 commit 7c46e07

File tree

3 files changed

+69
-12
lines changed

3 files changed

+69
-12
lines changed

src/hooks/useCopyToClipboard.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useCallback, useRef, useState } from 'react'
33

44
import { useJsonViewerStore } from '../stores/JsonViewerStore'
55
import type { JsonViewerOnCopy } from '../type'
6-
import { circularStringify } from '../utils'
6+
import { safeStringify } from '../utils'
77

88
/**
99
* useClipboard hook accepts one argument options in which copied status timeout duration is defined (defaults to 2000). Hook returns object with properties:
@@ -52,7 +52,7 @@ export function useClipboard ({ timeout = 2000 } = {}) {
5252
}]`, error)
5353
}
5454
} else {
55-
const valueToCopy = circularStringify(
55+
const valueToCopy = safeStringify(
5656
typeof value === 'function' ? value.toString() : value,
5757
' '
5858
)

src/utils/index.ts

+34-5
Original file line numberDiff line numberDiff line change
@@ -151,22 +151,51 @@ export function segmentArray<T> (arr: T[], size: number): T[][] {
151151
return result
152152
}
153153

154-
// https://stackoverflow.com/a/72457899
155-
export function circularStringify (obj: any, space?: string | number) {
154+
export function safeStringify (obj: any, space?: string | number) {
156155
const seenValues = []
157156

158-
function circularReplacer (key: string | number, value: any) {
157+
function replacer (key: string | number, value: any) {
158+
// https://github.com/GoogleChromeLabs/jsbi/issues/30
159+
if (typeof value === 'bigint') return value.toString()
160+
161+
// Map and Set are not supported by JSON.stringify
162+
if (value instanceof Map) {
163+
if ('toJSON' in value && typeof value.toJSON === 'function') return value.toJSON()
164+
if (value.size === 0) return {}
165+
166+
if (seenValues.includes(value)) return '[Circular]'
167+
seenValues.push(value)
168+
169+
const entries = Array.from(value.entries())
170+
if (entries.every(([key]) => typeof key === 'string' || typeof key === 'number')) {
171+
return Object.fromEntries(entries)
172+
}
173+
174+
// if keys are not string or number, we can't convert to object
175+
// fallback to default behavior
176+
return {}
177+
}
178+
if (value instanceof Set) {
179+
if ('toJSON' in value && typeof value.toJSON === 'function') return value.toJSON()
180+
181+
if (seenValues.includes(value)) return '[Circular]'
182+
seenValues.push(value)
183+
184+
return Array.from(value.values())
185+
}
186+
187+
// https://stackoverflow.com/a/72457899
159188
if (typeof value === 'object' && value !== null && Object.keys(value).length) {
160189
const stackSize = seenValues.length
161190
if (stackSize) {
162191
// clean up expired references
163-
for (let n = stackSize - 1; seenValues[n][key] !== value; --n) { seenValues.pop() }
192+
for (let n = stackSize - 1; n >= 0 && seenValues[n][key] !== value; --n) { seenValues.pop() }
164193
if (seenValues.includes(value)) return '[Circular]'
165194
}
166195
seenValues.push(value)
167196
}
168197
return value
169198
}
170199

171-
return JSON.stringify(obj, circularReplacer, space)
200+
return JSON.stringify(obj, replacer, space)
172201
}

tests/util.test.tsx

+33-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { describe, expect, test } from 'vitest'
44

55
import type { DataItemProps, Path } from '../src'
66
import { applyValue, createDataType, isCycleReference } from '../src'
7-
import { circularStringify, segmentArray } from '../src/utils'
7+
import { safeStringify, segmentArray } from '../src/utils'
88

99
describe('function applyValue', () => {
1010
const patches: any[] = [{}, undefined, 1, '2', 3n, 0.4]
@@ -239,7 +239,7 @@ describe('function segmentArray', () => {
239239
describe('function circularStringify', () => {
240240
test('should works as JSON.stringify', () => {
241241
const obj = { foo: 1, bar: 2 }
242-
expect(circularStringify(obj)).to.eq(JSON.stringify(obj))
242+
expect(safeStringify(obj)).to.eq(JSON.stringify(obj))
243243
})
244244

245245
test('should works with circular reference in object', () => {
@@ -251,14 +251,14 @@ describe('function circularStringify', () => {
251251
}
252252
}
253253
obj.bar.bar = obj.bar
254-
expect(circularStringify(obj)).to.eq('{"foo":1,"bar":{"foo":2,"bar":"[Circular]"}}')
254+
expect(safeStringify(obj)).to.eq('{"foo":1,"bar":{"foo":2,"bar":"[Circular]"}}')
255255
})
256256

257257
test('should works with circular reference in array', () => {
258258
const array = [1, 2, 3, 4, 5]
259259
// @ts-expect-error ignore
260260
array[2] = array
261-
expect(circularStringify(array)).to.eq('[1,2,"[Circular]",4,5]')
261+
expect(safeStringify(array)).to.eq('[1,2,"[Circular]",4,5]')
262262
})
263263

264264
test('should works with complex circular object', () => {
@@ -278,6 +278,34 @@ describe('function circularStringify', () => {
278278
obj.a.b.e = obj.e
279279
// @ts-expect-error ignore
280280
obj.e.g = obj.a.b
281-
expect(circularStringify(obj)).to.eq('{"a":{"b":{"c":1,"d":2,"e":{"f":3,"g":"[Circular]"}}},"e":{"f":3,"g":"[Circular]"}}')
281+
expect(safeStringify(obj)).to.eq('{"a":{"b":{"c":1,"d":2,"e":{"f":3,"g":"[Circular]"}}},"e":{"f":3,"g":"[Circular]"}}')
282+
})
283+
284+
test('should works with ES6 Map', () => {
285+
const map = new Map()
286+
map.set('foo', 1)
287+
map.set('bar', 2)
288+
expect(safeStringify(map)).to.eq('{"foo":1,"bar":2}')
289+
})
290+
291+
test('should works with ES6 Set', () => {
292+
const set = new Set()
293+
set.add(1)
294+
set.add(2)
295+
expect(safeStringify(set)).to.eq('[1,2]')
296+
})
297+
298+
test('should works with ES6 Map with circular reference', () => {
299+
const map = new Map()
300+
map.set('foo', 1)
301+
map.set('bar', map)
302+
expect(safeStringify(map)).to.eq('{"foo":1,"bar":"[Circular]"}')
303+
})
304+
305+
test('should works with ES6 Set with circular reference', () => {
306+
const set = new Set()
307+
set.add(1)
308+
set.add(set)
309+
expect(safeStringify(set)).to.eq('[1,"[Circular]"]')
282310
})
283311
})

0 commit comments

Comments
 (0)