slimjson 1.0.4 → 1.1.1

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
@@ -7,181 +7,302 @@
7
7
  * - 缺失的 key 在 rows 中填充 null
8
8
  */
9
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'; // [{...}, {...}]
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;
18
40
  }
19
- return 'primitive-array'; // [1, 2, 3] 或 []
20
- }
21
- if (typeof v === 'object') return 'object'; // {...}
22
- return 'primitive';
41
+
42
+ // 两者都是数组(不是对象 schema)→ 递归合并第一个元素
43
+ if (Array.isArray(first1) && Array.isArray(first2)) {
44
+ return [mergeSchemas(first1, first2)];
45
+ }
46
+
47
+ // 其他情况(原始值数组或类型不匹配)→ 取第一个
48
+ return s1;
23
49
  }
24
50
 
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);
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)];
38
61
  }
39
- }
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;
40
80
  }
41
- }
81
+ if (typeof value === 'object' && value !== null) {
82
+ return inferObjectSchema([value]);
83
+ }
84
+ return undefined;
85
+ }
42
86
 
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
- }
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
+ }
52
106
  }
53
107
 
54
- const kind = getValueKind(repValue);
108
+ return keyOrder.map(key => {
109
+ const values = keyValues.get(key) || [];
110
+ if (values.length === 0) return key;
55
111
 
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
- }
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) };
65
118
  }
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
- }
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)] };
80
138
  }
81
- }
82
- }
83
- }
84
- keys.push({ [keyName]: buildKeys(allItems) });
85
139
 
86
- } else {
87
- // primitive / primitive-array / null(全当字符串 key 处理)
88
- keys.push(keyName);
89
- }
90
- }
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
+ }
91
160
 
92
- return keys;
93
- }
161
+ // 原始值数组(如 ["张三","李四"])→ 不压缩,直接用 key 名
162
+ return key;
163
+ }
94
164
 
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);
165
+ // 原始值 → 直接用 key 名
166
+ return key;
167
+ });
103
168
  }
104
169
 
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);
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
+ }
124
182
 
125
- } else {
126
- // 单个嵌套对象
127
- const sub = buildRow(val, childKeys, trim);
128
- row.push(trim ? trimTrailingNulls(sub) : sub);
129
- }
183
+ // schema 包含 undefined → 原始值数组,不压缩
184
+ if (Array.isArray(schema) && schema.some(s => s === undefined || s === null)) {
185
+ return value;
130
186
  }
131
- }
132
- if (trim) {
133
- return trimTrailingNulls(row);
134
- }
135
- return row;
136
- }
137
187
 
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
- }
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
+ }
152
206
 
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];
207
+ return value;
208
+ }
159
209
 
160
- if (typeof key === 'string') {
161
- obj[key] = val === undefined ? null : val;
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
+ }
162
221
 
163
- } else {
164
- const [[keyName, childKeys]] = Object.entries(key);
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
+ }
165
233
 
166
- if (val === null || val === undefined) {
167
- // 字段缺失或为 null 写入 null
168
- obj[keyName] = null;
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
+ }
169
250
 
170
- } else if (Array.isArray(val) && val.length > 0 && Array.isArray(val[0])) {
171
- // 对象数组
172
- obj[keyName] = val.map(r => buildFromRow(r, childKeys));
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
+ }
173
264
 
174
- } else {
175
- // 单个嵌套对象(或被 trim 的子 row)
176
- obj[keyName] = buildFromRow(val, childKeys);
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);
177
290
  }
178
- }
291
+ return obj;
179
292
  }
180
- return obj;
181
- }
182
- return compressed.rows.map(row => buildFromRow(row, compressed.keys));
293
+
294
+ return data;
183
295
  }
184
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
+ }
185
306
  /* ============================================================
186
307
  判断字符串是否可安全省略引号
187
308
  ============================================================ */
@@ -421,3 +542,4 @@ function parse(text) {
421
542
  }
422
543
 
423
544
  module.exports = { compress, decompress, stringify, parse };
545
+ module.exports.default = module.exports;
@@ -1,42 +1,42 @@
1
- /**
2
- * 用法: node decompress-file.js <压缩文件> [输出.json]
3
- *
4
- * 默认输出文件名: <输入名>.json(去掉 .slim 后缀)
5
- */
6
- const fs = require('fs');
7
- const path = require('path');
8
- const { decompress, parse } = require('./compress');
9
-
10
- const input = process.argv[2];
11
- if (!input) {
12
- console.error('用法: node decompress-file.js <压缩文件> [输出.json]');
13
- process.exit(1);
14
- }
15
- if (!fs.existsSync(input)) {
16
- console.error(`文件不存在: ${input}`);
17
- process.exit(1);
18
- }
19
-
20
- const output = process.argv[3] || input.replace(/\.json\.slim$/i, '.json');
21
-
22
- let text;
23
- try {
24
- text = fs.readFileSync(input, 'utf8');
25
- } catch (e) {
26
- console.error(`读取文件失败: ${e.message}`);
27
- process.exit(1);
28
- }
29
-
30
- let compressed;
31
- try {
32
- compressed = parse(text);
33
- } catch (e) {
34
- console.error(`解析失败: ${e.message}`);
35
- process.exit(1);
36
- }
37
-
38
- const data = decompress(compressed);
39
- fs.writeFileSync(output, JSON.stringify(data, null, 2), 'utf8');
40
-
41
- console.log(`输入: ${path.basename(input)}`);
42
- console.log(`输出: ${path.basename(output)} (${data.length} 条)`);
1
+ /**
2
+ * 用法: node decompress-file.js <压缩文件> [输出.json]
3
+ *
4
+ * 默认输出文件名: <输入名>.json(去掉 .slim 后缀)
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { decompress, parse } = require('./compress');
9
+
10
+ const input = process.argv[2];
11
+ if (!input) {
12
+ console.error('用法: node decompress-file.js <压缩文件> [输出.json]');
13
+ process.exit(1);
14
+ }
15
+ if (!fs.existsSync(input)) {
16
+ console.error(`文件不存在: ${input}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ const output = process.argv[3] || input.replace(/\.json\.slim$/i, '.json');
21
+
22
+ let text;
23
+ try {
24
+ text = fs.readFileSync(input, 'utf8');
25
+ } catch (e) {
26
+ console.error(`读取文件失败: ${e.message}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ let compressed;
31
+ try {
32
+ compressed = parse(text);
33
+ } catch (e) {
34
+ console.error(`解析失败: ${e.message}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const data = decompress(compressed);
39
+ fs.writeFileSync(output, JSON.stringify(data, null, 2), 'utf8');
40
+
41
+ console.log(`输入: ${path.basename(input)}`);
42
+ console.log(`输出: ${path.basename(output)} (${data.length} 条)`);
package/esm.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { createRequire } from 'node:module';
2
- const require = createRequire(import.meta.url);
3
- const { compress, decompress, stringify, parse } = require('./compress.js');
4
- export { compress, decompress, stringify, parse };
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ const { compress, decompress, stringify, parse } = require('./compress.js');
4
+ export { compress, decompress, stringify, parse };
5
+ export default { compress, decompress, stringify, parse };
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
- {
2
- "name": "slimjson",
3
- "version": "1.0.4",
4
- "main": "compress.js",
5
- "exports": {
6
- ".": {
7
- "import": "./esm.mjs",
8
- "require": "./compress.js"
9
- }
10
- },
11
- "scripts": {
12
- "test": "jest"
13
- },
14
- "author": "lastheaven",
15
- "license": "MIT",
16
- "description": "轻量级对象数组压缩工具",
17
- "devDependencies": {
18
- "jest": "^30.4.2"
19
- },
20
- "repository": {
21
- "type": "git",
22
- "url": "https://github.com/LastHeaven/slimjson.git"
23
- }
24
- }
1
+ {
2
+ "name": "slimjson",
3
+ "version": "1.1.1",
4
+ "main": "compress.js",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./esm.mjs",
8
+ "require": "./compress.js"
9
+ }
10
+ },
11
+ "scripts": {
12
+ "test": "jest"
13
+ },
14
+ "author": "lastheaven",
15
+ "license": "MIT",
16
+ "description": "轻量级对象数组压缩工具",
17
+ "devDependencies": {
18
+ "jest": "^30.4.2"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/LastHeaven/slimjson.git"
23
+ }
24
+ }