1 | const {
|
---|
2 | normalizeReplacer,
|
---|
3 | normalizeSpace,
|
---|
4 | replaceValue,
|
---|
5 | getTypeNative,
|
---|
6 | getTypeAsync,
|
---|
7 | isLeadingSurrogate,
|
---|
8 | isTrailingSurrogate,
|
---|
9 | escapableCharCodeSubstitution,
|
---|
10 | type: {
|
---|
11 | PRIMITIVE,
|
---|
12 | OBJECT,
|
---|
13 | ARRAY,
|
---|
14 | PROMISE,
|
---|
15 | STRING_STREAM,
|
---|
16 | OBJECT_STREAM
|
---|
17 | }
|
---|
18 | } = require('./utils');
|
---|
19 | const charLength2048 = Array.from({ length: 2048 }).map((_, code) => {
|
---|
20 | if (escapableCharCodeSubstitution.hasOwnProperty(code)) {
|
---|
21 | return 2; // \X
|
---|
22 | }
|
---|
23 |
|
---|
24 | if (code < 0x20) {
|
---|
25 | return 6; // \uXXXX
|
---|
26 | }
|
---|
27 |
|
---|
28 | return code < 128 ? 1 : 2; // UTF8 bytes
|
---|
29 | });
|
---|
30 |
|
---|
31 | function stringLength(str) {
|
---|
32 | let len = 0;
|
---|
33 | let prevLeadingSurrogate = false;
|
---|
34 |
|
---|
35 | for (let i = 0; i < str.length; i++) {
|
---|
36 | const code = str.charCodeAt(i);
|
---|
37 |
|
---|
38 | if (code < 2048) {
|
---|
39 | len += charLength2048[code];
|
---|
40 | } else if (isLeadingSurrogate(code)) {
|
---|
41 | len += 6; // \uXXXX since no pair with trailing surrogate yet
|
---|
42 | prevLeadingSurrogate = true;
|
---|
43 | continue;
|
---|
44 | } else if (isTrailingSurrogate(code)) {
|
---|
45 | len = prevLeadingSurrogate
|
---|
46 | ? len - 2 // surrogate pair (4 bytes), since we calculate prev leading surrogate as 6 bytes, substruct 2 bytes
|
---|
47 | : len + 6; // \uXXXX
|
---|
48 | } else {
|
---|
49 | len += 3; // code >= 2048 is 3 bytes length for UTF8
|
---|
50 | }
|
---|
51 |
|
---|
52 | prevLeadingSurrogate = false;
|
---|
53 | }
|
---|
54 |
|
---|
55 | return len + 2; // +2 for quotes
|
---|
56 | }
|
---|
57 |
|
---|
58 | function primitiveLength(value) {
|
---|
59 | switch (typeof value) {
|
---|
60 | case 'string':
|
---|
61 | return stringLength(value);
|
---|
62 |
|
---|
63 | case 'number':
|
---|
64 | return Number.isFinite(value) ? String(value).length : 4 /* null */;
|
---|
65 |
|
---|
66 | case 'boolean':
|
---|
67 | return value ? 4 /* true */ : 5 /* false */;
|
---|
68 |
|
---|
69 | case 'undefined':
|
---|
70 | case 'object':
|
---|
71 | return 4; /* null */
|
---|
72 |
|
---|
73 | default:
|
---|
74 | return 0;
|
---|
75 | }
|
---|
76 | }
|
---|
77 |
|
---|
78 | function spaceLength(space) {
|
---|
79 | space = normalizeSpace(space);
|
---|
80 | return typeof space === 'string' ? space.length : 0;
|
---|
81 | }
|
---|
82 |
|
---|
83 | module.exports = function jsonStringifyInfo(value, replacer, space, options) {
|
---|
84 | function walk(holder, key, value) {
|
---|
85 | if (stop) {
|
---|
86 | return;
|
---|
87 | }
|
---|
88 |
|
---|
89 | value = replaceValue(holder, key, value, replacer);
|
---|
90 |
|
---|
91 | let type = getType(value);
|
---|
92 |
|
---|
93 | // check for circular structure
|
---|
94 | if (type !== PRIMITIVE && stack.has(value)) {
|
---|
95 | circular.add(value);
|
---|
96 | length += 4; // treat as null
|
---|
97 |
|
---|
98 | if (!options.continueOnCircular) {
|
---|
99 | stop = true;
|
---|
100 | }
|
---|
101 |
|
---|
102 | return;
|
---|
103 | }
|
---|
104 |
|
---|
105 | switch (type) {
|
---|
106 | case PRIMITIVE:
|
---|
107 | if (value !== undefined || Array.isArray(holder)) {
|
---|
108 | length += primitiveLength(value);
|
---|
109 | } else if (holder === root) {
|
---|
110 | length += 9; // FIXME: that's the length of undefined, should we normalize behaviour to convert it to null?
|
---|
111 | }
|
---|
112 | break;
|
---|
113 |
|
---|
114 | case OBJECT: {
|
---|
115 | if (visited.has(value)) {
|
---|
116 | duplicate.add(value);
|
---|
117 | length += visited.get(value);
|
---|
118 | break;
|
---|
119 | }
|
---|
120 |
|
---|
121 | const valueLength = length;
|
---|
122 | let entries = 0;
|
---|
123 |
|
---|
124 | length += 2; // {}
|
---|
125 |
|
---|
126 | stack.add(value);
|
---|
127 |
|
---|
128 | for (const key in value) {
|
---|
129 | if (hasOwnProperty.call(value, key) && (allowlist === null || allowlist.has(key))) {
|
---|
130 | const prevLength = length;
|
---|
131 | walk(value, key, value[key]);
|
---|
132 |
|
---|
133 | if (prevLength !== length) {
|
---|
134 | // value is printed
|
---|
135 | length += stringLength(key) + 1; // "key":
|
---|
136 | entries++;
|
---|
137 | }
|
---|
138 | }
|
---|
139 | }
|
---|
140 |
|
---|
141 | if (entries > 1) {
|
---|
142 | length += entries - 1; // commas
|
---|
143 | }
|
---|
144 |
|
---|
145 | stack.delete(value);
|
---|
146 |
|
---|
147 | if (space > 0 && entries > 0) {
|
---|
148 | length += (1 + (stack.size + 1) * space + 1) * entries; // for each key-value: \n{space}
|
---|
149 | length += 1 + stack.size * space; // for }
|
---|
150 | }
|
---|
151 |
|
---|
152 | visited.set(value, length - valueLength);
|
---|
153 |
|
---|
154 | break;
|
---|
155 | }
|
---|
156 |
|
---|
157 | case ARRAY: {
|
---|
158 | if (visited.has(value)) {
|
---|
159 | duplicate.add(value);
|
---|
160 | length += visited.get(value);
|
---|
161 | break;
|
---|
162 | }
|
---|
163 |
|
---|
164 | const valueLength = length;
|
---|
165 |
|
---|
166 | length += 2; // []
|
---|
167 |
|
---|
168 | stack.add(value);
|
---|
169 |
|
---|
170 | for (let i = 0; i < value.length; i++) {
|
---|
171 | walk(value, i, value[i]);
|
---|
172 | }
|
---|
173 |
|
---|
174 | if (value.length > 1) {
|
---|
175 | length += value.length - 1; // commas
|
---|
176 | }
|
---|
177 |
|
---|
178 | stack.delete(value);
|
---|
179 |
|
---|
180 | if (space > 0 && value.length > 0) {
|
---|
181 | length += (1 + (stack.size + 1) * space) * value.length; // for each element: \n{space}
|
---|
182 | length += 1 + stack.size * space; // for ]
|
---|
183 | }
|
---|
184 |
|
---|
185 | visited.set(value, length - valueLength);
|
---|
186 |
|
---|
187 | break;
|
---|
188 | }
|
---|
189 |
|
---|
190 | case PROMISE:
|
---|
191 | case STRING_STREAM:
|
---|
192 | async.add(value);
|
---|
193 | break;
|
---|
194 |
|
---|
195 | case OBJECT_STREAM:
|
---|
196 | length += 2; // []
|
---|
197 | async.add(value);
|
---|
198 | break;
|
---|
199 | }
|
---|
200 | }
|
---|
201 |
|
---|
202 | let allowlist = null;
|
---|
203 | replacer = normalizeReplacer(replacer);
|
---|
204 |
|
---|
205 | if (Array.isArray(replacer)) {
|
---|
206 | allowlist = new Set(replacer);
|
---|
207 | replacer = null;
|
---|
208 | }
|
---|
209 |
|
---|
210 | space = spaceLength(space);
|
---|
211 | options = options || {};
|
---|
212 |
|
---|
213 | const visited = new Map();
|
---|
214 | const stack = new Set();
|
---|
215 | const duplicate = new Set();
|
---|
216 | const circular = new Set();
|
---|
217 | const async = new Set();
|
---|
218 | const getType = options.async ? getTypeAsync : getTypeNative;
|
---|
219 | const root = { '': value };
|
---|
220 | let stop = false;
|
---|
221 | let length = 0;
|
---|
222 |
|
---|
223 | walk(root, '', value);
|
---|
224 |
|
---|
225 | return {
|
---|
226 | minLength: isNaN(length) ? Infinity : length,
|
---|
227 | circular: [...circular],
|
---|
228 | duplicate: [...duplicate],
|
---|
229 | async: [...async]
|
---|
230 | };
|
---|
231 | };
|
---|