slimjson 1.0.3 → 1.1.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/compress.js CHANGED
@@ -1,410 +1,544 @@
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 buildRow(obj, keys) {
99
- const row = [];
100
- for (const key of keys) {
101
- if (typeof key === 'string') {
102
- // 普通字段:缺失则 push null;显式的 undefined 也转 null
103
- const v = (obj != null && typeof obj === 'object') ? obj[key] : undefined;
104
- row.push(v === undefined ? null : v);
105
- } else {
106
- // 嵌套结构
107
- const [[keyName, childKeys]] = Object.entries(key);
108
- const val = obj != null && typeof obj === 'object' ? obj[keyName] : undefined;
109
-
110
- if (val === undefined || val === null) {
111
- row.push(null); // 字段缺失或为 null
112
-
113
- } else if (Array.isArray(val)) {
114
- // 对象数组
115
- row.push(val.map(item => buildRow(item, childKeys)));
116
-
117
- } else {
118
- // 单个嵌套对象
119
- row.push(buildRow(val, childKeys));
120
- }
121
- }
122
- }
123
- return row;
124
- }
125
-
126
- /* ============================================================
127
- compress / decompress
128
- ============================================================ */
129
- function compress(source) {
130
- if (Object.prototype.toString.call(source) === '[object Object]') {
131
- source = [source]
132
- } else if (!Array.isArray(source) || source.length === 0) {
133
- return source; // 不满足条件直接返回原值
134
- }
135
- const keys = buildKeys(source);
136
- const rows = source.map(obj => buildRow(obj, keys));
137
- return { keys, rows };
138
- }
139
-
140
- function decompress(compressed) {
141
- function buildFromRow(row, keys) {
142
- const obj = {};
143
- for (let i = 0; i < keys.length; i++) {
144
- const key = keys[i];
145
- const val = row[i];
146
-
147
- if (typeof key === 'string') {
148
- obj[key] = val;
149
-
150
- } else {
151
- const [[keyName, childKeys]] = Object.entries(key);
152
-
153
- if (val === null || val === undefined) {
154
- // 字段缺失或为 null → 写入 null
155
- obj[keyName] = val;
156
-
157
- } else if (Array.isArray(val) && val.length > 0 && Array.isArray(val[0])) {
158
- // 对象数组
159
- obj[keyName] = val.map(r => buildFromRow(r, childKeys));
160
-
161
- } else {
162
- // 单个嵌套对象(也可能是空数组 [],当作空对象处理)
163
- obj[keyName] = buildFromRow(val, childKeys);
164
- }
165
- }
166
- }
167
- return obj;
168
- }
169
- return compressed.rows.map(row => buildFromRow(row, compressed.keys));
170
- }
171
-
172
- /* ============================================================
173
- 判断字符串是否可安全省略引号
174
- ============================================================ */
175
- function isSafeBareString(s) {
176
- if (s === '') return false;
177
- if (s === 'null' || s === 'true' || s === 'false') return false;
178
- // 看起来像数字的不省略
179
- if (/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/.test(s)) return false;
180
- // 以数字或减号开头的也不省略(避免 parseValue 的数字分支误吞)
181
- if (/^[-\d]/.test(s)) return false;
182
- // 含分隔符、空白的不省略
183
- if (/[\s\[\]{},:"]/.test(s)) return false;
184
- return true;
185
- }
186
-
187
- /* ============================================================
188
- stringify / parse 省略 null 的文本化与还原
189
- ============================================================ */
190
-
191
- /**
192
- * compress 结果文本化
193
- * - 数组中的 null 被省略为逗号空槽:[null, 1, null] → [,1,]
194
- * - 安全的字符串省略引号:"hello" hello
195
- *
196
- * @param {*} compressed 待序列化的值
197
- */
198
- function stringify(compressed) {
199
- return serializeValue(compressed);
200
- }
201
-
202
- /** 序列化单个值 */
203
- function serializeValue(v) {
204
- if (v === null || v === undefined) return 'null';
205
- if (typeof v === 'string') {
206
- if (isSafeBareString(v)) return v;
207
- return JSON.stringify(v);
208
- }
209
- if (typeof v === 'number') {
210
- if (Number.isFinite(v)) return String(v);
211
- return 'null'; // NaN, Infinity → null
212
- }
213
- if (typeof v === 'boolean') return String(v);
214
- if (Array.isArray(v)) return serializeArray(v);
215
- if (typeof v === 'object') return serializeObject(v);
216
- return 'null';
217
- }
218
-
219
- /** 序列化数组:null 变为空槽(保留逗号) */
220
- function serializeArray(arr) {
221
- if (arr.length === 0) return '[]';
222
- const parts = arr.map(item => {
223
- if (item === null || item === undefined) return '';
224
- return serializeValue(item);
225
- });
226
- const inner = parts.join(',');
227
- const result = '[' + inner + ']';
228
- // 特殊边界:单 null 元素无法用逗号表示
229
- // [,] 在本格式中代表 2 个 null,所以 [null] 必须保留 null 文字
230
- if (result === '[]') return '[null]';
231
- return result;
232
- }
233
-
234
- /** 序列化对象(用于 keys 中的嵌套结构) */
235
- function serializeObject(obj) {
236
- const pairs = Object.entries(obj).map(([k, v]) => {
237
- const keyStr = isSafeBareString(k) ? k : JSON.stringify(k);
238
- return keyStr + ':' + serializeValue(v);
239
- });
240
- return '{' + pairs.join(',') + '}';
241
- }
242
-
243
- /**
244
- * 解析 stringify 产生的文本,恢复省略的 null
245
- */
246
- function parse(text) {
247
- let pos = 0;
248
-
249
- function error(msg) {
250
- throw new Error(`Parse error at ${pos}: ${msg} — near "${text.slice(Math.max(0, pos - 5), pos + 10)}"`);
251
- }
252
-
253
- function skipWs() {
254
- while (pos < text.length && /\s/.test(text[pos])) pos++;
255
- }
256
-
257
- /** 判断字符是否为值边界(分隔符或 EOF) */
258
- function isBoundaryChar(c) {
259
- return c === undefined || /[\s\[\]{},:]/.test(c);
260
- }
261
-
262
- function parseValue() {
263
- skipWs();
264
- if (pos >= text.length) error('Unexpected end');
265
- const ch = text[pos];
266
- if (ch === '"') return parseString();
267
- if (ch === '{') return parseObject();
268
- if (ch === '[') return parseArray();
269
- // 关键字:精确匹配且后接边界符或 EOF,否则归为裸字符串
270
- if (text.startsWith('null', pos) && isBoundaryChar(text[pos + 4])) { pos += 4; return null; }
271
- if (text.startsWith('true', pos) && isBoundaryChar(text[pos + 4])) { pos += 4; return true; }
272
- if (text.startsWith('false', pos) && isBoundaryChar(text[pos + 5])) { pos += 5; return false; }
273
- if (ch === '-' || (ch >= '0' && ch <= '9')) return parseNumber();
274
- // 裸字符串(无引号标识符)
275
- return parseBareString();
276
- }
277
-
278
- function parseString() {
279
- let result = '';
280
- pos++; // skip opening "
281
- while (pos < text.length) {
282
- const ch = text[pos];
283
- if (ch === '"') { pos++; return result; }
284
- if (ch === '\\') {
285
- pos++;
286
- const esc = text[pos];
287
- switch (esc) {
288
- case '"': result += '"'; break;
289
- case '\\': result += '\\'; break;
290
- case '/': result += '/'; break;
291
- case 'b': result += '\b'; break;
292
- case 'f': result += '\f'; break;
293
- case 'n': result += '\n'; break;
294
- case 'r': result += '\r'; break;
295
- case 't': result += '\t'; break;
296
- case 'u': {
297
- const hex = text.substring(pos + 1, pos + 5);
298
- result += String.fromCharCode(parseInt(hex, 16));
299
- pos += 4;
300
- break;
301
- }
302
- default: result += esc;
303
- }
304
- } else {
305
- result += ch;
306
- }
307
- pos++;
308
- }
309
- error('Unterminated string');
310
- }
311
-
312
- /** 解析数组:逗号 = null,空槽 = null */
313
- function parseArray() {
314
- pos++; // skip [
315
- const result = [];
316
- skipWs();
317
- if (text[pos] === ']') { pos++; return result; }
318
-
319
- while (true) {
320
- skipWs();
321
- const ch = text[pos];
322
-
323
- if (ch === ',' || ch === ']') {
324
- // 空槽 → null
325
- if (ch === ']') {
326
- result.push(null);
327
- pos++;
328
- return result;
329
- }
330
- result.push(null);
331
- pos++; // skip comma
332
- skipWs();
333
- if (text[pos] === ']') {
334
- result.push(null); // 尾部空槽
335
- pos++;
336
- return result;
337
- }
338
- continue;
339
- }
340
-
341
- result.push(parseValue());
342
- skipWs();
343
-
344
- if (text[pos] === ']') { pos++; return result; }
345
- if (text[pos] === ',') { pos++; continue; }
346
- error(`Expected , or ], got: ${text[pos]}`);
347
- }
348
- }
349
-
350
- function parseObject() {
351
- pos++; // skip {
352
- const obj = {};
353
- skipWs();
354
- if (text[pos] === '}') { pos++; return obj; }
355
-
356
- while (true) {
357
- skipWs();
358
- // key 支持引号字符串或裸字符串
359
- const key = text[pos] === '"' ? parseString() : parseBareString();
360
- skipWs();
361
- if (text[pos] !== ':') error('Expected :');
362
- pos++;
363
- const val = parseValue();
364
- obj[key] = val;
365
- skipWs();
366
- if (text[pos] === '}') { pos++; return obj; }
367
- if (text[pos] === ',') { pos++; continue; }
368
- error(`Expected , or }`);
369
- }
370
- }
371
-
372
- /** 解析裸字符串(无引号标识符),读到分隔符或 EOF 为止 */
373
- function parseBareString() {
374
- const start = pos;
375
- while (pos < text.length && !/[\s\[\]{},:]/.test(text[pos])) {
376
- pos++;
377
- }
378
- const result = text.substring(start, pos);
379
- if (result === '') error('Expected value');
380
- if (result === 'null') return null;
381
- if (result === 'true') return true;
382
- if (result === 'false') return false;
383
- return result;
384
- }
385
-
386
- function parseNumber() {
387
- const start = pos;
388
- if (text[pos] === '-') pos++;
389
- while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
390
- if (text[pos] === '.') {
391
- pos++;
392
- while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
393
- }
394
- if (text[pos] === 'e' || text[pos] === 'E') {
395
- pos++;
396
- if (text[pos] === '+' || text[pos] === '-') pos++;
397
- while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
398
- }
399
- const num = Number(text.substring(start, pos));
400
- if (!Number.isFinite(num)) return null;
401
- return num;
402
- }
403
-
404
- const result = parseValue();
405
- skipWs();
406
- if (pos < text.length) error(`Unexpected trailing: "${text.slice(pos)}"`);
407
- return result;
408
- }
409
-
410
- module.exports = { compress, decompress, stringify, parse };
1
+ /**
2
+ * 对象数组压缩转换工具
3
+ *
4
+ * 支持:不同对象有不同 key(后端未返回 null 字段时)
5
+ * - buildKeys 扫描所有对象取 key 并集
6
+ * - 嵌套对象/数组递归时同样合并子结构
7
+ * - 缺失的 key 在 rows 中填充 null
8
+ */
9
+
10
+ /**
11
+ * 合并两个 schema(取键的并集,保持顺序)
12
+ * 对象 schema 合并键,嵌套数组 schema 递归合并内层,原始值数组取第一个
13
+ */
14
+ function mergeSchemas(s1, s2) {
15
+ if (!Array.isArray(s1) || !Array.isArray(s2)) return s1;
16
+
17
+ const first1 = s1[0];
18
+ const first2 = s2[0];
19
+
20
+ // 两者都是对象 schema(元素是字符串或 {key: sub} 对象)→ 合并字段
21
+ const isObj1 = s1.length === 0 || typeof first1 === 'string' ||
22
+ (typeof first1 === 'object' && first1 !== null && !Array.isArray(first1));
23
+ const isObj2 = s2.length === 0 || typeof first2 === 'string' ||
24
+ (typeof first2 === 'object' && first2 !== null && !Array.isArray(first2));
25
+
26
+ if (isObj1 && isObj2) {
27
+ const merged = [...s1];
28
+ const existingKeys = new Set();
29
+ for (const field of merged) {
30
+ existingKeys.add(typeof field === 'string' ? field : Object.keys(field)[0]);
31
+ }
32
+ for (const field of s2) {
33
+ const key = typeof field === 'string' ? field : Object.keys(field)[0];
34
+ if (!existingKeys.has(key)) {
35
+ merged.push(field);
36
+ existingKeys.add(key);
37
+ }
38
+ }
39
+ return merged;
40
+ }
41
+
42
+ // 两者都是数组(不是对象 schema)→ 递归合并第一个元素
43
+ if (Array.isArray(first1) && Array.isArray(first2)) {
44
+ return [mergeSchemas(first1, first2)];
45
+ }
46
+
47
+ // 其他情况(原始值数组或类型不匹配)→ 取第一个
48
+ return s1;
49
+ }
50
+
51
+ /**
52
+ * 推断值的 schema(从所有数据中收集完整结构)
53
+ */
54
+ function inferSchema(value) {
55
+ if (Array.isArray(value)) {
56
+ if (value.length === 0) return [[]];
57
+ const first = value[0];
58
+ if (typeof first === 'object' && first !== null && !Array.isArray(first)) {
59
+ // 对象数组
60
+ return [inferObjectSchema(value)];
61
+ }
62
+ if (Array.isArray(first)) {
63
+ // 数组的数组:对每个内层数组递归推断 schema,然后合并
64
+ let merged = null;
65
+ for (const inner of value) {
66
+ const s = inferSchema(inner);
67
+ if (s) {
68
+ merged = merged ? mergeSchemas(merged, s) : s;
69
+ }
70
+ }
71
+ return [merged || inferSchema(first)];
72
+ }
73
+ // 检查是否含对象(混合数组)
74
+ const objects = value.filter(v => v && typeof v === 'object' && !Array.isArray(v));
75
+ if (objects.length > 0) {
76
+ return [inferObjectSchema(objects)];
77
+ }
78
+ // 原始值数组 - 不压缩,由父级处理
79
+ return undefined;
80
+ }
81
+ if (typeof value === 'object' && value !== null) {
82
+ return inferObjectSchema([value]);
83
+ }
84
+ return undefined;
85
+ }
86
+
87
+ /**
88
+ * 从多个对象中推断对象 schema(取所有 key 的并集)
89
+ */
90
+ function inferObjectSchema(objects) {
91
+ const keyOrder = [];
92
+ const keyValues = new Map();
93
+
94
+ for (const obj of objects) {
95
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) continue;
96
+ for (const key of Object.keys(obj)) {
97
+ if (!keyValues.has(key)) {
98
+ keyOrder.push(key);
99
+ keyValues.set(key, []);
100
+ }
101
+ const val = obj[key];
102
+ if (val != null) {
103
+ keyValues.get(key).push(val);
104
+ }
105
+ }
106
+ }
107
+
108
+ return keyOrder.map(key => {
109
+ const values = keyValues.get(key) || [];
110
+ if (values.length === 0) return key;
111
+
112
+ const sample = values[0];
113
+
114
+ // 值是对象 → 递归推断对象 schema(单对象,非数组)
115
+ if (typeof sample === 'object' && sample !== null && !Array.isArray(sample)) {
116
+ const subObjects = values.filter(v => typeof v === 'object' && v !== null && !Array.isArray(v));
117
+ return { [key]: inferObjectSchema(subObjects) };
118
+ }
119
+
120
+ // 值是数组
121
+ if (Array.isArray(sample)) {
122
+ // 空数组 → 无法推断,用 key 名
123
+ if (sample.length === 0) return key;
124
+
125
+ // 数组元素是对象 → 对象数组:{ key: [objectSchema] }
126
+ if (typeof sample[0] === 'object' && sample[0] !== null && !Array.isArray(sample[0])) {
127
+ const allItems = [];
128
+ for (const v of values) {
129
+ if (Array.isArray(v)) {
130
+ for (const item of v) {
131
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
132
+ allItems.push(item);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return { [key]: [inferObjectSchema(allItems)] };
138
+ }
139
+
140
+ // 数组元素是数组 → 检查是否含对象
141
+ if (Array.isArray(sample[0])) {
142
+ // 不含对象的嵌套数组(如 [[1,2],[3,4]])→ 不压缩,直接用 key 名
143
+ if (!containsObject(sample)) return key;
144
+
145
+ // 含对象的嵌套数组 递归推断内层 schema 并合并
146
+ let merged = null;
147
+ for (const v of values) {
148
+ if (Array.isArray(v)) {
149
+ const s = inferSchema(v);
150
+ if (s) {
151
+ // inferSchema 返回 [innerSchema],取 innerSchema 用于合并
152
+ const inner = Array.isArray(s) && s.length === 1 ? s[0] : s;
153
+ merged = merged ? mergeSchemas(merged, inner) : inner;
154
+ }
155
+ }
156
+ }
157
+ // 再包一层 [] 表示"数组的数组"
158
+ return { [key]: [merged || inferSchema(sample[0])] };
159
+ }
160
+
161
+ // 原始值数组(如 ["张三","李四"])→ 不压缩,直接用 key 名
162
+ return key;
163
+ }
164
+
165
+ // 原始值 → 直接用 key 名
166
+ return key;
167
+ });
168
+ }
169
+
170
+ /**
171
+ * 使用已知 schema 压缩值为 data
172
+ */
173
+ function compressWithSchema(value, schema) {
174
+ if (schema === undefined) return value;
175
+
176
+ // schema [innerSchema] 值是数组
177
+ if (Array.isArray(schema) && schema.length === 1 && Array.isArray(schema[0])) {
178
+ const inner = schema[0];
179
+ if (!Array.isArray(value)) return null;
180
+ return value.map(item => compressWithSchema(item, inner));
181
+ }
182
+
183
+ // schema 包含 undefined → 原始值数组,不压缩
184
+ if (Array.isArray(schema) && schema.some(s => s === undefined || s === null)) {
185
+ return value;
186
+ }
187
+
188
+ // schema 是数组(对象 schema)→ 值是对象
189
+ if (Array.isArray(schema)) {
190
+ if (Array.isArray(value)) return value.length === 0 ? [] : null;
191
+ if (!value || typeof value !== 'object') return value;
192
+ return schema.map(fieldDef => {
193
+ let key, valueSchema;
194
+ if (typeof fieldDef === 'string') {
195
+ key = fieldDef;
196
+ valueSchema = undefined;
197
+ } else {
198
+ key = Object.keys(fieldDef)[0];
199
+ valueSchema = fieldDef[key];
200
+ }
201
+ const val = value[key];
202
+ if (val == null) return null;
203
+ return compressWithSchema(val, valueSchema);
204
+ });
205
+ }
206
+
207
+ return value;
208
+ }
209
+
210
+ /**
211
+ * 判断值是否包含对象(递归检查)
212
+ */
213
+ function containsObject(value) {
214
+ if (value === null || typeof value !== 'object') return false;
215
+ if (!Array.isArray(value)) return true;
216
+ for (const item of value) {
217
+ if (containsObject(item)) return true;
218
+ }
219
+ return false;
220
+ }
221
+
222
+ /**
223
+ * 递归去掉数组尾部连续 null
224
+ */
225
+ function trimTrailingNullsDeep(data) {
226
+ if (!Array.isArray(data)) return data;
227
+ const trimmed = data.map(item => trimTrailingNullsDeep(item));
228
+ while (trimmed.length > 0 && trimmed[trimmed.length - 1] === null) {
229
+ trimmed.pop();
230
+ }
231
+ return trimmed;
232
+ }
233
+
234
+ /**
235
+ * 压缩任意 JSON 值为 { schema, data } 格式
236
+ * 不含对象的数组和非对象非数组的值直接返回
237
+ * @param {any} value - 要压缩的值
238
+ * @param {object} [options] - 选项
239
+ * @param {boolean} [options.trimTrailingNulls=false] - 去掉数组尾部连续 null
240
+ */
241
+ function compress(value, options) {
242
+ if (!containsObject(value)) return value;
243
+ const schema = inferSchema(value);
244
+ let data = compressWithSchema(value, schema);
245
+ if (options && options.trimTrailingNulls) {
246
+ data = trimTrailingNullsDeep(data);
247
+ }
248
+ return { schema, data };
249
+ }
250
+
251
+ /**
252
+ * 使用 schema 还原 data 为原始对象
253
+ */
254
+ function decompressWithSchema(data, schema) {
255
+ if (schema === undefined) return data;
256
+ if (data == null) return null;
257
+
258
+ // schema 是 [innerSchema] → 还原为数组
259
+ if (Array.isArray(schema) && schema.length === 1 && Array.isArray(schema[0])) {
260
+ if (!Array.isArray(data)) return data;
261
+ const inner = schema[0];
262
+ return data.map(item => decompressWithSchema(item, inner));
263
+ }
264
+
265
+ // schema 是数组
266
+ if (Array.isArray(schema)) {
267
+ // schema 包含 undefined 元素 → 原始值数组,不压缩
268
+ if (schema.some(s => s === undefined || s === null)) return data;
269
+
270
+ // 原始值(混合数组中的原始元素)→ 直接返回
271
+ if (typeof data !== 'object' || data === null) return data;
272
+
273
+ // 对象 schema 还原为对象
274
+ const obj = {};
275
+ for (let i = 0; i < schema.length; i++) {
276
+ const fieldDef = schema[i];
277
+ let key, valueSchema;
278
+ if (typeof fieldDef === 'string') {
279
+ key = fieldDef;
280
+ valueSchema = undefined;
281
+ } else if (typeof fieldDef === 'object' && fieldDef !== null) {
282
+ key = Object.keys(fieldDef)[0];
283
+ valueSchema = fieldDef[key];
284
+ } else {
285
+ continue;
286
+ }
287
+ const val = data[i];
288
+ if (val === undefined) { obj[key] = null; continue; }
289
+ obj[key] = decompressWithSchema(val, valueSchema);
290
+ }
291
+ return obj;
292
+ }
293
+
294
+ return data;
295
+ }
296
+
297
+ /**
298
+ * 从 { schema, data } 还原为原始值
299
+ * 如果输入不含 schema(直接值),原样返回
300
+ */
301
+ function decompress(compressed) {
302
+ if (compressed === null || typeof compressed !== 'object' || Array.isArray(compressed)) return compressed;
303
+ if (!('data' in compressed)) return compressed;
304
+ return decompressWithSchema(compressed.data, compressed.schema);
305
+ }
306
+ /* ============================================================
307
+ 判断字符串是否可安全省略引号
308
+ ============================================================ */
309
+ function isSafeBareString(s) {
310
+ if (s === '') return false;
311
+ if (s === 'null' || s === 'true' || s === 'false') return false;
312
+ // 看起来像数字的不省略
313
+ if (/^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/.test(s)) return false;
314
+ // 以数字或减号开头的也不省略(避免 parseValue 的数字分支误吞)
315
+ if (/^[-\d]/.test(s)) return false;
316
+ // 含分隔符、空白的不省略
317
+ if (/[\s\[\]{},:"]/.test(s)) return false;
318
+ return true;
319
+ }
320
+
321
+ /* ============================================================
322
+ stringify / parse — 省略 null 的文本化与还原
323
+ ============================================================ */
324
+
325
+ /**
326
+ * 将 compress 结果文本化
327
+ * - 数组中的 null 被省略为逗号空槽:[null, 1, null] → [,1,]
328
+ * - 安全的字符串省略引号:"hello" → hello
329
+ *
330
+ * @param {*} compressed 待序列化的值
331
+ */
332
+ function stringify(compressed) {
333
+ return serializeValue(compressed);
334
+ }
335
+
336
+ /** 序列化单个值 */
337
+ function serializeValue(v) {
338
+ if (v === null || v === undefined) return 'null';
339
+ if (typeof v === 'string') {
340
+ if (isSafeBareString(v)) return v;
341
+ return JSON.stringify(v);
342
+ }
343
+ if (typeof v === 'number') {
344
+ if (Number.isFinite(v)) return String(v);
345
+ return 'null'; // NaN, Infinity null
346
+ }
347
+ if (typeof v === 'boolean') return String(v);
348
+ if (Array.isArray(v)) return serializeArray(v);
349
+ if (typeof v === 'object') return serializeObject(v);
350
+ return 'null';
351
+ }
352
+
353
+ /** 序列化数组:null 变为空槽(保留逗号) */
354
+ function serializeArray(arr) {
355
+ if (arr.length === 0) return '[]';
356
+ const parts = arr.map(item => {
357
+ if (item === null || item === undefined) return '';
358
+ return serializeValue(item);
359
+ });
360
+ const inner = parts.join(',');
361
+ const result = '[' + inner + ']';
362
+ // 特殊边界:单 null 元素无法用逗号表示
363
+ // [,] 在本格式中代表 2 个 null,所以 [null] 必须保留 null 文字
364
+ if (result === '[]') return '[null]';
365
+ return result;
366
+ }
367
+
368
+ /** 序列化对象(用于 keys 中的嵌套结构) */
369
+ function serializeObject(obj) {
370
+ const pairs = Object.entries(obj).map(([k, v]) => {
371
+ const keyStr = isSafeBareString(k) ? k : JSON.stringify(k);
372
+ return keyStr + ':' + serializeValue(v);
373
+ });
374
+ return '{' + pairs.join(',') + '}';
375
+ }
376
+
377
+ /**
378
+ * 解析 stringify 产生的文本,恢复省略的 null
379
+ */
380
+ function parse(text) {
381
+ let pos = 0;
382
+
383
+ function error(msg) {
384
+ throw new Error(`Parse error at ${pos}: ${msg} — near "${text.slice(Math.max(0, pos - 5), pos + 10)}"`);
385
+ }
386
+
387
+ function skipWs() {
388
+ while (pos < text.length && /\s/.test(text[pos])) pos++;
389
+ }
390
+
391
+ /** 判断字符是否为值边界(分隔符或 EOF) */
392
+ function isBoundaryChar(c) {
393
+ return c === undefined || /[\s\[\]{},:]/.test(c);
394
+ }
395
+
396
+ function parseValue() {
397
+ skipWs();
398
+ if (pos >= text.length) error('Unexpected end');
399
+ const ch = text[pos];
400
+ if (ch === '"') return parseString();
401
+ if (ch === '{') return parseObject();
402
+ if (ch === '[') return parseArray();
403
+ // 关键字:精确匹配且后接边界符或 EOF,否则归为裸字符串
404
+ if (text.startsWith('null', pos) && isBoundaryChar(text[pos + 4])) { pos += 4; return null; }
405
+ if (text.startsWith('true', pos) && isBoundaryChar(text[pos + 4])) { pos += 4; return true; }
406
+ if (text.startsWith('false', pos) && isBoundaryChar(text[pos + 5])) { pos += 5; return false; }
407
+ if (ch === '-' || (ch >= '0' && ch <= '9')) return parseNumber();
408
+ // 裸字符串(无引号标识符)
409
+ return parseBareString();
410
+ }
411
+
412
+ function parseString() {
413
+ let result = '';
414
+ pos++; // skip opening "
415
+ while (pos < text.length) {
416
+ const ch = text[pos];
417
+ if (ch === '"') { pos++; return result; }
418
+ if (ch === '\\') {
419
+ pos++;
420
+ const esc = text[pos];
421
+ switch (esc) {
422
+ case '"': result += '"'; break;
423
+ case '\\': result += '\\'; break;
424
+ case '/': result += '/'; break;
425
+ case 'b': result += '\b'; break;
426
+ case 'f': result += '\f'; break;
427
+ case 'n': result += '\n'; break;
428
+ case 'r': result += '\r'; break;
429
+ case 't': result += '\t'; break;
430
+ case 'u': {
431
+ const hex = text.substring(pos + 1, pos + 5);
432
+ result += String.fromCharCode(parseInt(hex, 16));
433
+ pos += 4;
434
+ break;
435
+ }
436
+ default: result += esc;
437
+ }
438
+ } else {
439
+ result += ch;
440
+ }
441
+ pos++;
442
+ }
443
+ error('Unterminated string');
444
+ }
445
+
446
+ /** 解析数组:逗号 = null,空槽 = null */
447
+ function parseArray() {
448
+ pos++; // skip [
449
+ const result = [];
450
+ skipWs();
451
+ if (text[pos] === ']') { pos++; return result; }
452
+
453
+ while (true) {
454
+ skipWs();
455
+ const ch = text[pos];
456
+
457
+ if (ch === ',' || ch === ']') {
458
+ // 空槽 → null
459
+ if (ch === ']') {
460
+ result.push(null);
461
+ pos++;
462
+ return result;
463
+ }
464
+ result.push(null);
465
+ pos++; // skip comma
466
+ skipWs();
467
+ if (text[pos] === ']') {
468
+ result.push(null); // 尾部空槽
469
+ pos++;
470
+ return result;
471
+ }
472
+ continue;
473
+ }
474
+
475
+ result.push(parseValue());
476
+ skipWs();
477
+
478
+ if (text[pos] === ']') { pos++; return result; }
479
+ if (text[pos] === ',') { pos++; continue; }
480
+ error(`Expected , or ], got: ${text[pos]}`);
481
+ }
482
+ }
483
+
484
+ function parseObject() {
485
+ pos++; // skip {
486
+ const obj = {};
487
+ skipWs();
488
+ if (text[pos] === '}') { pos++; return obj; }
489
+
490
+ while (true) {
491
+ skipWs();
492
+ // key 支持引号字符串或裸字符串
493
+ const key = text[pos] === '"' ? parseString() : parseBareString();
494
+ skipWs();
495
+ if (text[pos] !== ':') error('Expected :');
496
+ pos++;
497
+ const val = parseValue();
498
+ obj[key] = val;
499
+ skipWs();
500
+ if (text[pos] === '}') { pos++; return obj; }
501
+ if (text[pos] === ',') { pos++; continue; }
502
+ error(`Expected , or }`);
503
+ }
504
+ }
505
+
506
+ /** 解析裸字符串(无引号标识符),读到分隔符或 EOF 为止 */
507
+ function parseBareString() {
508
+ const start = pos;
509
+ while (pos < text.length && !/[\s\[\]{},:]/.test(text[pos])) {
510
+ pos++;
511
+ }
512
+ const result = text.substring(start, pos);
513
+ if (result === '') error('Expected value');
514
+ if (result === 'null') return null;
515
+ if (result === 'true') return true;
516
+ if (result === 'false') return false;
517
+ return result;
518
+ }
519
+
520
+ function parseNumber() {
521
+ const start = pos;
522
+ if (text[pos] === '-') pos++;
523
+ while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
524
+ if (text[pos] === '.') {
525
+ pos++;
526
+ while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
527
+ }
528
+ if (text[pos] === 'e' || text[pos] === 'E') {
529
+ pos++;
530
+ if (text[pos] === '+' || text[pos] === '-') pos++;
531
+ while (pos < text.length && text[pos] >= '0' && text[pos] <= '9') pos++;
532
+ }
533
+ const num = Number(text.substring(start, pos));
534
+ if (!Number.isFinite(num)) return null;
535
+ return num;
536
+ }
537
+
538
+ const result = parseValue();
539
+ skipWs();
540
+ if (pos < text.length) error(`Unexpected trailing: "${text.slice(pos)}"`);
541
+ return result;
542
+ }
543
+
544
+ module.exports = { compress, decompress, stringify, parse };