slimjson 1.0.3 → 1.0.4
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/.claude/settings.local.json +8 -0
- package/README.md +77 -26
- package/README_EN.md +74 -33
- package/compress-test.js +51 -25
- package/compress.js +423 -410
- package/data/searchGroup.json +96365 -0
- package/package.json +1 -1
- package/test.js +473 -384
package/compress.js
CHANGED
|
@@ -1,410 +1,423 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 对象数组压缩转换工具
|
|
3
|
-
*
|
|
4
|
-
* 支持:不同对象有不同 key(后端未返回 null 字段时)
|
|
5
|
-
* - buildKeys 扫描所有对象取 key 并集
|
|
6
|
-
* - 嵌套对象/数组递归时同样合并子结构
|
|
7
|
-
* - 缺失的 key 在 rows 中填充 null
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/* ============================================================
|
|
11
|
-
内部辅助:确定一个值的类型分类
|
|
12
|
-
============================================================ */
|
|
13
|
-
function getValueKind(v) {
|
|
14
|
-
if (v === null || v === undefined) return 'null';
|
|
15
|
-
if (Array.isArray(v)) {
|
|
16
|
-
if (v.length > 0 && typeof v[0] === 'object' && v[0] !== null && !Array.isArray(v[0])) {
|
|
17
|
-
return 'object-array'; // [{...}, {...}]
|
|
18
|
-
}
|
|
19
|
-
return 'primitive-array'; // [1, 2, 3] 或 []
|
|
20
|
-
}
|
|
21
|
-
if (typeof v === 'object') return 'object'; // {...}
|
|
22
|
-
return 'primitive';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/* ============================================================
|
|
26
|
-
buildKeys — 扫描所有对象,递归构建完整 key 结构
|
|
27
|
-
============================================================ */
|
|
28
|
-
function buildKeys(sources) {
|
|
29
|
-
// ---- 1. 取所有对象 key 的并集,按首次出现顺序排列 ----
|
|
30
|
-
const orderedKeys = [];
|
|
31
|
-
const seen = new Set();
|
|
32
|
-
for (const obj of sources) {
|
|
33
|
-
if (obj !== null && typeof obj === 'object') {
|
|
34
|
-
for (const k of Object.keys(obj)) {
|
|
35
|
-
if (!seen.has(k)) {
|
|
36
|
-
seen.add(k);
|
|
37
|
-
orderedKeys.push(k);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const keys = [];
|
|
44
|
-
for (const keyName of orderedKeys) {
|
|
45
|
-
// ---- 2. 找第一个非 null/undefined 的值来推断类型 ----
|
|
46
|
-
let repValue = undefined;
|
|
47
|
-
for (const obj of sources) {
|
|
48
|
-
if (obj !== null && typeof obj === 'object') {
|
|
49
|
-
const v = obj[keyName];
|
|
50
|
-
if (v !== undefined && v !== null) { repValue = v; break; }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const kind = getValueKind(repValue);
|
|
55
|
-
|
|
56
|
-
if (kind === 'object') {
|
|
57
|
-
// ---- 收集所有非 null 的嵌套对象,合并子 key ----
|
|
58
|
-
const allNestedObjs = [];
|
|
59
|
-
for (const obj of sources) {
|
|
60
|
-
if (obj !== null && typeof obj === 'object') {
|
|
61
|
-
const v = obj[keyName];
|
|
62
|
-
if (v !== undefined && v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
63
|
-
allNestedObjs.push(v);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
keys.push({ [keyName]: buildKeys(allNestedObjs) });
|
|
68
|
-
|
|
69
|
-
} else if (kind === 'object-array') {
|
|
70
|
-
// ---- 收集所有数组中的所有对象,合并子 key ----
|
|
71
|
-
const allItems = [];
|
|
72
|
-
for (const obj of sources) {
|
|
73
|
-
if (obj !== null && typeof obj === 'object') {
|
|
74
|
-
const arr = obj[keyName];
|
|
75
|
-
if (Array.isArray(arr)) {
|
|
76
|
-
for (const item of arr) {
|
|
77
|
-
if (item !== null && typeof item === 'object') {
|
|
78
|
-
allItems.push(item);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
keys.push({ [keyName]: buildKeys(allItems) });
|
|
85
|
-
|
|
86
|
-
} else {
|
|
87
|
-
// primitive / primitive-array / null(全当字符串 key 处理)
|
|
88
|
-
keys.push(keyName);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return keys;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* ============================================================
|
|
96
|
-
buildRow — 按 keys 结构将单个源对象转为 row
|
|
97
|
-
============================================================ */
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
*
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
function
|
|
387
|
-
const start = pos;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 对象数组压缩转换工具
|
|
3
|
+
*
|
|
4
|
+
* 支持:不同对象有不同 key(后端未返回 null 字段时)
|
|
5
|
+
* - buildKeys 扫描所有对象取 key 并集
|
|
6
|
+
* - 嵌套对象/数组递归时同样合并子结构
|
|
7
|
+
* - 缺失的 key 在 rows 中填充 null
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* ============================================================
|
|
11
|
+
内部辅助:确定一个值的类型分类
|
|
12
|
+
============================================================ */
|
|
13
|
+
function getValueKind(v) {
|
|
14
|
+
if (v === null || v === undefined) return 'null';
|
|
15
|
+
if (Array.isArray(v)) {
|
|
16
|
+
if (v.length > 0 && typeof v[0] === 'object' && v[0] !== null && !Array.isArray(v[0])) {
|
|
17
|
+
return 'object-array'; // [{...}, {...}]
|
|
18
|
+
}
|
|
19
|
+
return 'primitive-array'; // [1, 2, 3] 或 []
|
|
20
|
+
}
|
|
21
|
+
if (typeof v === 'object') return 'object'; // {...}
|
|
22
|
+
return 'primitive';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ============================================================
|
|
26
|
+
buildKeys — 扫描所有对象,递归构建完整 key 结构
|
|
27
|
+
============================================================ */
|
|
28
|
+
function buildKeys(sources) {
|
|
29
|
+
// ---- 1. 取所有对象 key 的并集,按首次出现顺序排列 ----
|
|
30
|
+
const orderedKeys = [];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
for (const obj of sources) {
|
|
33
|
+
if (obj !== null && typeof obj === 'object') {
|
|
34
|
+
for (const k of Object.keys(obj)) {
|
|
35
|
+
if (!seen.has(k)) {
|
|
36
|
+
seen.add(k);
|
|
37
|
+
orderedKeys.push(k);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const keys = [];
|
|
44
|
+
for (const keyName of orderedKeys) {
|
|
45
|
+
// ---- 2. 找第一个非 null/undefined 的值来推断类型 ----
|
|
46
|
+
let repValue = undefined;
|
|
47
|
+
for (const obj of sources) {
|
|
48
|
+
if (obj !== null && typeof obj === 'object') {
|
|
49
|
+
const v = obj[keyName];
|
|
50
|
+
if (v !== undefined && v !== null) { repValue = v; break; }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const kind = getValueKind(repValue);
|
|
55
|
+
|
|
56
|
+
if (kind === 'object') {
|
|
57
|
+
// ---- 收集所有非 null 的嵌套对象,合并子 key ----
|
|
58
|
+
const allNestedObjs = [];
|
|
59
|
+
for (const obj of sources) {
|
|
60
|
+
if (obj !== null && typeof obj === 'object') {
|
|
61
|
+
const v = obj[keyName];
|
|
62
|
+
if (v !== undefined && v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
63
|
+
allNestedObjs.push(v);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
keys.push({ [keyName]: buildKeys(allNestedObjs) });
|
|
68
|
+
|
|
69
|
+
} else if (kind === 'object-array') {
|
|
70
|
+
// ---- 收集所有数组中的所有对象,合并子 key ----
|
|
71
|
+
const allItems = [];
|
|
72
|
+
for (const obj of sources) {
|
|
73
|
+
if (obj !== null && typeof obj === 'object') {
|
|
74
|
+
const arr = obj[keyName];
|
|
75
|
+
if (Array.isArray(arr)) {
|
|
76
|
+
for (const item of arr) {
|
|
77
|
+
if (item !== null && typeof item === 'object') {
|
|
78
|
+
allItems.push(item);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
keys.push({ [keyName]: buildKeys(allItems) });
|
|
85
|
+
|
|
86
|
+
} else {
|
|
87
|
+
// primitive / primitive-array / null(全当字符串 key 处理)
|
|
88
|
+
keys.push(keyName);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return keys;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ============================================================
|
|
96
|
+
buildRow — 按 keys 结构将单个源对象转为 row
|
|
97
|
+
============================================================ */
|
|
98
|
+
function trimTrailingNulls(arr) {
|
|
99
|
+
let end = arr.length;
|
|
100
|
+
while (end > 0 && arr[end - 1] === null) end--;
|
|
101
|
+
if (end === arr.length) return arr;
|
|
102
|
+
return arr.slice(0, end);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildRow(obj, keys, trim) {
|
|
106
|
+
const row = [];
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
if (typeof key === 'string') {
|
|
109
|
+
// 普通字段:缺失则 push null;显式的 undefined 也转 null
|
|
110
|
+
const v = (obj != null && typeof obj === 'object') ? obj[key] : undefined;
|
|
111
|
+
row.push(v === undefined ? null : v);
|
|
112
|
+
} else {
|
|
113
|
+
// 嵌套结构
|
|
114
|
+
const [[keyName, childKeys]] = Object.entries(key);
|
|
115
|
+
const val = obj != null && typeof obj === 'object' ? obj[keyName] : undefined;
|
|
116
|
+
|
|
117
|
+
if (val === undefined || val === null) {
|
|
118
|
+
row.push(null); // 字段缺失或为 null
|
|
119
|
+
|
|
120
|
+
} else if (Array.isArray(val)) {
|
|
121
|
+
// 对象数组
|
|
122
|
+
const arr = val.map(item => buildRow(item, childKeys, trim));
|
|
123
|
+
row.push(trim ? arr.map(r => trimTrailingNulls(r)) : arr);
|
|
124
|
+
|
|
125
|
+
} else {
|
|
126
|
+
// 单个嵌套对象
|
|
127
|
+
const sub = buildRow(val, childKeys, trim);
|
|
128
|
+
row.push(trim ? trimTrailingNulls(sub) : sub);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (trim) {
|
|
133
|
+
return trimTrailingNulls(row);
|
|
134
|
+
}
|
|
135
|
+
return row;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ============================================================
|
|
139
|
+
compress / decompress
|
|
140
|
+
============================================================ */
|
|
141
|
+
function compress(source, opts) {
|
|
142
|
+
if (Object.prototype.toString.call(source) === '[object Object]') {
|
|
143
|
+
source = [source]
|
|
144
|
+
} else if (!Array.isArray(source) || source.length === 0) {
|
|
145
|
+
return source; // 不满足条件直接返回原值
|
|
146
|
+
}
|
|
147
|
+
const trim = opts && opts.trimTrailingNulls;
|
|
148
|
+
const keys = buildKeys(source);
|
|
149
|
+
const rows = source.map(obj => buildRow(obj, keys, trim));
|
|
150
|
+
return { keys, rows };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function decompress(compressed) {
|
|
154
|
+
function buildFromRow(row, keys) {
|
|
155
|
+
const obj = {};
|
|
156
|
+
for (let i = 0; i < keys.length; i++) {
|
|
157
|
+
const key = keys[i];
|
|
158
|
+
const val = row[i];
|
|
159
|
+
|
|
160
|
+
if (typeof key === 'string') {
|
|
161
|
+
obj[key] = val === undefined ? null : val;
|
|
162
|
+
|
|
163
|
+
} else {
|
|
164
|
+
const [[keyName, childKeys]] = Object.entries(key);
|
|
165
|
+
|
|
166
|
+
if (val === null || val === undefined) {
|
|
167
|
+
// 字段缺失或为 null → 写入 null
|
|
168
|
+
obj[keyName] = null;
|
|
169
|
+
|
|
170
|
+
} else if (Array.isArray(val) && val.length > 0 && Array.isArray(val[0])) {
|
|
171
|
+
// 对象数组
|
|
172
|
+
obj[keyName] = val.map(r => buildFromRow(r, childKeys));
|
|
173
|
+
|
|
174
|
+
} else {
|
|
175
|
+
// 单个嵌套对象(或被 trim 的子 row)
|
|
176
|
+
obj[keyName] = buildFromRow(val, childKeys);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return obj;
|
|
181
|
+
}
|
|
182
|
+
return compressed.rows.map(row => buildFromRow(row, compressed.keys));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ============================================================
|
|
186
|
+
判断字符串是否可安全省略引号
|
|
187
|
+
============================================================ */
|
|
188
|
+
function isSafeBareString(s) {
|
|
189
|
+
if (s === '') return false;
|
|
190
|
+
if (s === 'null' || s === 'true' || s === 'false') return false;
|
|
191
|
+
// 看起来像数字的不省略
|
|
192
|
+
if (/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/.test(s)) return false;
|
|
193
|
+
// 以数字或减号开头的也不省略(避免 parseValue 的数字分支误吞)
|
|
194
|
+
if (/^[-\d]/.test(s)) return false;
|
|
195
|
+
// 含分隔符、空白的不省略
|
|
196
|
+
if (/[\s\[\]{},:"]/.test(s)) return false;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* ============================================================
|
|
201
|
+
stringify / parse — 省略 null 的文本化与还原
|
|
202
|
+
============================================================ */
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 将 compress 结果文本化
|
|
206
|
+
* - 数组中的 null 被省略为逗号空槽:[null, 1, null] → [,1,]
|
|
207
|
+
* - 安全的字符串省略引号:"hello" → hello
|
|
208
|
+
*
|
|
209
|
+
* @param {*} compressed 待序列化的值
|
|
210
|
+
*/
|
|
211
|
+
function stringify(compressed) {
|
|
212
|
+
return serializeValue(compressed);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 序列化单个值 */
|
|
216
|
+
function serializeValue(v) {
|
|
217
|
+
if (v === null || v === undefined) return 'null';
|
|
218
|
+
if (typeof v === 'string') {
|
|
219
|
+
if (isSafeBareString(v)) return v;
|
|
220
|
+
return JSON.stringify(v);
|
|
221
|
+
}
|
|
222
|
+
if (typeof v === 'number') {
|
|
223
|
+
if (Number.isFinite(v)) return String(v);
|
|
224
|
+
return 'null'; // NaN, Infinity → null
|
|
225
|
+
}
|
|
226
|
+
if (typeof v === 'boolean') return String(v);
|
|
227
|
+
if (Array.isArray(v)) return serializeArray(v);
|
|
228
|
+
if (typeof v === 'object') return serializeObject(v);
|
|
229
|
+
return 'null';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 序列化数组:null 变为空槽(保留逗号) */
|
|
233
|
+
function serializeArray(arr) {
|
|
234
|
+
if (arr.length === 0) return '[]';
|
|
235
|
+
const parts = arr.map(item => {
|
|
236
|
+
if (item === null || item === undefined) return '';
|
|
237
|
+
return serializeValue(item);
|
|
238
|
+
});
|
|
239
|
+
const inner = parts.join(',');
|
|
240
|
+
const result = '[' + inner + ']';
|
|
241
|
+
// 特殊边界:单 null 元素无法用逗号表示
|
|
242
|
+
// [,] 在本格式中代表 2 个 null,所以 [null] 必须保留 null 文字
|
|
243
|
+
if (result === '[]') return '[null]';
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** 序列化对象(用于 keys 中的嵌套结构) */
|
|
248
|
+
function serializeObject(obj) {
|
|
249
|
+
const pairs = Object.entries(obj).map(([k, v]) => {
|
|
250
|
+
const keyStr = isSafeBareString(k) ? k : JSON.stringify(k);
|
|
251
|
+
return keyStr + ':' + serializeValue(v);
|
|
252
|
+
});
|
|
253
|
+
return '{' + pairs.join(',') + '}';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 解析 stringify 产生的文本,恢复省略的 null
|
|
258
|
+
*/
|
|
259
|
+
function parse(text) {
|
|
260
|
+
let pos = 0;
|
|
261
|
+
|
|
262
|
+
function error(msg) {
|
|
263
|
+
throw new Error(`Parse error at ${pos}: ${msg} — near "${text.slice(Math.max(0, pos - 5), pos + 10)}"`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function skipWs() {
|
|
267
|
+
while (pos < text.length && /\s/.test(text[pos])) pos++;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 判断字符是否为值边界(分隔符或 EOF) */
|
|
271
|
+
function isBoundaryChar(c) {
|
|
272
|
+
return c === undefined || /[\s\[\]{},:]/.test(c);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseValue() {
|
|
276
|
+
skipWs();
|
|
277
|
+
if (pos >= text.length) error('Unexpected end');
|
|
278
|
+
const ch = text[pos];
|
|
279
|
+
if (ch === '"') return parseString();
|
|
280
|
+
if (ch === '{') return parseObject();
|
|
281
|
+
if (ch === '[') return parseArray();
|
|
282
|
+
// 关键字:精确匹配且后接边界符或 EOF,否则归为裸字符串
|
|
283
|
+
if (text.startsWith('null', pos) && isBoundaryChar(text[pos + 4])) { pos += 4; return null; }
|
|
284
|
+
if (text.startsWith('true', pos) && isBoundaryChar(text[pos + 4])) { pos += 4; return true; }
|
|
285
|
+
if (text.startsWith('false', pos) && isBoundaryChar(text[pos + 5])) { pos += 5; return false; }
|
|
286
|
+
if (ch === '-' || (ch >= '0' && ch <= '9')) return parseNumber();
|
|
287
|
+
// 裸字符串(无引号标识符)
|
|
288
|
+
return parseBareString();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseString() {
|
|
292
|
+
let result = '';
|
|
293
|
+
pos++; // skip opening "
|
|
294
|
+
while (pos < text.length) {
|
|
295
|
+
const ch = text[pos];
|
|
296
|
+
if (ch === '"') { pos++; return result; }
|
|
297
|
+
if (ch === '\\') {
|
|
298
|
+
pos++;
|
|
299
|
+
const esc = text[pos];
|
|
300
|
+
switch (esc) {
|
|
301
|
+
case '"': result += '"'; break;
|
|
302
|
+
case '\\': result += '\\'; break;
|
|
303
|
+
case '/': result += '/'; break;
|
|
304
|
+
case 'b': result += '\b'; break;
|
|
305
|
+
case 'f': result += '\f'; break;
|
|
306
|
+
case 'n': result += '\n'; break;
|
|
307
|
+
case 'r': result += '\r'; break;
|
|
308
|
+
case 't': result += '\t'; break;
|
|
309
|
+
case 'u': {
|
|
310
|
+
const hex = text.substring(pos + 1, pos + 5);
|
|
311
|
+
result += String.fromCharCode(parseInt(hex, 16));
|
|
312
|
+
pos += 4;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
default: result += esc;
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
result += ch;
|
|
319
|
+
}
|
|
320
|
+
pos++;
|
|
321
|
+
}
|
|
322
|
+
error('Unterminated string');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** 解析数组:逗号 = null,空槽 = null */
|
|
326
|
+
function parseArray() {
|
|
327
|
+
pos++; // skip [
|
|
328
|
+
const result = [];
|
|
329
|
+
skipWs();
|
|
330
|
+
if (text[pos] === ']') { pos++; return result; }
|
|
331
|
+
|
|
332
|
+
while (true) {
|
|
333
|
+
skipWs();
|
|
334
|
+
const ch = text[pos];
|
|
335
|
+
|
|
336
|
+
if (ch === ',' || ch === ']') {
|
|
337
|
+
// 空槽 → null
|
|
338
|
+
if (ch === ']') {
|
|
339
|
+
result.push(null);
|
|
340
|
+
pos++;
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
result.push(null);
|
|
344
|
+
pos++; // skip comma
|
|
345
|
+
skipWs();
|
|
346
|
+
if (text[pos] === ']') {
|
|
347
|
+
result.push(null); // 尾部空槽
|
|
348
|
+
pos++;
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
result.push(parseValue());
|
|
355
|
+
skipWs();
|
|
356
|
+
|
|
357
|
+
if (text[pos] === ']') { pos++; return result; }
|
|
358
|
+
if (text[pos] === ',') { pos++; continue; }
|
|
359
|
+
error(`Expected , or ], got: ${text[pos]}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function parseObject() {
|
|
364
|
+
pos++; // skip {
|
|
365
|
+
const obj = {};
|
|
366
|
+
skipWs();
|
|
367
|
+
if (text[pos] === '}') { pos++; return obj; }
|
|
368
|
+
|
|
369
|
+
while (true) {
|
|
370
|
+
skipWs();
|
|
371
|
+
// key 支持引号字符串或裸字符串
|
|
372
|
+
const key = text[pos] === '"' ? parseString() : parseBareString();
|
|
373
|
+
skipWs();
|
|
374
|
+
if (text[pos] !== ':') error('Expected :');
|
|
375
|
+
pos++;
|
|
376
|
+
const val = parseValue();
|
|
377
|
+
obj[key] = val;
|
|
378
|
+
skipWs();
|
|
379
|
+
if (text[pos] === '}') { pos++; return obj; }
|
|
380
|
+
if (text[pos] === ',') { pos++; continue; }
|
|
381
|
+
error(`Expected , or }`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** 解析裸字符串(无引号标识符),读到分隔符或 EOF 为止 */
|
|
386
|
+
function parseBareString() {
|
|
387
|
+
const start = pos;
|
|
388
|
+
while (pos < text.length && !/[\s\[\]{},:]/.test(text[pos])) {
|
|
389
|
+
pos++;
|
|
390
|
+
}
|
|
391
|
+
const result = text.substring(start, pos);
|
|
392
|
+
if (result === '') error('Expected value');
|
|
393
|
+
if (result === 'null') return null;
|
|
394
|
+
if (result === 'true') return true;
|
|
395
|
+
if (result === 'false') return false;
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parseNumber() {
|
|
400
|
+
const start = pos;
|
|
401
|
+
if (text[pos] === '-') pos++;
|
|
402
|
+
while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
|
|
403
|
+
if (text[pos] === '.') {
|
|
404
|
+
pos++;
|
|
405
|
+
while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
|
|
406
|
+
}
|
|
407
|
+
if (text[pos] === 'e' || text[pos] === 'E') {
|
|
408
|
+
pos++;
|
|
409
|
+
if (text[pos] === '+' || text[pos] === '-') pos++;
|
|
410
|
+
while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
|
|
411
|
+
}
|
|
412
|
+
const num = Number(text.substring(start, pos));
|
|
413
|
+
if (!Number.isFinite(num)) return null;
|
|
414
|
+
return num;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const result = parseValue();
|
|
418
|
+
skipWs();
|
|
419
|
+
if (pos < text.length) error(`Unexpected trailing: "${text.slice(pos)}"`);
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
module.exports = { compress, decompress, stringify, parse };
|