diff --git a/docs/pages/apis.mdx b/docs/pages/apis.mdx index 6084974b..e597beb8 100644 --- a/docs/pages/apis.mdx +++ b/docs/pages/apis.mdx @@ -10,12 +10,16 @@ | `sx` | `SxProps` | - | [The `sx` prop](https://mui.com/system/getting-started/the-sx-prop/) lets you style elements inline, using values from the theme. | | `indentWidth` | `number` | 3 | Indent width for nested objects | | `keyRenderer` | `{when: (props) => boolean}` | - | Customize a key, if `keyRenderer.when` returns `true`. | -| `valueTypes` | `ValueTypes` | - | Customize the definition of data types. See [Defining Data Types](/how-to/data-types) | +| `valueTypes` | `ValueTypes` | - | Customize the definition of data types. See [Defining Data Types](/how-to/data-types) | +| `enableAdd` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable add feature. Provide a function to customize this behavior by returning a boolean based on the value and path. | +| `enableDelete` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable delete feature. Provide a function to customize this behavior by returning a boolean based on the value and path. | +| `enableClipboard` | `boolean` | `false` | Whether enable clipboard feature. | +| `editable` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable edit feature. Provide a function to customize this behavior by returning a boolean based on the value and path. | | `onChange` | `(path, oldVal, newVal) => void` | - | Callback when value changed. | | `onCopy` | `(path, value) => void` | - | Callback when value copied, you can use it to customize the copy behavior.
\*Note: you will have to write the data to the clipboard by yourself. | | `onSelect` | `(path, value) => void` | - | Callback when value selected. | -| `enableClipboard` | `boolean` | `true` | Whether enable clipboard feature. | -| `editable` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable edit feature. Provide a function to customize this behavior by returning a boolean based on the value and path. | +| `onAdd` | `(path) => void` | - | Callback when the add button is clicked. This is the function which implements the add feature. Please see the [DEMO](/full) for more details. | +| `onDelete` | `(path) => void` | - | Callback when the delete button is clicked. This is the function which implements the delete feature. Please see the [DEMO](/full) for more details. | | `defaultInspectDepth` | `number` | 5 | Default inspect depth for nested objects.

_\* If the number is set too large, it could result in performance issues._ | | `defaultInspectControl` | `(path, currentValue) => boolean` | - | Whether expand or collapse a field by default. Using this will override `defaultInspectDepth`. | | `maxDisplayLength` | `number` | 30 | Hide items after reaching the count.
`Array` and `Object` will be affected.

_\* If the number is set too large, it could result in performance issues._ | diff --git a/docs/pages/full/index.tsx b/docs/pages/full/index.tsx index 4eeaef08..b23dcb3a 100644 --- a/docs/pages/full/index.tsx +++ b/docs/pages/full/index.tsx @@ -16,12 +16,15 @@ import { import type { DataType, JsonViewerKeyRenderer, + JsonViewerOnAdd, JsonViewerOnChange, + JsonViewerOnDelete, JsonViewerTheme } from '@textea/json-viewer' import { applyValue, defineDataType, + deleteValue, JsonViewer, stringType } from '@textea/json-viewer' @@ -336,6 +339,8 @@ const IndexPage: FC = () => { highlightUpdates={highlightUpdates} indentWidth={indent} theme={theme} + enableAdd={true} + enableDelete={true} displayDataTypes={displayDataTypes} displaySize={displaySize} groupArraysAfterLength={groupArraysAfterLength} @@ -345,6 +350,17 @@ const IndexPage: FC = () => { linkType, imageDataType ]} + onAdd={ + useCallback( + (path) => { + const key = prompt('Key:') + if (key === null) return + const value = prompt('Value:') + if (value === null) return + setSrc(src => applyValue(src, [...path, key], value)) + }, [] + ) + } onChange={ useCallback( (path, oldValue, newValue) => { @@ -352,6 +368,13 @@ const IndexPage: FC = () => { }, [] ) } + onDelete={ + useCallback( + (path, value) => { + setSrc(src => deleteValue(src, path, value)) + }, [] + ) + } sx={{ paddingLeft: 2 }} diff --git a/docs/pages/how-to/data-types.mdx b/docs/pages/how-to/data-types.mdx index 55c1194a..dcef7d48 100644 --- a/docs/pages/how-to/data-types.mdx +++ b/docs/pages/how-to/data-types.mdx @@ -25,6 +25,7 @@ The `is` function takes a value and a path and returns true if the value belongs The `Component` prop is a React component that renders the value of the data type. It receives a `DataItemProps` object as a `prop`, which includes the following: +- `props.path` - The path to the value. - `props.value` - The value to render. - `props.inspect` - A Boolean flag indicating whether the value is being inspected (expanded). - `props.setInspect` - A function that can be used to toggle the inspect state. diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx index d7d858a2..0642b348 100644 --- a/src/components/DataKeyPair.tsx +++ b/src/components/DataKeyPair.tsx @@ -8,12 +8,14 @@ import { useInspect } from '../hooks/useInspect' import { useJsonViewerStore } from '../stores/JsonViewerStore' import { useTypeComponents } from '../stores/typeRegistry' import type { DataItemProps } from '../type' -import { copyString, getValueSize } from '../utils' +import { copyString, getValueSize, isPlainObject } from '../utils' import { + AddBoxIcon, CheckIcon, ChevronRightIcon, CloseIcon, ContentCopyIcon, + DeleteIcon, EditIcon, ExpandMoreIcon } from './Icons' @@ -82,6 +84,51 @@ export const DataKeyPair: FC = (props) => { const isRoot = root === value const isNumberKey = Number.isInteger(Number(key)) + const storeEnableAdd = useJsonViewerStore(store => store.enableAdd) + const onAdd = useJsonViewerStore(store => store.onAdd) + const enableAdd = useMemo(() => { + if (!onAdd || nestedIndex !== undefined) return false + + if (storeEnableAdd === false) { + return false + } + if (propsEditable === false) { + // props.editable is false which means we cannot provide the suitable way to edit it + return false + } + if (typeof storeEnableAdd === 'function') { + return !!storeEnableAdd(path, value) + } + + if (Array.isArray(value) || isPlainObject(value)) { + return true + } + + return false + }, [onAdd, nestedIndex, path, storeEnableAdd, propsEditable, value]) + + const storeEnableDelete = useJsonViewerStore(store => store.enableDelete) + const onDelete = useJsonViewerStore(store => store.onDelete) + const enableDelete = useMemo(() => { + if (!onDelete || nestedIndex !== undefined) return false + + if (isRoot) { + // don't allow delete root + return false + } + if (storeEnableDelete === false) { + return false + } + if (propsEditable === false) { + // props.editable is false which means we cannot provide the suitable way to edit it + return false + } + if (typeof storeEnableDelete === 'function') { + return !!storeEnableDelete(path, value) + } + return storeEnableDelete + }, [onDelete, nestedIndex, isRoot, path, storeEnableDelete, propsEditable, value]) + const enableClipboard = useJsonViewerStore(store => store.enableClipboard) const { copy, copied } = useClipboard() @@ -205,6 +252,26 @@ export const DataKeyPair: FC = (props) => { )} + {enableAdd && ( + { + event.preventDefault() + onAdd?.(path) + }} + > + + + )} + {enableDelete && ( + { + event.preventDefault() + onDelete?.(path, value) + }} + > + + + )} ) }, @@ -217,9 +284,13 @@ export const DataKeyPair: FC = (props) => { editable, editing, enableClipboard, + enableAdd, + enableDelete, tempValue, path, value, + onAdd, + onDelete, startEditing, abortEditing, commitEditing diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index 20bd5bae..ea688711 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -10,6 +10,7 @@ const BaseIcon: FC = ({ d, ...props }) => { ) } +const AddBox = 'M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V5h14zm-8-2h2v-4h4v-2h-4V7h-2v4H7v2h4z' const Check = 'M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z' const ChevronRight = 'M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z' const CircularArrows = 'M 12 2 C 10.615 1.998 9.214625 2.2867656 7.890625 2.8847656 L 8.9003906 4.6328125 C 9.9043906 4.2098125 10.957 3.998 12 4 C 15.080783 4 17.738521 5.7633175 19.074219 8.3222656 L 17.125 9 L 21.25 11 L 22.875 7 L 20.998047 7.6523438 C 19.377701 4.3110398 15.95585 2 12 2 z M 6.5097656 4.4882812 L 2.2324219 5.0820312 L 3.734375 6.3808594 C 1.6515335 9.4550558 1.3615962 13.574578 3.3398438 17 C 4.0308437 18.201 4.9801562 19.268234 6.1601562 20.115234 L 7.1699219 18.367188 C 6.3019219 17.710187 5.5922656 16.904 5.0722656 16 C 3.5320014 13.332354 3.729203 10.148679 5.2773438 7.7128906 L 6.8398438 9.0625 L 6.5097656 4.4882812 z M 19.929688 13 C 19.794687 14.08 19.450734 15.098 18.927734 16 C 17.386985 18.668487 14.531361 20.090637 11.646484 19.966797 L 12.035156 17.9375 L 8.2402344 20.511719 L 10.892578 23.917969 L 11.265625 21.966797 C 14.968963 22.233766 18.681899 20.426323 20.660156 17 C 21.355156 15.801 21.805219 14.445 21.949219 13 L 19.929688 13 z' @@ -17,6 +18,11 @@ const Close = 'M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 const ContentCopy = 'M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z' const Edit = 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z' const ExpandMore = 'M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z' +const Delete = 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6zM8 9h8v10H8zm7.5-5l-1-1h-5l-1 1H5v2h14V4z' + +export const AddBoxIcon: FC = (props) => { + return +} export const CheckIcon: FC = (props) => { return @@ -45,3 +51,7 @@ export const EditIcon: FC = (props) => { export const ExpandMoreIcon: FC = (props) => { return } + +export const DeleteIcon: FC = (props) => { + return +} diff --git a/src/index.tsx b/src/index.tsx index 0519816b..f3180eb8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -50,19 +50,23 @@ const JsonViewerInner: FC = (props) => { value: props.value })) }, [props.value, setState]) - useSetIfNotUndefinedEffect('editable', props.editable) + useSetIfNotUndefinedEffect('rootName', props.rootName) useSetIfNotUndefinedEffect('indentWidth', props.indentWidth) - useSetIfNotUndefinedEffect('onChange', props.onChange) - useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength) useSetIfNotUndefinedEffect('keyRenderer', props.keyRenderer) - useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength) + useSetIfNotUndefinedEffect('enableAdd', props.enableAdd) + useSetIfNotUndefinedEffect('enableDelete', props.enableDelete) useSetIfNotUndefinedEffect('enableClipboard', props.enableClipboard) - useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates) - useSetIfNotUndefinedEffect('rootName', props.rootName) - useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes) - useSetIfNotUndefinedEffect('displaySize', props.displaySize) + useSetIfNotUndefinedEffect('editable', props.editable) + useSetIfNotUndefinedEffect('onChange', props.onChange) useSetIfNotUndefinedEffect('onCopy', props.onCopy) useSetIfNotUndefinedEffect('onSelect', props.onSelect) + useSetIfNotUndefinedEffect('onAdd', props.onAdd) + useSetIfNotUndefinedEffect('onDelete', props.onDelete) + useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength) + useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength) + useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes) + useSetIfNotUndefinedEffect('displaySize', props.displaySize) + useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates) useEffect(() => { if (props.theme === 'light') { setState({ @@ -179,4 +183,4 @@ export const JsonViewer = function JsonViewer (props: JsonViewerProps null DefaultKeyRenderer.when = () => false export type JsonViewerState = { - inspectCache: Record - hoverPath: { path: Path; nestedIndex?: number } | null + rootName: false | string indentWidth: number - groupArraysAfterLength: number + keyRenderer: JsonViewerKeyRenderer + enableAdd: boolean | ((path: Path, currentValue: U) => boolean) + enableDelete: boolean | ((path: Path, currentValue: U) => boolean) enableClipboard: boolean - highlightUpdates: boolean - maxDisplayLength: number + editable: boolean | ((path: Path, currentValue: U) => boolean) + onChange: JsonViewerOnChange + onCopy: JsonViewerOnCopy | undefined + onSelect: JsonViewerOnSelect | undefined + onAdd: JsonViewerOnAdd | undefined + onDelete: JsonViewerOnDelete | undefined defaultInspectDepth: number defaultInspectControl?: (path: Path, value: unknown) => boolean + maxDisplayLength: number + groupArraysAfterLength: number collapseStringsAfterLength: number objectSortKeys: boolean | ((a: string, b: string) => number) quotesOnKeys: boolean - colorspace: Colorspace - editable: boolean | ((path: Path, currentValue: U) => boolean) displayDataTypes: boolean - rootName: false | string - prevValue: T | undefined - value: T - onChange: JsonViewerOnChange - onCopy: JsonViewerOnCopy | undefined - onSelect: JsonViewerOnSelect | undefined - keyRenderer: JsonViewerKeyRenderer displaySize: boolean | ((path: Path, value: unknown) => boolean) + highlightUpdates: boolean + + inspectCache: Record + hoverPath: { path: Path; nestedIndex?: number } | null + colorspace: Colorspace + value: T + prevValue: T | undefined getInspectCache: (path: Path, nestedIndex?: number) => boolean setInspectCache: (path: Path, action: SetStateAction, nestedIndex?: number) => void @@ -50,33 +55,38 @@ export type JsonViewerState = { export const createJsonViewerStore = (props: JsonViewerProps) => { return create()((set, get) => ({ // provided by user - enableClipboard: props.enableClipboard ?? true, - highlightUpdates: props.highlightUpdates ?? false, - indentWidth: props.indentWidth ?? 3, - groupArraysAfterLength: props.groupArraysAfterLength ?? 100, - collapseStringsAfterLength: - (props.collapseStringsAfterLength === false) - ? Number.MAX_VALUE - : props.collapseStringsAfterLength ?? 50, - maxDisplayLength: props.maxDisplayLength ?? 30, rootName: props.rootName ?? 'root', + indentWidth: props.indentWidth ?? 3, + keyRenderer: props.keyRenderer ?? DefaultKeyRenderer, + enableAdd: props.enableAdd ?? false, + enableDelete: props.enableDelete ?? false, + enableClipboard: props.enableClipboard ?? true, + editable: props.editable ?? false, onChange: props.onChange ?? (() => {}), onCopy: props.onCopy ?? undefined, onSelect: props.onSelect ?? undefined, - keyRenderer: props.keyRenderer ?? DefaultKeyRenderer, - editable: props.editable ?? false, + onAdd: props.onAdd ?? undefined, + onDelete: props.onDelete ?? undefined, defaultInspectDepth: props.defaultInspectDepth ?? 5, defaultInspectControl: props.defaultInspectControl ?? undefined, + maxDisplayLength: props.maxDisplayLength ?? 30, + groupArraysAfterLength: props.groupArraysAfterLength ?? 100, + collapseStringsAfterLength: + (props.collapseStringsAfterLength === false) + ? Number.MAX_VALUE + : props.collapseStringsAfterLength ?? 50, objectSortKeys: props.objectSortKeys ?? false, quotesOnKeys: props.quotesOnKeys ?? true, displayDataTypes: props.displayDataTypes ?? true, + displaySize: props.displaySize ?? true, + highlightUpdates: props.highlightUpdates ?? false, + // internal state inspectCache: {}, hoverPath: null, colorspace: lightColorspace, value: props.value, prevValue: undefined, - displaySize: props.displaySize ?? true, getInspectCache: (path, nestedIndex) => { const target = nestedIndex !== undefined diff --git a/src/type.ts b/src/type.ts index 6903d956..7a501e83 100644 --- a/src/type.ts +++ b/src/type.ts @@ -19,7 +19,7 @@ export type JsonViewerOnChange = ( /** * @param path path to the target value - * @param value + * @param value the value to be copied * @param copy the function to copy the value to clipboard */ export type JsonViewerOnCopy = ( @@ -30,13 +30,29 @@ export type JsonViewerOnCopy = ( /** * @param path path to the target value - * @param value + * @param value the value to be selected */ export type JsonViewerOnSelect = ( path: Path, value: U, ) => void +/** + * @param path path to the parent target where the value will be added + */ +export type JsonViewerOnAdd = ( + path: Path, +) => void + +/** + * @param path path to the target value + * @param value the value to be deleted + */ +export type JsonViewerOnDelete = ( + path: Path, + value: U, +) => void + export interface DataItemProps { inspect: boolean setInspect: Dispatch> @@ -151,18 +167,28 @@ export type JsonViewerProps = { * @see https://viewer.textea.io/how-to/data-types */ valueTypes?: DataType[] - /** Callback when value changed. */ - onChange?: JsonViewerOnChange - /** Callback when value copied, you can use it to customize the copy behavior.
\*Note: you will have to write the data to the clipboard by yourself. */ - onCopy?: JsonViewerOnCopy - /** Callback when value selected. */ - onSelect?: JsonViewerOnSelect + + /** + * Whether enable add feature. + * + * @default false + * */ + enableAdd?: boolean | ((path: Path, currentValue: U) => boolean) + + /** + * Whether enable delete feature. + * + * @default false + * */ + enableDelete?: boolean | ((path: Path, currentValue: U) => boolean) + /** * Whether enable clipboard feature. * * @default true */ enableClipboard?: boolean + /** * Whether this value can be edited. * @@ -171,6 +197,18 @@ export type JsonViewerProps = { * @default false */ editable?: boolean | ((path: Path, currentValue: U) => boolean) + + /** Callback when value changed. */ + onChange?: JsonViewerOnChange + /** Callback when value copied, you can use it to customize the copy behavior.
\*Note: you will have to write the data to the clipboard by yourself. */ + onCopy?: JsonViewerOnCopy + /** Callback when value selected. */ + onSelect?: JsonViewerOnSelect + /** Callback when the add button is clicked. This is the function which implements the add feature. Please see the official demo for more details. */ + onAdd?: JsonViewerOnAdd + /** Callback when the delete button is clicked. This is the function which implements the delete feature. Please see the official demo for more details. */ + onDelete?: JsonViewerOnDelete + /** * Default inspect depth for nested objects. * _If the number is set too large, it could result in performance issues._ @@ -178,12 +216,14 @@ export type JsonViewerProps = { * @default 5 */ defaultInspectDepth?: number + /** * Default inspect control for nested objects. * * Provide a function to customize which fields should be expanded by default. */ defaultInspectControl?: (path: Path, value: unknown) => boolean + /** * Hide items after reaching the count. * `Array` and `Object` will be affected. @@ -193,6 +233,7 @@ export type JsonViewerProps = { * @default 30 */ maxDisplayLength?: number + /** * When an integer value is assigned, arrays will be displayed in groups by count of the value. * Groups are displayed with bracket notation and can be expanded and collapsed by clicking on the brackets. @@ -200,6 +241,7 @@ export type JsonViewerProps = { * @default 100 */ groupArraysAfterLength?: number + /** * Cut off the string after reaching the count. * Collapsed strings are followed by an ellipsis. diff --git a/src/utils/index.ts b/src/utils/index.ts index 5cdb6e10..d67cdcfc 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,7 +5,7 @@ import type { DataItemProps, DataType, EditorProps, Path } from '../type' // reference: https://github.com/immerjs/immer/blob/main/src/utils/common.ts const objectCtorString = Object.prototype.constructor.toString() -function isPlainObject (value: any): boolean { +export function isPlainObject (value: any): boolean { if (!value || typeof value !== 'object') return false const proto = Object.getPrototypeOf(value) @@ -38,6 +38,9 @@ function shallowCopy (value: any) { return value } +/** + * Apply a value to a given path of an object. + */ export function applyValue (input: any, path: (string | number)[], value: any) { if (typeof input !== 'object' || input === null) { if (path.length !== 0) { @@ -63,6 +66,38 @@ export function applyValue (input: any, path: (string | number)[], value: any) { return input } +/** + * Delete a value from a given path of an object. + */ +export function deleteValue (input: any, path: (string | number)[], value: any) { + if (typeof input !== 'object' || input === null) { + if (path.length !== 0) { + throw new Error('path is incorrect') + } + return value + } + + const shouldCopy = shouldShallowCopy(input) + if (shouldCopy) input = shallowCopy(input) + + const [key, ...restPath] = path + if (key !== undefined) { + if (key === '__proto__') { + throw new TypeError('Modification of prototype is not allowed') + } + if (restPath.length > 0) { + input[key] = deleteValue(input[key], restPath, value) + } else { + if (Array.isArray(input)) { + input.splice(Number(key), 1) + } else { + delete input[key] + } + } + } + return input +} + /** * @deprecated use `defineDataType` instead */ diff --git a/tests/util.test.tsx b/tests/util.test.tsx index 350832b4..65ab2e33 100644 --- a/tests/util.test.tsx +++ b/tests/util.test.tsx @@ -3,7 +3,7 @@ import type { ComponentType } from 'react' import { describe, expect, test } from 'vitest' import type { DataItemProps, Path } from '../src' -import { applyValue, createDataType, isCycleReference } from '../src' +import { applyValue, createDataType, deleteValue, isCycleReference } from '../src' import { safeStringify, segmentArray } from '../src/utils' describe('function applyValue', () => { @@ -17,6 +17,13 @@ describe('function applyValue', () => { }).toThrow() }) + test('prototype polluting', () => { + const original = {} + expect(() => { + applyValue(original, ['__proto__', 'polluted'], 1) + }).toThrow() + }) + test('undefined', () => { patches.forEach(patch => { const newValue = applyValue(undefined, [], patch) @@ -90,6 +97,99 @@ describe('function applyValue', () => { }) }) +describe('function deleteValue', () => { + const patches: any[] = [{}, undefined, 1, '2', 3n, 0.4] + test('incorrect arguments', () => { + expect(() => { + deleteValue({}, ['not', 'exist'], 1) + }).toThrow() + expect(() => { + deleteValue(1, ['not', 'exist'], 1) + }).toThrow() + }) + + test('prototype polluting', () => { + const original = {} + expect(() => { + deleteValue(original, ['__proto__', 'polluted'], 1) + }).toThrow() + }) + + test('undefined', () => { + patches.forEach(patch => { + const newValue = deleteValue(undefined, [], patch) + expect(newValue).is.eq(patch) + }) + }) + + test('null', () => { + patches.forEach(patch => { + const newValue = deleteValue(null, [], patch) + expect(newValue).is.eq(patch) + }) + }) + + test('number', () => { + patches.forEach(patch => { + const newValue = deleteValue(1, [], patch) + expect(newValue).is.eq(patch) + }) + patches.forEach(patch => { + const newValue = deleteValue(114514, [], patch) + expect(newValue).is.eq(patch) + }) + }) + + test('string', () => { + patches.forEach(patch => { + const newValue = deleteValue('', [], patch) + expect(newValue).is.eq(patch) + }) + }) + + test('object', () => { + const original = { + foo: 1, + bar: 2 + } + const newValue = deleteValue(original, ['foo'], 1) + expect(newValue).is.deep.eq({ + bar: 2 + }) + }) + + test('object nested', () => { + const original = { + foo: { + bar: { + baz: 1, + qux: 2 + } + } + } + const newValue = deleteValue(original, ['foo', 'bar', 'qux'], 2) + expect(newValue).is.deep.eq({ + foo: { + bar: { + baz: 1 + } + } + }) + }) + + test('array', () => { + const original = [1, 2, 3] + const newValue = deleteValue(original, [1], 2) + expect(newValue).is.deep.eq([1, 3]) + }) + + test('array nested', () => { + const original = [1, [2, [3, 4]]] + const newValue = deleteValue(original, [1, 1, 1], 4) + expect(newValue).is.deep.eq([1, [2, [3]]]) + }) +}) + describe('function isCycleReference', () => { test('root is leaf', () => { const root = {