Skip to content

Commit d8ba567

Browse files
authored
Merge pull request #1 from MichaelDoyle/preview_strings
Add a function for truncated previews
2 parents c5cc9ae + 1cd8a67 commit d8ba567

File tree

4 files changed

+263
-48
lines changed

4 files changed

+263
-48
lines changed

projects/demo/src/app/app.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@ export class AppComponent {
2525
...structuredClone(this.baseObj),
2626
nested: {
2727
...structuredClone(this.baseObj),
28+
function: () => {
29+
return 'foo';
30+
},
2831
deeplyNested: {
2932
...structuredClone(this.baseObj),
33+
function: () => {
34+
return 'bar';
35+
},
3036
},
3137
},
3238
function: () => {
33-
return 'foo';
39+
return 'baz';
3440
},
3541
};
3642
}

projects/ngx-json-treeview/src/lib/ngx-json-treeview.component.ts

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CommonModule } from '@angular/common';
22
import { Component, computed, input } from '@angular/core';
3+
import { decycle, previewString } from './util';
34

45
export interface Segment {
56
key: string;
@@ -24,7 +25,7 @@ export class NgxJsonTreeviewComponent {
2425

2526
// computed values
2627
segments = computed<Segment[]>(() => {
27-
const json = this.decycle(this.json());
28+
const json = decycle(this.json());
2829
const arr = [];
2930
if (typeof json === 'object') {
3031
Object.keys(json).forEach((key) => {
@@ -87,14 +88,13 @@ export class NgxJsonTreeviewComponent {
8788
segment.description = 'null';
8889
} else if (Array.isArray(segment.value)) {
8990
segment.type = 'array';
90-
const len = segment.value.length;
91-
segment.description = `Array[${len}] ${JSON.stringify(segment.value)}`;
91+
segment.description = previewString(segment.value);
9292
} else if (segment.value instanceof Date) {
9393
segment.type = 'date';
94-
segment.description = segment.value.toISOString();
94+
segment.description = `"${segment.value.toISOString()}"`;
9595
} else {
9696
segment.type = 'object';
97-
segment.description = `Object ${JSON.stringify(segment.value)}`;
97+
segment.description = previewString(segment.value);
9898
}
9999
break;
100100
default:
@@ -103,46 +103,4 @@ export class NgxJsonTreeviewComponent {
103103

104104
return segment;
105105
}
106-
107-
// https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
108-
private decycle(object: any) {
109-
const objects = new WeakMap();
110-
return (function derez(value, path) {
111-
let old_path;
112-
let nu: any;
113-
114-
if (
115-
typeof value === 'object' &&
116-
value !== null &&
117-
!(value instanceof Boolean) &&
118-
!(value instanceof Date) &&
119-
!(value instanceof Number) &&
120-
!(value instanceof RegExp) &&
121-
!(value instanceof String)
122-
) {
123-
old_path = objects.get(value);
124-
if (old_path !== undefined) {
125-
return { $ref: old_path };
126-
}
127-
objects.set(value, path);
128-
129-
if (Array.isArray(value)) {
130-
nu = [];
131-
value.forEach(function (element, i) {
132-
nu[i] = derez(element, path + '[' + i + ']');
133-
});
134-
} else {
135-
nu = {};
136-
Object.keys(value).forEach(function (name) {
137-
nu[name] = derez(
138-
value[name],
139-
path + '[' + JSON.stringify(name) + ']'
140-
);
141-
});
142-
}
143-
return nu;
144-
}
145-
return value;
146-
})(object, '$');
147-
}
148106
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { decycle, previewString } from '../util';
2+
3+
describe('Util', () => {
4+
describe('previewString', () => {
5+
it('should handle null values', () => {
6+
expect(previewString(null)).toEqual('null');
7+
});
8+
9+
it('should handle undefined values', () => {
10+
expect(previewString(undefined)).toEqual('undefined');
11+
});
12+
13+
it('should handle string values', () => {
14+
expect(previewString('hello')).toEqual('"hello"');
15+
});
16+
17+
it('should handle boolean values', () => {
18+
expect(previewString(true)).toEqual('true');
19+
});
20+
21+
it('should handle number (integer) values', () => {
22+
expect(previewString(42)).toEqual('42');
23+
});
24+
25+
it('should handle number (decimal) values', () => {
26+
expect(previewString(5.6)).toEqual('5.6');
27+
});
28+
29+
it('should handle date objects', () => {
30+
const date = new Date();
31+
expect(previewString(date)).toEqual(`"${date.toISOString()}"`);
32+
});
33+
34+
it('should handle arrays', () => {
35+
expect(previewString([1, 2, 3])).toEqual('Array[3] [1,2,3]');
36+
});
37+
38+
it('should handle regular objects', () => {
39+
const obj = { a: 1, b: 'hello' };
40+
expect(previewString(obj)).toEqual('Object {"a":1,"b":"hello"}');
41+
});
42+
43+
it('should handle function values', () => {
44+
expect(previewString(() => {})).toEqual('Function');
45+
});
46+
47+
it('should truncate when limit is exceeded', () => {
48+
const obj = { a: 1, b: 'hello'.repeat(50) };
49+
expect(previewString(obj, 20)).toEqual('Object {"a":1,"b":"h…');
50+
});
51+
52+
it('should truncate string values in objects when stringsLimit is exceeded', () => {
53+
const obj = { a: 1, b: 'hello'.repeat(50) };
54+
expect(previewString(obj, 200, 10)).toEqual(
55+
'Object {"a":1,"b":"hellohello…"}'
56+
);
57+
});
58+
59+
it('should truncate string values in arrays when stringsLimit is exceeded', () => {
60+
expect(previewString(['longstring'.repeat(10)], 200, 10)).toEqual(
61+
'Array[1] ["longstring…"]'
62+
);
63+
});
64+
65+
describe('parity with JSON.stringify()', () => {
66+
it('should handle null values', () => {
67+
expect(previewString(null)).toEqual(JSON.stringify(null));
68+
});
69+
70+
it('should handle undefined values', () => {
71+
expect(previewString(undefined)).toEqual(
72+
JSON.stringify(undefined) + ''
73+
);
74+
});
75+
76+
it('should handle string values', () => {
77+
expect(previewString('hello')).toEqual(JSON.stringify('hello'));
78+
});
79+
80+
it('should handle number (integer) values', () => {
81+
expect(previewString(42)).toEqual(JSON.stringify(42));
82+
});
83+
84+
it('should handle number (decimal) values', () => {
85+
expect(previewString(5.6)).toEqual(JSON.stringify(5.6));
86+
});
87+
88+
it('should handle boolean values', () => {
89+
expect(previewString(true)).toEqual(JSON.stringify(true));
90+
});
91+
92+
it('should handle date objects', () => {
93+
const date = new Date();
94+
expect(previewString(date)).toEqual(JSON.stringify(date));
95+
});
96+
97+
it('should handle regular objects', () => {
98+
const obj = { a: 1, b: 'hello' };
99+
expect(previewString(obj)).toEqual('Object ' + JSON.stringify(obj));
100+
});
101+
102+
it('should handle arrays', () => {
103+
const arr = [1, 2, 'hello'];
104+
expect(previewString(arr)).toEqual('Array[3] ' + JSON.stringify(arr));
105+
});
106+
107+
// functions have intentional differences
108+
});
109+
});
110+
111+
describe('decycle', () => {
112+
it('should replace circular references with $ref properties', () => {
113+
const obj = { a: 1 } as any;
114+
obj.b = obj;
115+
expect(decycle(obj)).toEqual({ a: 1, b: { $ref: '$' } });
116+
});
117+
118+
it('should handle arrays with circular references', () => {
119+
const arr: any[] = [1];
120+
arr[1] = arr;
121+
expect(decycle(arr)).toEqual([1, { $ref: '$' }]);
122+
});
123+
124+
it('should handle nested objects with circular references', () => {
125+
const obj1 = { a: 1 } as any;
126+
const obj2 = { b: 2, c: obj1 } as any;
127+
obj1.d = obj2;
128+
expect(decycle(obj1)).toEqual({ a: 1, d: { b: 2, c: { $ref: '$' } } });
129+
});
130+
});
131+
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Generates a preview string representation of an object.
3+
*
4+
* @param obj The object to preview.
5+
* @param limit The maximum length of the preview string. Defaults to 200.
6+
* @param stringsLimit The maximum length of a string to display before
7+
* truncating. Defaults to 10.
8+
* @returns A preview string representation of the object.
9+
*/
10+
export function previewString(obj: any, limit = 200, stringsLimit = 10) {
11+
let result = '';
12+
13+
if (obj === null) {
14+
result += 'null';
15+
} else if (obj === undefined) {
16+
result += 'undefined';
17+
} else if (typeof obj === 'string') {
18+
if (obj.length > stringsLimit) {
19+
result += `"${obj.substring(0, stringsLimit)}…"`;
20+
} else {
21+
result += `"${obj}"`;
22+
}
23+
} else if (typeof obj === 'boolean') {
24+
result += `${obj ? 'true' : 'false'}`;
25+
} else if (typeof obj === 'number') {
26+
result += `${obj}`;
27+
} else if (typeof obj === 'object') {
28+
if (obj instanceof Date) {
29+
result += `"${obj.toISOString()}"`;
30+
} else if (Array.isArray(obj)) {
31+
result += `Array[${obj.length}] [`;
32+
for (const key in obj) {
33+
if (result.length >= limit) {
34+
break;
35+
}
36+
result += previewString(obj[key], limit - result.length);
37+
result += ',';
38+
}
39+
if (result.endsWith(',')) {
40+
result = result.slice(0, -1);
41+
}
42+
result += ']';
43+
} else {
44+
result += 'Object {';
45+
for (const key in obj) {
46+
if (result.length >= limit) {
47+
break;
48+
}
49+
if (obj[key] !== undefined) {
50+
result += `"${key}":`;
51+
result += previewString(obj[key], limit - result.length);
52+
result += ',';
53+
}
54+
}
55+
if (result.endsWith(',')) {
56+
result = result.slice(0, -1);
57+
}
58+
result += '}';
59+
}
60+
} else if (typeof obj === 'function') {
61+
result += 'Function';
62+
}
63+
64+
if (result.length >= limit) {
65+
return result.substring(0, limit) + '…';
66+
}
67+
68+
return result;
69+
}
70+
71+
/**
72+
* Decycles a JavaScript object by replacing circular references with `$ref`
73+
* properties. This is useful for serializing objects that contain circular
74+
* references, preventing infinite loops.
75+
*
76+
* Original: https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
77+
*
78+
* @param object The object to decycle.
79+
* @returns A decycled version of the object.
80+
*/
81+
export function decycle(object: any): any {
82+
const objects = new WeakMap();
83+
return (function derez(value, path) {
84+
let old_path;
85+
let nu: any;
86+
87+
if (
88+
typeof value === 'object' &&
89+
value !== null &&
90+
!(value instanceof Boolean) &&
91+
!(value instanceof Date) &&
92+
!(value instanceof Number) &&
93+
!(value instanceof RegExp) &&
94+
!(value instanceof String)
95+
) {
96+
old_path = objects.get(value);
97+
if (old_path !== undefined) {
98+
return { $ref: old_path };
99+
}
100+
objects.set(value, path);
101+
102+
if (Array.isArray(value)) {
103+
nu = [];
104+
value.forEach(function (element, i) {
105+
nu[i] = derez(element, path + '[' + i + ']');
106+
});
107+
} else {
108+
nu = {};
109+
Object.keys(value).forEach(function (name) {
110+
nu[name] = derez(
111+
value[name],
112+
path + '[' + JSON.stringify(name) + ']'
113+
);
114+
});
115+
}
116+
return nu;
117+
}
118+
return value;
119+
})(object, '$');
120+
}

0 commit comments

Comments
 (0)