@thumbmarkjs/thumbmarkjs 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +30 -3
- package/dist/thumbmark.esm.js +1 -1
- package/dist/thumbmark.esm.js.map +1 -1
- package/dist/thumbmark.umd.js +1 -1
- package/dist/thumbmark.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/components/mathml/index.ts +74 -46
- package/src/components/permissions/index.ts +11 -6
- package/src/components/speech/index.ts +104 -0
- package/src/components/webrtc/index.ts +21 -11
- package/src/factory.ts +3 -1
- package/src/functions/api.ts +10 -7
- package/src/functions/index.ts +80 -72
- package/src/index.ts +8 -3
- package/src/options.ts +3 -10
- package/src/utils/log.ts +3 -3
- package/src/utils/stableStringify.test.ts +335 -0
- package/src/utils/stableStringify.ts +70 -0
package/src/index.ts
CHANGED
|
@@ -2,19 +2,24 @@ import {
|
|
|
2
2
|
getFingerprint,
|
|
3
3
|
getFingerprintData,
|
|
4
4
|
getFingerprintPerformance
|
|
5
|
-
|
|
5
|
+
} from './functions/legacy_functions'
|
|
6
6
|
import { getThumbmark } from './functions'
|
|
7
7
|
import { getVersion } from './utils/version';
|
|
8
8
|
import { setOption, optionsInterface, stabilizationExclusionRules } from './options'
|
|
9
9
|
import { includeComponent } from './factory'
|
|
10
10
|
import { Thumbmark } from './thumbmark'
|
|
11
11
|
import { filterThumbmarkData } from './functions/filterComponents'
|
|
12
|
+
import { stableStringify } from './utils/stableStringify'
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
Thumbmark, getThumbmark, getVersion,
|
|
12
16
|
|
|
13
|
-
export { Thumbmark, getThumbmark, getVersion,
|
|
14
|
-
|
|
15
17
|
// Filtering functions for server-side use
|
|
16
18
|
filterThumbmarkData, optionsInterface, stabilizationExclusionRules,
|
|
17
19
|
|
|
20
|
+
// Stable JSON stringify for consistent hashing
|
|
21
|
+
stableStringify,
|
|
22
|
+
|
|
18
23
|
// deprecated functions. Don't use anymore.
|
|
19
24
|
setOption, getFingerprint, getFingerprintData, getFingerprintPerformance, includeComponent
|
|
20
25
|
}
|
package/src/options.ts
CHANGED
|
@@ -5,13 +5,14 @@ export interface optionsInterface {
|
|
|
5
5
|
timeout?: number,
|
|
6
6
|
logging?: boolean,
|
|
7
7
|
api_key?: string,
|
|
8
|
+
api_endpoint?: string,
|
|
8
9
|
cache_api_call?: boolean,
|
|
9
10
|
performance?: boolean,
|
|
10
11
|
stabilize?: string[],
|
|
11
12
|
experimental?: boolean,
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export const
|
|
15
|
+
export const DEFAULT_API_ENDPOINT = 'https://api.thumbmarkjs.com';
|
|
15
16
|
|
|
16
17
|
export const defaultOptions: optionsInterface = {
|
|
17
18
|
exclude: [],
|
|
@@ -46,22 +47,14 @@ export const stabilizationExclusionRules = {
|
|
|
46
47
|
'iframe': [
|
|
47
48
|
{
|
|
48
49
|
exclude: [
|
|
49
|
-
'permissions.camera',
|
|
50
|
-
'permission.geolocation',
|
|
51
|
-
'permissions.microphone',
|
|
52
50
|
'system.applePayVersion',
|
|
53
51
|
'system.cookieEnabled',
|
|
54
52
|
],
|
|
55
53
|
browsers: ['safari']
|
|
56
54
|
},
|
|
57
55
|
{
|
|
58
|
-
exclude: [
|
|
59
|
-
'permissions.background-fetch',
|
|
60
|
-
'permissions.storage-access',
|
|
61
|
-
],
|
|
62
|
-
browsers: ['chrome', 'brave', 'edge', 'opera']
|
|
56
|
+
exclude: ['permissions']
|
|
63
57
|
}
|
|
64
|
-
|
|
65
58
|
],
|
|
66
59
|
'vpn': [
|
|
67
60
|
{ exclude: ['ip'] },
|
package/src/utils/log.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { componentInterface } from '../factory';
|
|
2
|
-
import { optionsInterface } from '../options';
|
|
2
|
+
import { optionsInterface, DEFAULT_API_ENDPOINT } from '../options';
|
|
3
3
|
import { getVersion } from './version';
|
|
4
|
-
import { API_ENDPOINT } from '../options';
|
|
5
4
|
|
|
6
5
|
// ===================== Logging (Internal) =====================
|
|
7
6
|
|
|
@@ -11,7 +10,8 @@ import { API_ENDPOINT } from '../options';
|
|
|
11
10
|
* @internal
|
|
12
11
|
*/
|
|
13
12
|
export async function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface, experimentalData: componentInterface = {}): Promise<void> {
|
|
14
|
-
const
|
|
13
|
+
const apiEndpoint = DEFAULT_API_ENDPOINT;
|
|
14
|
+
const url = `${apiEndpoint}/log`;
|
|
15
15
|
const payload = {
|
|
16
16
|
thumbmark: thisHash,
|
|
17
17
|
components: thumbmarkData,
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { stableStringify } from './stableStringify';
|
|
2
|
+
|
|
3
|
+
describe('stableStringify', () => {
|
|
4
|
+
describe('basic functionality', () => {
|
|
5
|
+
test('sorts object keys alphabetically', () => {
|
|
6
|
+
const obj = { z: 3, a: 1, m: 2 };
|
|
7
|
+
const result = stableStringify(obj);
|
|
8
|
+
expect(result).toBe('{"a":1,"m":2,"z":3}');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('produces valid JSON', () => {
|
|
12
|
+
const obj = { b: 2, a: 1, c: 3 };
|
|
13
|
+
const result = stableStringify(obj);
|
|
14
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
15
|
+
expect(JSON.parse(result)).toEqual({ a: 1, b: 2, c: 3 });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('produces consistent output for same input', () => {
|
|
19
|
+
const obj = { z: 3, a: 1, m: 2 };
|
|
20
|
+
const result1 = stableStringify(obj);
|
|
21
|
+
const result2 = stableStringify(obj);
|
|
22
|
+
expect(result1).toBe(result2);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('produces same output regardless of key insertion order', () => {
|
|
26
|
+
const obj1 = { a: 1, b: 2, c: 3 };
|
|
27
|
+
const obj2 = { c: 3, a: 1, b: 2 };
|
|
28
|
+
const obj3 = { b: 2, c: 3, a: 1 };
|
|
29
|
+
|
|
30
|
+
const result1 = stableStringify(obj1);
|
|
31
|
+
const result2 = stableStringify(obj2);
|
|
32
|
+
const result3 = stableStringify(obj3);
|
|
33
|
+
|
|
34
|
+
expect(result1).toBe(result2);
|
|
35
|
+
expect(result2).toBe(result3);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('nested objects', () => {
|
|
40
|
+
test('sorts keys in nested objects', () => {
|
|
41
|
+
const obj = {
|
|
42
|
+
z: { y: 2, x: 1 },
|
|
43
|
+
a: { c: 4, b: 3 }
|
|
44
|
+
};
|
|
45
|
+
const result = stableStringify(obj);
|
|
46
|
+
expect(result).toBe('{"a":{"b":3,"c":4},"z":{"x":1,"y":2}}');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('handles deeply nested objects', () => {
|
|
50
|
+
const obj = {
|
|
51
|
+
level1: {
|
|
52
|
+
z: 'last',
|
|
53
|
+
a: {
|
|
54
|
+
nested: {
|
|
55
|
+
z: 3,
|
|
56
|
+
a: 1,
|
|
57
|
+
m: 2
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const result = stableStringify(obj);
|
|
63
|
+
const parsed = JSON.parse(result);
|
|
64
|
+
expect(parsed).toEqual({
|
|
65
|
+
level1: {
|
|
66
|
+
a: {
|
|
67
|
+
nested: {
|
|
68
|
+
a: 1,
|
|
69
|
+
m: 2,
|
|
70
|
+
z: 3
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
z: 'last'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('arrays', () => {
|
|
80
|
+
test('preserves array order', () => {
|
|
81
|
+
const arr = [3, 1, 2];
|
|
82
|
+
const result = stableStringify(arr);
|
|
83
|
+
expect(result).toBe('[3,1,2]');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('handles arrays with objects', () => {
|
|
87
|
+
const arr = [
|
|
88
|
+
{ z: 2, a: 1 },
|
|
89
|
+
{ b: 4, a: 3 }
|
|
90
|
+
];
|
|
91
|
+
const result = stableStringify(arr);
|
|
92
|
+
expect(result).toBe('[{"a":1,"z":2},{"a":3,"b":4}]');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('handles nested arrays', () => {
|
|
96
|
+
const arr = [[3, 2, 1], [6, 5, 4]];
|
|
97
|
+
const result = stableStringify(arr);
|
|
98
|
+
expect(result).toBe('[[3,2,1],[6,5,4]]');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('handles mixed arrays', () => {
|
|
102
|
+
const arr = [1, 'string', { z: 2, a: 1 }, [3, 4], null, true];
|
|
103
|
+
const result = stableStringify(arr);
|
|
104
|
+
expect(result).toBe('[1,"string",{"a":1,"z":2},[3,4],null,true]');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('primitive types', () => {
|
|
109
|
+
test('handles strings', () => {
|
|
110
|
+
expect(stableStringify('hello')).toBe('"hello"');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('handles numbers', () => {
|
|
114
|
+
expect(stableStringify(42)).toBe('42');
|
|
115
|
+
expect(stableStringify(0)).toBe('0');
|
|
116
|
+
expect(stableStringify(-42)).toBe('-42');
|
|
117
|
+
expect(stableStringify(3.14)).toBe('3.14');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('handles booleans', () => {
|
|
121
|
+
expect(stableStringify(true)).toBe('true');
|
|
122
|
+
expect(stableStringify(false)).toBe('false');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('handles null', () => {
|
|
126
|
+
expect(stableStringify(null)).toBe('null');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('handles undefined', () => {
|
|
130
|
+
expect(stableStringify(undefined)).toBe('');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('handles undefined in objects', () => {
|
|
134
|
+
const obj = { a: 1, b: undefined, c: 3 };
|
|
135
|
+
const result = stableStringify(obj);
|
|
136
|
+
expect(result).toBe('{"a":1,"c":3}');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('handles undefined in arrays', () => {
|
|
140
|
+
const arr = [1, undefined, 3];
|
|
141
|
+
const result = stableStringify(arr);
|
|
142
|
+
expect(result).toBe('[1,null,3]');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('special number values', () => {
|
|
147
|
+
test('handles Infinity as null', () => {
|
|
148
|
+
expect(stableStringify(Infinity)).toBe('null');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('handles -Infinity as null', () => {
|
|
152
|
+
expect(stableStringify(-Infinity)).toBe('null');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('handles NaN as null', () => {
|
|
156
|
+
expect(stableStringify(NaN)).toBe('null');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('handles special numbers in objects', () => {
|
|
160
|
+
const obj = { a: Infinity, b: NaN, c: -Infinity };
|
|
161
|
+
const result = stableStringify(obj);
|
|
162
|
+
expect(result).toBe('{"a":null,"b":null,"c":null}');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('circular references', () => {
|
|
167
|
+
test('throws TypeError on circular reference', () => {
|
|
168
|
+
const obj: any = { a: 1 };
|
|
169
|
+
obj.self = obj;
|
|
170
|
+
|
|
171
|
+
expect(() => stableStringify(obj)).toThrow(TypeError);
|
|
172
|
+
expect(() => stableStringify(obj)).toThrow('Converting circular structure to JSON');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('throws on nested circular reference', () => {
|
|
176
|
+
const obj: any = { a: { b: {} } };
|
|
177
|
+
obj.a.b.circular = obj;
|
|
178
|
+
|
|
179
|
+
expect(() => stableStringify(obj)).toThrow(TypeError);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('throws on array circular reference', () => {
|
|
183
|
+
const arr: any = [1, 2, 3];
|
|
184
|
+
arr.push(arr);
|
|
185
|
+
|
|
186
|
+
// Note: Array circular references cause stack overflow (RangeError)
|
|
187
|
+
// rather than being caught by the circular reference check
|
|
188
|
+
expect(() => stableStringify(arr)).toThrow(RangeError);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('toJSON method', () => {
|
|
193
|
+
test('calls toJSON method if present', () => {
|
|
194
|
+
const obj = {
|
|
195
|
+
value: 42,
|
|
196
|
+
toJSON() {
|
|
197
|
+
return { transformed: this.value * 2 };
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const result = stableStringify(obj);
|
|
201
|
+
expect(result).toBe('{"transformed":84}');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('handles Date objects via toJSON', () => {
|
|
205
|
+
const date = new Date('2023-01-01T00:00:00.000Z');
|
|
206
|
+
const result = stableStringify(date);
|
|
207
|
+
expect(result).toBe('"2023-01-01T00:00:00.000Z"');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('handles nested objects with toJSON', () => {
|
|
211
|
+
const obj = {
|
|
212
|
+
z: 'last',
|
|
213
|
+
a: {
|
|
214
|
+
toJSON() {
|
|
215
|
+
return { custom: 'value' };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const result = stableStringify(obj);
|
|
220
|
+
expect(result).toBe('{"a":{"custom":"value"},"z":"last"}');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('complex scenarios', () => {
|
|
225
|
+
test('handles empty object', () => {
|
|
226
|
+
expect(stableStringify({})).toBe('{}');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('handles empty array', () => {
|
|
230
|
+
expect(stableStringify([])).toBe('[]');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('handles complex nested structure', () => {
|
|
234
|
+
const complex = {
|
|
235
|
+
users: [
|
|
236
|
+
{ name: 'Alice', age: 30, active: true },
|
|
237
|
+
{ name: 'Bob', age: 25, active: false }
|
|
238
|
+
],
|
|
239
|
+
metadata: {
|
|
240
|
+
version: 1,
|
|
241
|
+
timestamp: 1234567890,
|
|
242
|
+
config: {
|
|
243
|
+
enabled: true,
|
|
244
|
+
options: ['a', 'b', 'c']
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
count: 42
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = stableStringify(complex);
|
|
251
|
+
const parsed = JSON.parse(result);
|
|
252
|
+
|
|
253
|
+
// Verify it's valid JSON
|
|
254
|
+
expect(parsed).toBeDefined();
|
|
255
|
+
|
|
256
|
+
// Verify structure is preserved
|
|
257
|
+
expect(parsed.users).toHaveLength(2);
|
|
258
|
+
expect(parsed.metadata.config.options).toEqual(['a', 'b', 'c']);
|
|
259
|
+
expect(parsed.count).toBe(42);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('handles objects with special characters in keys', () => {
|
|
263
|
+
const obj = {
|
|
264
|
+
'key with spaces': 1,
|
|
265
|
+
'key-with-dashes': 2,
|
|
266
|
+
'key_with_underscores': 3,
|
|
267
|
+
'key.with.dots': 4
|
|
268
|
+
};
|
|
269
|
+
const result = stableStringify(obj);
|
|
270
|
+
const parsed = JSON.parse(result);
|
|
271
|
+
expect(parsed).toEqual(obj);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('handles objects with numeric string keys', () => {
|
|
275
|
+
const obj = { '2': 'two', '1': 'one', '10': 'ten' };
|
|
276
|
+
const result = stableStringify(obj);
|
|
277
|
+
// Keys should be sorted as strings: "1", "10", "2"
|
|
278
|
+
expect(result).toBe('{"1":"one","10":"ten","2":"two"}');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('JSON validity', () => {
|
|
283
|
+
test('output is always valid JSON for valid inputs', () => {
|
|
284
|
+
const testCases = [
|
|
285
|
+
{ a: 1, b: 2 },
|
|
286
|
+
[1, 2, 3],
|
|
287
|
+
'string',
|
|
288
|
+
42,
|
|
289
|
+
true,
|
|
290
|
+
null,
|
|
291
|
+
{ nested: { deeply: { value: 'test' } } },
|
|
292
|
+
[{ a: 1 }, { b: 2 }],
|
|
293
|
+
{ arr: [1, 2, { c: 3 }] }
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
testCases.forEach(testCase => {
|
|
297
|
+
const result = stableStringify(testCase);
|
|
298
|
+
expect(() => JSON.parse(result)).not.toThrow();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('parsed output equals original structure', () => {
|
|
303
|
+
const obj = {
|
|
304
|
+
z: 3,
|
|
305
|
+
a: 1,
|
|
306
|
+
nested: {
|
|
307
|
+
y: 2,
|
|
308
|
+
x: 1
|
|
309
|
+
},
|
|
310
|
+
arr: [3, 2, 1]
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const result = stableStringify(obj);
|
|
314
|
+
const parsed = JSON.parse(result);
|
|
315
|
+
|
|
316
|
+
expect(parsed).toEqual(obj);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('stability comparison', () => {
|
|
321
|
+
test('produces same hash for equivalent objects', () => {
|
|
322
|
+
const obj1 = { b: 2, a: 1, c: { z: 26, y: 25 } };
|
|
323
|
+
const obj2 = { c: { y: 25, z: 26 }, a: 1, b: 2 };
|
|
324
|
+
|
|
325
|
+
expect(stableStringify(obj1)).toBe(stableStringify(obj2));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('produces different output for different objects', () => {
|
|
329
|
+
const obj1 = { a: 1, b: 2 };
|
|
330
|
+
const obj2 = { a: 1, b: 3 };
|
|
331
|
+
|
|
332
|
+
expect(stableStringify(obj1)).not.toBe(stableStringify(obj2));
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable JSON stringify implementation
|
|
3
|
+
* Based on fast-json-stable-stringify by Evgeny Poberezkin
|
|
4
|
+
* https://github.com/epoberezkin/fast-json-stable-stringify
|
|
5
|
+
*
|
|
6
|
+
* This implementation ensures consistent JSON serialization by sorting object keys,
|
|
7
|
+
* which is critical for generating stable hashes from fingerprint data.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Converts data to a stable JSON string with sorted keys
|
|
12
|
+
*
|
|
13
|
+
* @param data - The data to stringify
|
|
14
|
+
* @returns Stable JSON string representation
|
|
15
|
+
* @throws TypeError if circular reference is detected
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const obj = { b: 2, a: 1 };
|
|
20
|
+
* stableStringify(obj); // '{"a":1,"b":2}'
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function stableStringify(data: any): string {
|
|
24
|
+
|
|
25
|
+
const seen: any[] = [];
|
|
26
|
+
|
|
27
|
+
return (function stringify(node: any): string | undefined {
|
|
28
|
+
if (node && node.toJSON && typeof node.toJSON === 'function') {
|
|
29
|
+
node = node.toJSON();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (node === undefined) return;
|
|
33
|
+
if (typeof node === 'number') return isFinite(node) ? '' + node : 'null';
|
|
34
|
+
if (typeof node !== 'object') return JSON.stringify(node);
|
|
35
|
+
|
|
36
|
+
let i: number;
|
|
37
|
+
let out: string;
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(node)) {
|
|
40
|
+
out = '[';
|
|
41
|
+
for (i = 0; i < node.length; i++) {
|
|
42
|
+
if (i) out += ',';
|
|
43
|
+
out += stringify(node[i]) || 'null';
|
|
44
|
+
}
|
|
45
|
+
return out + ']';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (node === null) return 'null';
|
|
49
|
+
|
|
50
|
+
if (seen.indexOf(node) !== -1) {
|
|
51
|
+
throw new TypeError('Converting circular structure to JSON');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const seenIndex = seen.push(node) - 1;
|
|
55
|
+
const keys = Object.keys(node).sort();
|
|
56
|
+
out = '';
|
|
57
|
+
|
|
58
|
+
for (i = 0; i < keys.length; i++) {
|
|
59
|
+
const key = keys[i];
|
|
60
|
+
const value = stringify(node[key]);
|
|
61
|
+
|
|
62
|
+
if (!value) continue;
|
|
63
|
+
if (out) out += ',';
|
|
64
|
+
out += JSON.stringify(key) + ':' + value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
seen.splice(seenIndex, 1);
|
|
68
|
+
return '{' + out + '}';
|
|
69
|
+
})(data) || '';
|
|
70
|
+
}
|