@@ -28,26 +28,60 @@ interface DecycledObject {
2828 $ref ?: string ;
2929}
3030
31+ /**
32+ * Makes a deep copy of an object or array, assuring that there is at most
33+ * one instance of each object or array in the resulting structure. The
34+ * duplicate references (which might be forming cycles) are replaced with
35+ * an object of the form `{"$ref": PATH}` where the PATH is a JSONPath
36+ * string that locates the first occurance.
37+ *
38+ * @example
39+ * ```ts
40+ * var a = [];
41+ * a[0] = a;
42+ * JSON.stringify(decycle(a)); // '[{"$ref":"$"}]'
43+ * ```
44+ *
45+ * If a replacer function is provided, then it will be called for each value.
46+ * A replacer function receives a value and returns a replacement value.
47+ *
48+ * JSONPath is used to locate the unique object. `$` indicates the top level of
49+ * the object or array. `[NUMBER]` or `[STRING]` indicates a child element or
50+ * property.
51+ *
52+ * @param object - The object or array to decycle.
53+ * @param replacer - Optional replacer function called for each value.
54+ * @returns A deep copy of the object with circular references replaced by `$ref` objects.
55+ */
56+ export function decycle ( object : unknown , replacer ?: ReplacerFunction ) {
57+ const objects = new WeakMap < object , string > ( ) ; // object to path mappings
58+
59+ return deepCopy ( object , '$' , objects , replacer ) ;
60+ }
61+
62+ /**
63+ * Recursively deep copies a value, replacing circular references with
64+ * `{"$ref": PATH}` objects.
65+ *
66+ * @param value - The current value to copy.
67+ * @param path - The JSONPath to the current value.
68+ * @param objects - WeakMap tracking already-visited objects and their paths.
69+ * @param replacer - Optional replacer function called for each value.
70+ * @returns The deep-copied value.
71+ */
3172function deepCopy (
3273 value : unknown ,
3374 path : string ,
3475 objects : WeakMap < object , string > ,
3576 replacer ?: ReplacerFunction ,
3677) : unknown {
37- // Recurses through the object, producing the deep copy.
38-
39- let old_path : string | undefined ; // The path of an earlier occurance of value
40- let nu : unknown [ ] | DecycledObject ; // The new object or array
41-
42- // If a replacer function was provided, then call it to get a replacement value.
78+ let old_path : string | undefined ;
79+ let nu : unknown [ ] | DecycledObject ;
4380
4481 if ( replacer !== undefined ) {
4582 value = replacer ( value ) ;
4683 }
4784
48- // typeof null === "object", so go on if this value is really an object but not
49- // one of the weird builtin objects.
50-
5185 if (
5286 typeof value === 'object' &&
5387 value !== null &&
@@ -57,21 +91,13 @@ function deepCopy(
5791 ! ( value instanceof RegExp ) &&
5892 ! ( value instanceof String )
5993 ) {
60- // If the value is an object or array, look to see if we have already
61- // encountered it. If so, return a {"$ref":PATH} object. This uses an
62- // ES6 WeakMap.
63-
6494 old_path = objects . get ( value ) ;
6595 if ( old_path !== undefined ) {
6696 return { $ref : old_path } ;
6797 }
6898
69- // Otherwise, accumulate the unique value and its path.
70-
7199 objects . set ( value , path ) ;
72100
73- // If it is an array, replicate the array.
74-
75101 if ( Array . isArray ( value ) ) {
76102 nu = [ ] ;
77103 ( value as unknown [ ] ) . forEach ( function ( element : unknown , i : number ) {
@@ -83,8 +109,6 @@ function deepCopy(
83109 ) ;
84110 } ) ;
85111 } else {
86- // If it is an object, replicate the object.
87-
88112 nu = { } as DecycledObject ;
89113 Object . keys ( value as Record < string , unknown > ) . forEach ( function (
90114 name : string ,
@@ -101,33 +125,3 @@ function deepCopy(
101125 }
102126 return value ;
103127}
104-
105- export function decycle ( object : unknown , replacer ?: ReplacerFunction ) {
106- // Make a deep copy of an object or array, assuring that there is at most
107- // one instance of each object or array in the resulting structure. The
108- // duplicate references (which might be forming cycles) are replaced with
109- // an object of the form
110-
111- // {"$ref": PATH}
112-
113- // where the PATH is a JSONPath string that locates the first occurance.
114-
115- // So,
116-
117- // var a = [];
118- // a[0] = a;
119- // return JSON.stringify(JSON.decycle(a));
120-
121- // produces the string '[{"$ref":"$"}]'.
122-
123- // If a replacer function is provided, then it will be called for each value.
124- // A replacer function receives a value and returns a replacement value.
125-
126- // JSONPath is used to locate the unique object. $ indicates the top level of
127- // the object or array. [NUMBER] or [STRING] indicates a child element or
128- // property.
129-
130- const objects = new WeakMap < object , string > ( ) ; // object to path mappings
131-
132- return deepCopy ( object , '$' , objects , replacer ) ;
133- }
0 commit comments