Skip to content

Commit

Permalink
fix: copying on circular JSON/Array throws error
Browse files Browse the repository at this point in the history
  • Loading branch information
pionxzh committed Mar 19, 2023
1 parent d434d99 commit edfe2f3
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 3 deletions.
4 changes: 2 additions & 2 deletions src/hooks/useCopyToClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useRef, useState } from 'react'

import { useJsonViewerStore } from '../stores/JsonViewerStore'
import type { JsonViewerOnCopy } from '../type'
import { circularStringify } from '../utils'

/**
* useClipboard hook accepts one argument options in which copied status timeout duration is defined (defaults to 2000). Hook returns object with properties:
Expand Down Expand Up @@ -51,9 +52,8 @@ export function useClipboard ({ timeout = 2000 } = {}) {
}]`, error)
}
} else {
const valueToCopy = JSON.stringify(
const valueToCopy = circularStringify(
typeof value === 'function' ? value.toString() : value,
null,
' '
)
if ('clipboard' in navigator) {
Expand Down
20 changes: 20 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,23 @@ export function segmentArray<T> (arr: T[], size: number): T[][] {
}
return result
}

// https://stackoverflow.com/a/72457899
export function circularStringify (obj: any, space?: string | number) {
const seenValues = []

function circularReplacer (key: string | number, value: any) {
if (typeof value === 'object' && value !== null && Object.keys(value).length) {
const stackSize = seenValues.length
if (stackSize) {
// clean up expired references
for (let n = stackSize - 1; seenValues[n][key] !== value; --n) { seenValues.pop() }
if (seenValues.includes(value)) return '[Circular]'
}
seenValues.push(value)
}
return value
}

return JSON.stringify(obj, circularReplacer, space)
}
48 changes: 47 additions & 1 deletion tests/util.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, test } from 'vitest'

import type { DataItemProps, Path } from '../src'
import { applyValue, createDataType, isCycleReference } from '../src'
import { segmentArray } from '../src/utils'
import { circularStringify, segmentArray } from '../src/utils'

describe('function applyValue', () => {
const patches: any[] = [{}, undefined, 1, '2', 3n, 0.4]
Expand Down Expand Up @@ -235,3 +235,49 @@ describe('function segmentArray', () => {
])
})
})

describe('function circularStringify', () => {
test('should works as JSON.stringify', () => {
const obj = { foo: 1, bar: 2 }
expect(circularStringify(obj)).to.eq(JSON.stringify(obj))
})

test('should works with circular reference in object', () => {
const obj = {
foo: 1,
bar: {
foo: 2,
bar: null
}
}
obj.bar.bar = obj.bar
expect(circularStringify(obj)).to.eq('{"foo":1,"bar":{"foo":2,"bar":"[Circular]"}}')
})

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

test('should works with complex circular object', () => {
const obj = {
a: {
b: {
c: 1,
d: 2
}
},
e: {
f: 3,
g: 4
}
}
// @ts-expect-error ignore
obj.a.b.e = obj.e
// @ts-expect-error ignore
obj.e.g = obj.a.b
expect(circularStringify(obj)).to.eq('{"a":{"b":{"c":1,"d":2,"e":{"f":3,"g":"[Circular]"}}},"e":{"f":3,"g":"[Circular]"}}')
})
})

0 comments on commit edfe2f3

Please sign in to comment.