@@ -7,6 +7,9 @@ export default function convert(
77 classObject : { [ key : string ] : any } ,
88 maxDepth : number = 10 ,
99) : object {
10+ const visited = new WeakSet < any > ( ) ;
11+ visited . add ( classObject ) ;
12+
1013 // Begin conversion
1114 const convertedObj : { [ key : string ] : any } = { } ;
1215 for ( const key in classObject ) {
@@ -16,8 +19,10 @@ export default function convert(
1619 }
1720 // Handle objects (potentially cyclical)
1821 else if ( typeof classObject [ key ] === "object" ) {
19- const result = deepCloneClass ( classObject [ key ] , maxDepth ) ;
20- convertedObj [ key ] = result ;
22+ const result = deepCloneClass ( classObject [ key ] , maxDepth , visited ) ;
23+ if ( result !== maxDepthSignal ) {
24+ convertedObj [ key ] = result ;
25+ }
2126 }
2227 // Handle simple types (non-cyclical)
2328 else {
@@ -31,7 +36,7 @@ export default function convert(
3136 window . Event &&
3237 classObject instanceof window . Event
3338 ) {
34- convertedObj [ "selection" ] = serializeSelection ( maxDepth ) ;
39+ convertedObj [ "selection" ] = serializeSelection ( maxDepth , visited ) ;
3540 }
3641
3742 return convertedObj ;
@@ -40,7 +45,10 @@ export default function convert(
4045/**
4146 * Serialize the current window selection.
4247 */
43- function serializeSelection ( maxDepth : number ) : object | null {
48+ function serializeSelection (
49+ maxDepth : number ,
50+ visited : WeakSet < any > ,
51+ ) : object | null {
4452 if ( typeof window === "undefined" || ! window . getSelection ) {
4553 return null ;
4654 }
@@ -51,11 +59,11 @@ function serializeSelection(maxDepth: number): object | null {
5159 return {
5260 type : selection . type ,
5361 anchorNode : selection . anchorNode
54- ? deepCloneClass ( selection . anchorNode , maxDepth )
62+ ? deepCloneClass ( selection . anchorNode , maxDepth , visited )
5563 : null ,
5664 anchorOffset : selection . anchorOffset ,
5765 focusNode : selection . focusNode
58- ? deepCloneClass ( selection . focusNode , maxDepth )
66+ ? deepCloneClass ( selection . focusNode , maxDepth , visited )
5967 : null ,
6068 focusOffset : selection . focusOffset ,
6169 isCollapsed : selection . isCollapsed ,
@@ -67,33 +75,50 @@ function serializeSelection(maxDepth: number): object | null {
6775/**
6876 * Recursively convert a class-based object to a plain object.
6977 */
70- function deepCloneClass ( x : any , _maxDepth : number ) : object {
78+ function deepCloneClass (
79+ x : any ,
80+ _maxDepth : number ,
81+ visited : WeakSet < any > ,
82+ ) : object {
7183 const maxDepth = _maxDepth - 1 ;
7284
7385 // Return an indicator if maxDepth is reached
7486 if ( maxDepth <= 0 && typeof x === "object" ) {
7587 return maxDepthSignal ;
7688 }
7789
78- // Convert array-like class (e.g., NodeList, ClassList, HTMLCollection)
79- if (
80- Array . isArray ( x ) ||
81- ( typeof x ?. length === "number" &&
82- typeof x [ Symbol . iterator ] === "function" &&
83- ! Object . prototype . toString . call ( x ) . includes ( "Map" ) &&
84- ! ( x instanceof CSSStyleDeclaration ) )
85- ) {
86- return classToArray ( x , maxDepth ) ;
90+ if ( visited . has ( x ) ) {
91+ return maxDepthSignal ;
8792 }
93+ visited . add ( x ) ;
8894
89- // Convert mapping-like class (e.g., Node, Map, Set)
90- return classToObject ( x , maxDepth ) ;
95+ try {
96+ // Convert array-like class (e.g., NodeList, ClassList, HTMLCollection)
97+ if (
98+ Array . isArray ( x ) ||
99+ ( typeof x ?. length === "number" &&
100+ typeof x [ Symbol . iterator ] === "function" &&
101+ ! Object . prototype . toString . call ( x ) . includes ( "Map" ) &&
102+ ! ( x instanceof CSSStyleDeclaration ) )
103+ ) {
104+ return classToArray ( x , maxDepth , visited ) ;
105+ }
106+
107+ // Convert mapping-like class (e.g., Node, Map, Set)
108+ return classToObject ( x , maxDepth , visited ) ;
109+ } finally {
110+ visited . delete ( x ) ;
111+ }
91112}
92113
93114/**
94115 * Convert an array-like class to a plain array.
95116 */
96- function classToArray ( x : any , maxDepth : number ) : Array < any > {
117+ function classToArray (
118+ x : any ,
119+ maxDepth : number ,
120+ visited : WeakSet < any > ,
121+ ) : Array < any > {
97122 const result : Array < any > = [ ] ;
98123 for ( let i = 0 ; i < x . length ; i ++ ) {
99124 // Skip anything that should not be converted
@@ -102,7 +127,7 @@ function classToArray(x: any, maxDepth: number): Array<any> {
102127 }
103128 // Only push objects as if we haven't reached max depth
104129 else if ( typeof x [ i ] === "object" ) {
105- const converted = deepCloneClass ( x [ i ] , maxDepth ) ;
130+ const converted = deepCloneClass ( x [ i ] , maxDepth , visited ) ;
106131 if ( converted !== maxDepthSignal ) {
107132 result . push ( converted ) ;
108133 }
@@ -120,7 +145,11 @@ function classToArray(x: any, maxDepth: number): Array<any> {
120145 * We must iterate through it with a for-loop in order to gain
121146 * access to properties from all parent classes.
122147 */
123- function classToObject ( x : any , maxDepth : number ) : object {
148+ function classToObject (
149+ x : any ,
150+ maxDepth : number ,
151+ visited : WeakSet < any > ,
152+ ) : object {
124153 const result : { [ key : string ] : any } = { } ;
125154 for ( const key in x ) {
126155 // Skip anything that should not be converted
@@ -129,7 +158,7 @@ function classToObject(x: any, maxDepth: number): object {
129158 }
130159 // Add objects as a property if we haven't reached max depth
131160 else if ( typeof x [ key ] === "object" ) {
132- const converted = deepCloneClass ( x [ key ] , maxDepth ) ;
161+ const converted = deepCloneClass ( x [ key ] , maxDepth , visited ) ;
133162 if ( converted !== maxDepthSignal ) {
134163 result [ key ] = converted ;
135164 }
@@ -149,7 +178,7 @@ function classToObject(x: any, maxDepth: number): object {
149178 ) {
150179 const dataset = x [ "dataset" ] ;
151180 if ( ! shouldIgnoreValue ( dataset , "dataset" , x ) ) {
152- const converted = deepCloneClass ( dataset , maxDepth ) ;
181+ const converted = deepCloneClass ( dataset , maxDepth , visited ) ;
153182 if ( converted !== maxDepthSignal ) {
154183 result [ "dataset" ] = converted ;
155184 }
@@ -170,7 +199,7 @@ function classToObject(x: any, maxDepth: number): object {
170199 if ( typeof val === "object" ) {
171200 // Ensure files have enough depth to be serialized
172201 const propDepth = prop === "files" ? Math . max ( maxDepth , 3 ) : maxDepth ;
173- const converted = deepCloneClass ( val , propDepth ) ;
202+ const converted = deepCloneClass ( val , propDepth , visited ) ;
174203 if ( converted !== maxDepthSignal ) {
175204 result [ prop ] = converted ;
176205 }
@@ -199,7 +228,7 @@ function classToObject(x: any, maxDepth: number): object {
199228 ! shouldIgnoreValue ( element , element . name , x )
200229 ) {
201230 if ( typeof element === "object" ) {
202- const converted = deepCloneClass ( element , maxDepth ) ;
231+ const converted = deepCloneClass ( element , maxDepth , visited ) ;
203232 if ( converted !== maxDepthSignal ) {
204233 result [ element . name ] = converted ;
205234 }
0 commit comments