Skip to content

Commit edfe2f3

Browse files
committed
fix: copying on circular JSON/Array throws error
1 parent d434d99 commit edfe2f3

File tree

3 files changed

+69
-3
lines changed

3 files changed

+69
-3
lines changed

src/hooks/useCopyToClipboard.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +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'
67

78
/**
89
* useClipboard hook accepts one argument options in which copied status timeout duration is defined (defaults to 2000). Hook returns object with properties:
@@ -51,9 +52,8 @@ export function useClipboard ({ timeout = 2000 } = {}) {
5152
}]`, error)
5253
}
5354
} else {
54-
const valueToCopy = JSON.stringify(
55+
const valueToCopy = circularStringify(
5556
typeof value === 'function' ? value.toString() : value,
56-
null,
5757
' '
5858
)
5959
if ('clipboard' in navigator) {

src/utils/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,23 @@ export function segmentArray<T> (arr: T[], size: number): T[][] {
150150
}
151151
return result
152152
}
153+
154+
// https://stackoverflow.com/a/72457899
155+
export function circularStringify (obj: any, space?: string | number) {
156+
const seenValues = []
157+
158+
function circularReplacer (key: string | number, value: any) {
159+
if (typeof value === 'object' && value !== null && Object.keys(value).length) {
160+
const stackSize = seenValues.length
161+
if (stackSize) {
162+
// clean up expired references
163+
for (let n = stackSize - 1; seenValues[n][key] !== value; --n) { seenValues.pop() }
164+
if (seenValues.includes(value)) return '[Circular]'
165+
}
166+
seenValues.push(value)
167+
}
168+
return value
169+
}
170+
171+
return JSON.stringify(obj, circularReplacer, space)
172+
}

tests/util.test.tsx

Lines changed: 47 additions & 1 deletion
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 { segmentArray } from '../src/utils'
7+
import { circularStringify, segmentArray } from '../src/utils'
88

99
describe('function applyValue', () => {
1010
const patches: any[] = [{}, undefined, 1, '2', 3n, 0.4]
@@ -235,3 +235,49 @@ describe('function segmentArray', () => {
235235
])
236236
})
237237
})
238+
239+
describe('function circularStringify', () => {
240+
test('should works as JSON.stringify', () => {
241+
const obj = { foo: 1, bar: 2 }
242+
expect(circularStringify(obj)).to.eq(JSON.stringify(obj))
243+
})
244+
245+
test('should works with circular reference in object', () => {
246+
const obj = {
247+
foo: 1,
248+
bar: {
249+
foo: 2,
250+
bar: null
251+
}
252+
}
253+
obj.bar.bar = obj.bar
254+
expect(circularStringify(obj)).to.eq('{"foo":1,"bar":{"foo":2,"bar":"[Circular]"}}')
255+
})
256+
257+
test('should works with circular reference in array', () => {
258+
const array = [1, 2, 3, 4, 5]
259+
// @ts-expect-error ignore
260+
array[2] = array
261+
expect(circularStringify(array)).to.eq('[1,2,"[Circular]",4,5]')
262+
})
263+
264+
test('should works with complex circular object', () => {
265+
const obj = {
266+
a: {
267+
b: {
268+
c: 1,
269+
d: 2
270+
}
271+
},
272+
e: {
273+
f: 3,
274+
g: 4
275+
}
276+
}
277+
// @ts-expect-error ignore
278+
obj.a.b.e = obj.e
279+
// @ts-expect-error ignore
280+
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]"}}')
282+
})
283+
})

0 commit comments

Comments
 (0)