@jsonup/core 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Cocova
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @jsonup/core
2
+
3
+ `jsonup` 生态系统的核心 JSON 解析器、状态管理器和文档构建器。
4
+
5
+ ## 特性
6
+
7
+ - **纯 TypeScript**:框架无关,可在浏览器和 Node.js 环境中运行。
8
+ - **身份树 (Identity Tree)**:自动跟踪和维护更新前后的节点身份,允许 UI 状态(如展开/折叠)在 JSON 数据改变时保持持久化。
9
+ - **路径解析**:内置支持路径解析和 JSON 树搜索。
10
+ - **惰性求值 (Lazy Evaluation)**:专为高性能设计,即使面对海量 JSON 结构也能从容应对。
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ npm install @jsonup/core
16
+ # 或
17
+ pnpm add @jsonup/core
18
+ # 或
19
+ yarn add @jsonup/core
20
+ ```
21
+
22
+ ## 基础用法
23
+
24
+ ```ts
25
+ import { createDocument } from "@jsonup/core";
26
+
27
+ const data = {
28
+ hello: "world",
29
+ items: [1, 2, 3],
30
+ };
31
+
32
+ const doc = createDocument(data, {
33
+ defaultExpandedRoot: true,
34
+ });
35
+
36
+ console.log(doc.nodes); // 准备好被渲染的扁平 JSON 节点列表
37
+ ```
38
+
39
+ ## 许可证
40
+
41
+ ## 许可证
42
+
43
+ [MIT](../../LICENSE)
@@ -0,0 +1,316 @@
1
+ import { Decimal } from "decimal.js";
2
+
3
+ //#region src/type.d.ts
4
+ /**
5
+ * 表示 JSON 基本数据类型的联合类型,包含 string、number、boolean、null 和 Decimal
6
+ */
7
+ type JsonPrimitive = string | number | boolean | null | Decimal;
8
+ /**
9
+ * 表示任意合法的 JSON 值类型,可以是基本类型、对象或数组
10
+ */
11
+ type JsonValue = JsonPrimitive | JsonObject | JsonArray;
12
+ /**
13
+ * 表示 JSON 输入的基本数据类型的联合类型,包含 string、number、bigint、boolean、null 和 Decimal
14
+ */
15
+ type JsonInputPrimitive = string | number | bigint | boolean | null | Decimal;
16
+ /**
17
+ * 表示任意合法的 JSON 输入值类型,可以是基本类型、对象或数组
18
+ */
19
+ type JsonInputValue = JsonInputPrimitive | JsonInputObject | JsonInputArray;
20
+ /**
21
+ * 表示 JSON 对象,键为字符串,值为 JsonValue
22
+ */
23
+ interface JsonObject {
24
+ [key: string]: JsonValue;
25
+ }
26
+ /**
27
+ * 表示 JSON 数组,元素为 JsonValue
28
+ */
29
+ type JsonArray = JsonValue[];
30
+ /**
31
+ * 表示 JSON 输入对象,键为字符串,值为 JsonInputValue
32
+ */
33
+ interface JsonInputObject {
34
+ [key: string]: JsonInputValue;
35
+ }
36
+ /**
37
+ * 表示 JSON 输入数组,元素为 JsonInputValue
38
+ */
39
+ type JsonInputArray = JsonInputValue[];
40
+ /**
41
+ * 表示 JSON 文档的根类型,可以是对象 JSON 或数组 JSON
42
+ */
43
+ type JsonDocumentType = 'object-json' | 'array-json';
44
+ /**
45
+ * 表示 JSON 节点的唯一标识符类型
46
+ */
47
+ type JsonNodeId = string;
48
+ /**
49
+ * 表示 JSON 节点的类型枚举
50
+ */
51
+ type JsonNodeType = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null';
52
+ /**
53
+ * 表示 JSON 树中的单个节点,包含节点的所有元数据和状态信息
54
+ */
55
+ interface JsonNode {
56
+ /** 节点的唯一标识符 */
57
+ id: JsonNodeId;
58
+ /** 节点在父对象中的键名,如果父节点是数组或是根节点则可能为 undefined */
59
+ key?: string;
60
+ /** 节点的值,如果是容器类型(对象/数组)则为 undefined */
61
+ value?: JsonValue;
62
+ /** 子节点的数量 */
63
+ children: number;
64
+ /** 父节点的标识符,如果是根节点则为 null */
65
+ parent: JsonNodeId | null;
66
+ /** 节点在树中的深度(根节点为 0) */
67
+ depth: number;
68
+ /** 节点的数据类型 */
69
+ type: JsonNodeType;
70
+ /** 节点在文档中的路径(例如:"[0].name") */
71
+ path: string;
72
+ /** 节点是否可以展开(即是否为包含子元素的容器节点) */
73
+ expandable: boolean;
74
+ /** 节点在文档中前序遍历的顺序索引 */
75
+ order: number;
76
+ /** 节点当前是否处于展开状态 */
77
+ expanded: boolean;
78
+ /** 节点是否为叶子节点(即非容器节点) */
79
+ leaf: boolean;
80
+ /** 以当前节点为根的子树的节点总数(包含自身) */
81
+ size: number;
82
+ /** 当前节点的所有后代节点数量 */
83
+ descendants: number;
84
+ }
85
+ /**
86
+ * 表示 JSON 文档的动态状态,例如节点的展开状态
87
+ */
88
+ interface JsonDocumentState {
89
+ /** 当前处于展开状态的所有节点路径的集合 */
90
+ expandedPaths: Set<string>;
91
+ /** 当前可见的节点列表(父节点折叠时,子节点不可见) */
92
+ visibleNodes: JsonNode[];
93
+ /** 切换指定节点的展开/折叠状态 */
94
+ toggleExpanded: (id: JsonNodeId, expanded?: boolean) => JsonDocument;
95
+ }
96
+ /**
97
+ * 表示完整的 JSON 文档实例,包含原始数据、节点列表、状态以及操作方法
98
+ */
99
+ interface JsonDocument {
100
+ /** 文档的根类型 */
101
+ type: JsonDocumentType;
102
+ /** 文档的原始 JSON 数据 */
103
+ raw: JsonObject | JsonArray;
104
+ /** 文档中所有节点的扁平列表 */
105
+ nodes: JsonNode[];
106
+ /** 文档的当前状态 */
107
+ state: JsonDocumentState;
108
+ /** 当前可见的所有节点列表 */
109
+ readonly visibleNodes: JsonNode[];
110
+ /** 切换指定节点的展开/折叠状态,并返回更新后的文档实例 */
111
+ toggleExpanded: (id: JsonNodeId, expanded?: boolean) => JsonDocument;
112
+ /** 将文档格式化为 JSON 字符串 */
113
+ stringify: (space?: number) => string;
114
+ /** 根据节点标识符获取对应的节点 */
115
+ getNode: (id: JsonNodeId) => JsonNode | undefined;
116
+ /** 根据节点标识符获取其父节点 */
117
+ getParent: (id: JsonNodeId) => JsonNode | undefined;
118
+ /** 根据节点标识符获取其直接子节点列表 */
119
+ getChildren: (id: JsonNodeId) => JsonNode[];
120
+ /** 根据节点标识符获取其所有后代节点列表 */
121
+ getDescendants: (id: JsonNodeId) => JsonNode[];
122
+ /** 根据节点标识符获取其在文档中的路径 */
123
+ getPath: (id: JsonNodeId) => string | undefined;
124
+ /** 根据路径查找对应的节点 */
125
+ findByPath: (path: string) => JsonNode | undefined;
126
+ /** 根据查询字符串搜索节点 */
127
+ search: (query: string) => JsonNode[];
128
+ }
129
+ /**
130
+ * 表示 JSON 节点的标识树,用于在重新生成文档时保持节点标识的稳定性
131
+ */
132
+ interface JsonNodeIdentity {
133
+ /** 节点的唯一标识符 */
134
+ id: JsonNodeId;
135
+ /** 子节点的标识树记录,对应对象键或数组索引 */
136
+ children?: Record<string, JsonNodeIdentity> | JsonNodeIdentity[];
137
+ }
138
+ /**
139
+ * 创建 JSON 文档时可用的配置选项
140
+ */
141
+ interface CreateDocumentOptions {
142
+ /**
143
+ * 是否默认展开根节点。对于对象来说是顶层,对于数组来说是第一层元素
144
+ * @default true
145
+ */
146
+ defaultExpandedRoot?: boolean;
147
+ /**
148
+ * 是否默认展开所有节点
149
+ * @default false
150
+ */
151
+ defaultExpandedAll?: boolean;
152
+ /**
153
+ * 默认展开到指定层级。使用 `true` 表示展开所有层级
154
+ * @default undefined
155
+ */
156
+ defaultExpandedDepth?: number | true;
157
+ /**
158
+ * 默认展开的路径列表
159
+ * @default undefined
160
+ */
161
+ expandedPaths?: Iterable<string>;
162
+ /**
163
+ * 标识树,用于状态恢复
164
+ * @default undefined
165
+ */
166
+ identityTree?: JsonNodeIdentity;
167
+ /**
168
+ * 是否克隆原始输入数据以防止内部状态突变
169
+ * @default false
170
+ */
171
+ cloneRaw?: boolean;
172
+ }
173
+ /**
174
+ * 表示可接受的 JSON 输入类型
175
+ */
176
+ type JsonInput = string | JsonInputObject | JsonInputArray;
177
+ //#endregion
178
+ //#region src/document.d.ts
179
+ /**
180
+ * 创建并初始化一个 JSON 文档实例。
181
+ * 将原始输入数据转换为包含树形结构信息、节点标识和交互状态的文档对象。
182
+ *
183
+ * @param input - 原始的 JSON 输入或现有的 JSON 文档实例
184
+ * @param options - 创建文档时的配置选项
185
+ * @returns 初始化后的完整 JSON 文档实例
186
+ */
187
+ declare function createDocument(input: JsonInput | JsonDocument, options?: CreateDocumentOptions): JsonDocument;
188
+ /**
189
+ * 获取 JSON 文档的标识树(Identity Tree)。
190
+ * 标识树用于在重新生成文档时保持节点的唯一标识,从而维持状态(如展开/折叠状态)的一致性。
191
+ *
192
+ * @param document - 目标 JSON 文档实例
193
+ * @returns 对应的节点标识树
194
+ * @throws 当文档中缺少标识树时抛出错误
195
+ */
196
+ declare function getDocumentIdentityTree(document: JsonDocument): JsonNodeIdentity;
197
+ /**
198
+ * 将 JSON 文档序列化为 JSON 字符串。
199
+ * 会自动处理 Decimal 类型的数值。
200
+ *
201
+ * @param document - 要序列化的 JSON 文档实例
202
+ * @param space - 用于缩进的空格数,默认为 2
203
+ * @returns 序列化后的 JSON 字符串
204
+ */
205
+ declare function stringify(document: JsonDocument, space?: number): string;
206
+ /**
207
+ * 判断给定的值是否为有效的 JSON 文档实例。
208
+ *
209
+ * @param value - 要检查的值
210
+ * @returns 如果是有效的 JsonDocument 返回 true,否则返回 false
211
+ */
212
+ declare function isJsonDocument(value: JsonInput | JsonDocument): value is JsonDocument;
213
+ //#endregion
214
+ //#region src/identity.d.ts
215
+ /**
216
+ * 生成唯一的 JSON 节点标识符。
217
+ *
218
+ * @returns 长度为 28 的随机唯一字符串
219
+ */
220
+ declare function createJsonNodeId(): JsonNodeId;
221
+ //#endregion
222
+ //#region src/normalize.d.ts
223
+ /**
224
+ * 将给定的值深度转换为合法的 JsonValue,支持 bigint 和 Decimal 转换。
225
+ *
226
+ * @param value - 要转换的任意值
227
+ * @returns 规范化后的 JsonValue
228
+ * @throws 当值无法被 JSON 序列化时抛出错误
229
+ */
230
+ declare function toJsonValue(value: unknown): JsonValue;
231
+ /**
232
+ * 判断给定的 JsonValue 是否为容器类型(对象或数组)。
233
+ *
234
+ * @param value - 要判断的 JsonValue
235
+ * @returns 如果是对象或数组则返回 true
236
+ */
237
+ declare function isContainerValue(value: JsonValue): value is JsonObject | JsonArray;
238
+ /**
239
+ * 判断给定的 JsonValue 是否为普通 JSON 对象。
240
+ *
241
+ * @param value - 要判断的 JsonValue
242
+ * @returns 如果是普通对象则返回 true
243
+ */
244
+ declare function isJsonObject(value: JsonValue): value is JsonObject;
245
+ //#endregion
246
+ //#region src/query.d.ts
247
+ /**
248
+ * 根据节点标识符获取对应的节点实例。
249
+ *
250
+ * @param document - JSON 文档实例
251
+ * @param id - 节点标识符
252
+ * @returns 如果找到对应节点则返回,否则返回 undefined
253
+ */
254
+ declare function getNode(document: JsonDocument, id: JsonNodeId): JsonNode | undefined;
255
+ /**
256
+ * 根据节点标识符获取其父节点。
257
+ *
258
+ * @param document - JSON 文档实例
259
+ * @param id - 节点标识符
260
+ * @returns 如果存在父节点则返回,否则返回 undefined(如根节点或未找到)
261
+ */
262
+ declare function getParent(document: JsonDocument, id: JsonNodeId): JsonNode | undefined;
263
+ /**
264
+ * 根据节点标识符获取其所有的直接子节点。
265
+ *
266
+ * @param document - JSON 文档实例
267
+ * @param id - 节点标识符
268
+ * @returns 子节点列表数组
269
+ */
270
+ declare function getChildren(document: JsonDocument, id: JsonNodeId): JsonNode[];
271
+ /**
272
+ * 根据节点标识符获取其所有的后代节点(按前序遍历顺序)。
273
+ *
274
+ * @param document - JSON 文档实例
275
+ * @param id - 节点标识符
276
+ * @returns 所有后代节点的数组
277
+ */
278
+ declare function getDescendants(document: JsonDocument, id: JsonNodeId): JsonNode[];
279
+ /**
280
+ * 根据节点标识符获取该节点在文档中的路径。
281
+ *
282
+ * @param document - JSON 文档实例
283
+ * @param id - 节点标识符
284
+ * @returns 节点的路径字符串,未找到时返回 undefined
285
+ */
286
+ declare function getPath(document: JsonDocument, id: JsonNodeId): string | undefined;
287
+ /**
288
+ * 根据路径字符串精确查找对应的节点。
289
+ *
290
+ * @param document - JSON 文档实例
291
+ * @param path - 节点路径字符串
292
+ * @returns 找到的节点实例,未找到时返回 undefined
293
+ */
294
+ declare function findByPath(document: JsonDocument, path: string): JsonNode | undefined;
295
+ /**
296
+ * 根据查询字符串在文档中模糊搜索匹配的节点。
297
+ * 匹配范围包括:节点的键名、路径、类型名称以及叶子节点的值。
298
+ * 搜索忽略大小写。
299
+ *
300
+ * @param document - JSON 文档实例
301
+ * @param query - 查询字符串
302
+ * @returns 匹配到的节点列表
303
+ */
304
+ declare function search(document: JsonDocument, query: string): JsonNode[];
305
+ //#endregion
306
+ //#region src/state.d.ts
307
+ /**
308
+ * 根据所有节点列表和它们的展开状态,计算出当前可见的节点列表。
309
+ * 折叠状态下的子节点会被跳过。
310
+ *
311
+ * @param nodes - 文档中所有节点的列表(按前序遍历顺序)
312
+ * @returns 当前可见节点的列表
313
+ */
314
+ declare function getVisibleNodes(nodes: JsonNode[]): JsonNode[];
315
+ //#endregion
316
+ export { type CreateDocumentOptions, type JsonArray, type JsonDocument, type JsonDocumentState, type JsonDocumentType, type JsonInput, type JsonInputArray, type JsonInputObject, type JsonInputPrimitive, type JsonInputValue, type JsonNode, type JsonNodeId, type JsonNodeIdentity, type JsonNodeType, type JsonObject, type JsonPrimitive, type JsonValue, createDocument, createJsonNodeId, findByPath, getChildren, getDescendants, getDocumentIdentityTree, getNode, getParent, getPath, getVisibleNodes, isContainerValue, isJsonDocument, isJsonObject, search, stringify, toJsonValue };
package/dist/index.mjs ADDED
@@ -0,0 +1,617 @@
1
+ import { Decimal } from "decimal.js";
2
+ import { nanoid } from "nanoid";
3
+ //#region src/identity.ts
4
+ /**
5
+ * 内部用于在 JSON 文档实例上挂载标识树的 Symbol 键。
6
+ */
7
+ const DOCUMENT_IDENTITY = Symbol.for("@jsonup/document-identity");
8
+ /**
9
+ * 生成唯一的 JSON 节点标识符。
10
+ *
11
+ * @returns 长度为 28 的随机唯一字符串
12
+ */
13
+ function createJsonNodeId() {
14
+ return nanoid(28);
15
+ }
16
+ //#endregion
17
+ //#region src/normalize.ts
18
+ const DECIMAL_MARKER = "__jsonupDecimal__";
19
+ /**
20
+ * 将输入解析并规范化为 JSON 根对象或数组。
21
+ * 如果输入是字符串,将安全地解析 JSON 字符串(大数将转换为 Decimal 类型)。
22
+ *
23
+ * @param input - JSON 字符串、对象或数组
24
+ * @returns 规范化后的 JsonObject 或 JsonArray
25
+ * @throws 当解析结果不是对象或数组时抛出错误
26
+ */
27
+ function toJsonRoot(input) {
28
+ const value = toJsonValue(typeof input === "string" ? parseJsonWithDecimal(input) : input);
29
+ if (!isContainerValue(value)) throw new TypeError("JSON root must be an object or array.");
30
+ return value;
31
+ }
32
+ /**
33
+ * 将给定的值深度转换为合法的 JsonValue,支持 bigint 和 Decimal 转换。
34
+ *
35
+ * @param value - 要转换的任意值
36
+ * @returns 规范化后的 JsonValue
37
+ * @throws 当值无法被 JSON 序列化时抛出错误
38
+ */
39
+ function toJsonValue(value) {
40
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
41
+ if (typeof value === "bigint") return new Decimal(value.toString());
42
+ if (Decimal.isDecimal(value)) return value;
43
+ if (Array.isArray(value)) return value.map((item) => toJsonValue(item));
44
+ if (isPlainObject(value)) {
45
+ const decimalValue = getDecimalMarkerValue(value);
46
+ if (decimalValue !== void 0) return new Decimal(decimalValue);
47
+ const result = {};
48
+ for (const [key, item] of Object.entries(value)) result[key] = toJsonValue(item);
49
+ return result;
50
+ }
51
+ throw new TypeError("Value must be JSON serializable.");
52
+ }
53
+ /**
54
+ * 判断给定的 JsonValue 是否为容器类型(对象或数组)。
55
+ *
56
+ * @param value - 要判断的 JsonValue
57
+ * @returns 如果是对象或数组则返回 true
58
+ */
59
+ function isContainerValue(value) {
60
+ return value !== null && typeof value === "object" && !Decimal.isDecimal(value);
61
+ }
62
+ /**
63
+ * 判断给定的 JsonValue 是否为普通 JSON 对象。
64
+ *
65
+ * @param value - 要判断的 JsonValue
66
+ * @returns 如果是普通对象则返回 true
67
+ */
68
+ function isJsonObject(value) {
69
+ return value !== null && !Array.isArray(value) && typeof value === "object" && !Decimal.isDecimal(value);
70
+ }
71
+ function isPlainObject(value) {
72
+ return Object.prototype.toString.call(value) === "[object Object]";
73
+ }
74
+ function parseJsonWithDecimal(input) {
75
+ return JSON.parse(rewriteUnsafeJsonNumbers(input));
76
+ }
77
+ function rewriteUnsafeJsonNumbers(input) {
78
+ let output = "";
79
+ let index = 0;
80
+ let inString = false;
81
+ let escaping = false;
82
+ while (index < input.length) {
83
+ const char = input[index];
84
+ if (inString) {
85
+ output += char;
86
+ if (escaping) escaping = false;
87
+ else if (char === "\\") escaping = true;
88
+ else if (char === "\"") inString = false;
89
+ index += 1;
90
+ continue;
91
+ }
92
+ if (char === "\"") {
93
+ inString = true;
94
+ output += char;
95
+ index += 1;
96
+ continue;
97
+ }
98
+ if (char === "-" || isDigit(char)) {
99
+ const numberToken = readJsonNumber(input, index);
100
+ if (numberToken) {
101
+ output += shouldParseAsDecimal(numberToken) ? `{"${DECIMAL_MARKER}":"${numberToken}"}` : numberToken;
102
+ index += numberToken.length;
103
+ continue;
104
+ }
105
+ }
106
+ output += char;
107
+ index += 1;
108
+ }
109
+ return output;
110
+ }
111
+ function readJsonNumber(input, start) {
112
+ let index = start;
113
+ if (input[index] === "-") index += 1;
114
+ if (input[index] === "0") index += 1;
115
+ else if (isDigitOneToNine(input[index])) {
116
+ index += 1;
117
+ while (isDigit(input[index])) index += 1;
118
+ } else return;
119
+ if (input[index] === ".") {
120
+ index += 1;
121
+ if (!isDigit(input[index])) return;
122
+ while (isDigit(input[index])) index += 1;
123
+ }
124
+ if (input[index] === "e" || input[index] === "E") {
125
+ index += 1;
126
+ if (input[index] === "+" || input[index] === "-") index += 1;
127
+ if (!isDigit(input[index])) return;
128
+ while (isDigit(input[index])) index += 1;
129
+ }
130
+ return input.slice(start, index);
131
+ }
132
+ function shouldParseAsDecimal(numberToken) {
133
+ if (/[.e]/i.test(numberToken)) return true;
134
+ const decimal = new Decimal(numberToken);
135
+ return !decimal.isInteger() || decimal.abs().greaterThan(Number.MAX_SAFE_INTEGER);
136
+ }
137
+ function getDecimalMarkerValue(value) {
138
+ if (Object.keys(value).length !== 1) return;
139
+ const decimalValue = value[DECIMAL_MARKER];
140
+ return typeof decimalValue === "string" ? decimalValue : void 0;
141
+ }
142
+ function isDigit(value) {
143
+ return value !== void 0 && value >= "0" && value <= "9";
144
+ }
145
+ function isDigitOneToNine(value) {
146
+ return value !== void 0 && value >= "1" && value <= "9";
147
+ }
148
+ //#endregion
149
+ //#region src/path.ts
150
+ /**
151
+ * 构建子节点的路径字符串。
152
+ *
153
+ * @param parentPath - 父节点的路径
154
+ * @param childKey - 子节点的键或索引
155
+ * @param parentIsArray - 父节点是否为数组
156
+ * @returns 子节点的完整路径
157
+ */
158
+ function buildChildPath(parentPath, childKey, parentIsArray) {
159
+ if (parentIsArray) {
160
+ const segment = `[${childKey}]`;
161
+ return parentPath ? `${parentPath}${segment}` : segment;
162
+ }
163
+ return parentPath ? `${parentPath}.${childKey}` : childKey;
164
+ }
165
+ //#endregion
166
+ //#region src/query.ts
167
+ /**
168
+ * 根据节点标识符获取对应的节点实例。
169
+ *
170
+ * @param document - JSON 文档实例
171
+ * @param id - 节点标识符
172
+ * @returns 如果找到对应节点则返回,否则返回 undefined
173
+ */
174
+ function getNode(document, id) {
175
+ return document.nodes.find((node) => node.id === id);
176
+ }
177
+ /**
178
+ * 根据节点标识符获取其父节点。
179
+ *
180
+ * @param document - JSON 文档实例
181
+ * @param id - 节点标识符
182
+ * @returns 如果存在父节点则返回,否则返回 undefined(如根节点或未找到)
183
+ */
184
+ function getParent(document, id) {
185
+ const node = getNode(document, id);
186
+ if (!node || node.parent === null) return;
187
+ return getNode(document, node.parent);
188
+ }
189
+ /**
190
+ * 根据节点标识符获取其所有的直接子节点。
191
+ *
192
+ * @param document - JSON 文档实例
193
+ * @param id - 节点标识符
194
+ * @returns 子节点列表数组
195
+ */
196
+ function getChildren(document, id) {
197
+ return document.nodes.filter((node) => node.parent === id);
198
+ }
199
+ /**
200
+ * 根据节点标识符获取其所有的后代节点(按前序遍历顺序)。
201
+ *
202
+ * @param document - JSON 文档实例
203
+ * @param id - 节点标识符
204
+ * @returns 所有后代节点的数组
205
+ */
206
+ function getDescendants(document, id) {
207
+ const startIndex = document.nodes.findIndex((node) => node.id === id);
208
+ if (startIndex === -1) return [];
209
+ const current = document.nodes[startIndex];
210
+ const descendants = [];
211
+ for (let index = startIndex + 1; index < document.nodes.length; index += 1) {
212
+ const candidate = document.nodes[index];
213
+ if (candidate.depth <= current.depth) break;
214
+ descendants.push(candidate);
215
+ }
216
+ return descendants;
217
+ }
218
+ /**
219
+ * 根据节点标识符获取该节点在文档中的路径。
220
+ *
221
+ * @param document - JSON 文档实例
222
+ * @param id - 节点标识符
223
+ * @returns 节点的路径字符串,未找到时返回 undefined
224
+ */
225
+ function getPath(document, id) {
226
+ return getNode(document, id)?.path;
227
+ }
228
+ /**
229
+ * 根据路径字符串精确查找对应的节点。
230
+ *
231
+ * @param document - JSON 文档实例
232
+ * @param path - 节点路径字符串
233
+ * @returns 找到的节点实例,未找到时返回 undefined
234
+ */
235
+ function findByPath(document, path) {
236
+ return document.nodes.find((node) => node.path === path);
237
+ }
238
+ /**
239
+ * 根据查询字符串在文档中模糊搜索匹配的节点。
240
+ * 匹配范围包括:节点的键名、路径、类型名称以及叶子节点的值。
241
+ * 搜索忽略大小写。
242
+ *
243
+ * @param document - JSON 文档实例
244
+ * @param query - 查询字符串
245
+ * @returns 匹配到的节点列表
246
+ */
247
+ function search(document, query) {
248
+ const normalized = query.trim().toLowerCase();
249
+ if (!normalized) return [...document.nodes];
250
+ return document.nodes.filter((node) => {
251
+ return [
252
+ node.key,
253
+ node.path,
254
+ node.type,
255
+ node.leaf ? stringifyLeafValue(node.value) : void 0
256
+ ].some((segment) => segment?.toLowerCase().includes(normalized) === true);
257
+ });
258
+ }
259
+ function stringifyLeafValue(value) {
260
+ if (value === void 0 || value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value === void 0 ? void 0 : String(value);
261
+ return JSON.stringify(value);
262
+ }
263
+ //#endregion
264
+ //#region src/state.ts
265
+ /**
266
+ * 创建 JSON 文档的内部状态对象,用于管理展开路径和可见节点。
267
+ *
268
+ * @param document - JSON 文档实例
269
+ * @param expandedPaths - 初始展开的节点路径集合
270
+ * @returns 初始化后的内部状态对象
271
+ */
272
+ function createDocumentState(document, expandedPaths = []) {
273
+ const state = {
274
+ expandedPaths: new Set(expandedPaths),
275
+ visibleNodes: [],
276
+ toggleExpanded(id, expanded) {
277
+ const node = getNode(document, id);
278
+ if (!node?.expandable) return document;
279
+ const nextExpanded = expanded ?? !node.expanded;
280
+ if (node.expanded === nextExpanded) return document;
281
+ node.expanded = nextExpanded;
282
+ if (nextExpanded) state.expandedPaths.add(node.path);
283
+ else state.expandedPaths.delete(node.path);
284
+ state.syncVisibleNodes(document.nodes);
285
+ return document;
286
+ },
287
+ syncVisibleNodes(nodes) {
288
+ state.visibleNodes = getVisibleNodes(nodes);
289
+ return state.visibleNodes;
290
+ }
291
+ };
292
+ return state;
293
+ }
294
+ /**
295
+ * 根据所有节点列表和它们的展开状态,计算出当前可见的节点列表。
296
+ * 折叠状态下的子节点会被跳过。
297
+ *
298
+ * @param nodes - 文档中所有节点的列表(按前序遍历顺序)
299
+ * @returns 当前可见节点的列表
300
+ */
301
+ function getVisibleNodes(nodes) {
302
+ const visibleNodes = [];
303
+ for (let index = 0; index < nodes.length; index += 1) {
304
+ const node = nodes[index];
305
+ visibleNodes.push(node);
306
+ if (node.expandable && !node.expanded) index += node.descendants;
307
+ }
308
+ return visibleNodes;
309
+ }
310
+ //#endregion
311
+ //#region src/document.ts
312
+ /**
313
+ * 创建并初始化一个 JSON 文档实例。
314
+ * 将原始输入数据转换为包含树形结构信息、节点标识和交互状态的文档对象。
315
+ *
316
+ * @param input - 原始的 JSON 输入或现有的 JSON 文档实例
317
+ * @param options - 创建文档时的配置选项
318
+ * @returns 初始化后的完整 JSON 文档实例
319
+ */
320
+ function createDocument(input, options = {}) {
321
+ const normalizedInput = isJsonDocument(input) ? input.raw : toJsonRoot(input);
322
+ const resolvedOptions = resolveCreateDocumentOptions(input, options);
323
+ const raw = resolvedOptions.cloneRaw === false ? normalizedInput : structuredClone(normalizedInput);
324
+ const identityTree = createIdentityTree(raw, resolvedOptions.identityTree);
325
+ const state = {
326
+ nextOrder: 0,
327
+ expandedPaths: new Set(resolvedOptions.expandedPaths),
328
+ hasExplicitExpandedPaths: resolvedOptions.hasExplicitExpandedPaths,
329
+ defaultExpandedAll: resolvedOptions.defaultExpandedAll,
330
+ defaultExpandedDepth: resolvedOptions.defaultExpandedDepth
331
+ };
332
+ const nodes = [];
333
+ walkValue(raw, identityTree, {
334
+ depth: 0,
335
+ key: void 0,
336
+ parent: null,
337
+ path: "",
338
+ nodes,
339
+ state
340
+ });
341
+ const document = {
342
+ type: Array.isArray(raw) ? "array-json" : "object-json",
343
+ raw,
344
+ nodes,
345
+ state: void 0
346
+ };
347
+ const documentState = createDocumentState(document, state.expandedPaths);
348
+ Object.defineProperty(document, DOCUMENT_IDENTITY, {
349
+ value: identityTree,
350
+ enumerable: false,
351
+ configurable: false,
352
+ writable: false
353
+ });
354
+ Object.defineProperty(document, "state", {
355
+ value: documentState,
356
+ enumerable: true,
357
+ configurable: false,
358
+ writable: false
359
+ });
360
+ Object.defineProperties(document, {
361
+ visibleNodes: {
362
+ get() {
363
+ return documentState.visibleNodes;
364
+ },
365
+ enumerable: false,
366
+ configurable: false
367
+ },
368
+ toggleExpanded: {
369
+ value(id, expanded) {
370
+ return documentState.toggleExpanded(id, expanded);
371
+ },
372
+ enumerable: false,
373
+ configurable: false,
374
+ writable: false
375
+ },
376
+ stringify: {
377
+ value(space = 2) {
378
+ return stringify(document, space);
379
+ },
380
+ enumerable: false,
381
+ configurable: false,
382
+ writable: false
383
+ },
384
+ getNode: {
385
+ value(id) {
386
+ return getNode(document, id);
387
+ },
388
+ enumerable: false,
389
+ configurable: false,
390
+ writable: false
391
+ },
392
+ getParent: {
393
+ value(id) {
394
+ return getParent(document, id);
395
+ },
396
+ enumerable: false,
397
+ configurable: false,
398
+ writable: false
399
+ },
400
+ getChildren: {
401
+ value(id) {
402
+ return getChildren(document, id);
403
+ },
404
+ enumerable: false,
405
+ configurable: false,
406
+ writable: false
407
+ },
408
+ getDescendants: {
409
+ value(id) {
410
+ return getDescendants(document, id);
411
+ },
412
+ enumerable: false,
413
+ configurable: false,
414
+ writable: false
415
+ },
416
+ getPath: {
417
+ value(id) {
418
+ return getPath(document, id);
419
+ },
420
+ enumerable: false,
421
+ configurable: false,
422
+ writable: false
423
+ },
424
+ findByPath: {
425
+ value(path) {
426
+ return findByPath(document, path);
427
+ },
428
+ enumerable: false,
429
+ configurable: false,
430
+ writable: false
431
+ },
432
+ search: {
433
+ value(query) {
434
+ return search(document, query);
435
+ },
436
+ enumerable: false,
437
+ configurable: false,
438
+ writable: false
439
+ }
440
+ });
441
+ documentState.syncVisibleNodes(document.nodes);
442
+ return document;
443
+ }
444
+ /**
445
+ * 获取 JSON 文档的标识树(Identity Tree)。
446
+ * 标识树用于在重新生成文档时保持节点的唯一标识,从而维持状态(如展开/折叠状态)的一致性。
447
+ *
448
+ * @param document - 目标 JSON 文档实例
449
+ * @returns 对应的节点标识树
450
+ * @throws 当文档中缺少标识树时抛出错误
451
+ */
452
+ function getDocumentIdentityTree(document) {
453
+ const identityTree = document[DOCUMENT_IDENTITY];
454
+ if (!identityTree) throw new Error("Document identity tree is missing.");
455
+ return identityTree;
456
+ }
457
+ /**
458
+ * 将 JSON 文档序列化为 JSON 字符串。
459
+ * 会自动处理 Decimal 类型的数值。
460
+ *
461
+ * @param document - 要序列化的 JSON 文档实例
462
+ * @param space - 用于缩进的空格数,默认为 2
463
+ * @returns 序列化后的 JSON 字符串
464
+ */
465
+ function stringify(document, space = 2) {
466
+ return JSON.stringify(document.raw, (_key, value) => Decimal.isDecimal(value) ? value.toString() : value, space);
467
+ }
468
+ /**
469
+ * 判断给定的值是否为有效的 JSON 文档实例。
470
+ *
471
+ * @param value - 要检查的值
472
+ * @returns 如果是有效的 JsonDocument 返回 true,否则返回 false
473
+ */
474
+ function isJsonDocument(value) {
475
+ return typeof value === "object" && value !== null && "raw" in value && "nodes" in value && "state" in value && "type" in value;
476
+ }
477
+ function walkValue(value, identity, context) {
478
+ const { depth, key, parent, path, nodes, state } = context;
479
+ const type = getNodeType(value);
480
+ const container = isContainerValue(value);
481
+ const childrenEntries = container ? getContainerEntries(value, identity) : [];
482
+ const children = childrenEntries.length;
483
+ const expandable = container && children > 0;
484
+ const expanded = shouldExpandNode(path, depth, expandable, state);
485
+ const node = {
486
+ id: identity.id,
487
+ key,
488
+ value: container ? void 0 : value,
489
+ children,
490
+ parent,
491
+ depth,
492
+ type,
493
+ path,
494
+ expandable,
495
+ order: state.nextOrder,
496
+ expanded,
497
+ leaf: !container,
498
+ size: 1,
499
+ descendants: 0
500
+ };
501
+ if (expanded) state.expandedPaths.add(path);
502
+ nodes.push(node);
503
+ state.nextOrder += 1;
504
+ let subtreeSize = 1;
505
+ let hasExpandedDescendant = false;
506
+ for (const [childKey, childValue, childIdentity] of childrenEntries) {
507
+ const childResult = walkValue(childValue, childIdentity, {
508
+ depth: depth + 1,
509
+ key: childKey,
510
+ parent: identity.id,
511
+ path: buildChildPath(path, childKey, Array.isArray(value)),
512
+ nodes,
513
+ state
514
+ });
515
+ subtreeSize += childResult.size;
516
+ if (childResult.expanded) hasExpandedDescendant = true;
517
+ }
518
+ if (hasExpandedDescendant && expandable && !node.expanded) {
519
+ node.expanded = true;
520
+ state.expandedPaths.add(path);
521
+ }
522
+ node.size = subtreeSize;
523
+ node.descendants = subtreeSize - 1;
524
+ return {
525
+ size: subtreeSize,
526
+ expanded: node.expanded
527
+ };
528
+ }
529
+ function createIdentityTree(value, existing) {
530
+ const id = existing?.id ?? createJsonNodeId();
531
+ if (Array.isArray(value)) {
532
+ const existingChildren = Array.isArray(existing?.children) ? existing.children : [];
533
+ return {
534
+ id,
535
+ children: value.map((item, index) => createIdentityTree(item, existingChildren[index]))
536
+ };
537
+ }
538
+ if (isJsonObject(value)) {
539
+ const existingChildren = isIdentityRecord(existing?.children) ? existing.children : {};
540
+ const children = {};
541
+ for (const [key, item] of Object.entries(value)) children[key] = createIdentityTree(item, existingChildren[key]);
542
+ return {
543
+ id,
544
+ children
545
+ };
546
+ }
547
+ return { id };
548
+ }
549
+ function resolveCreateDocumentOptions(input, options) {
550
+ const defaultExpandedRoot = options.defaultExpandedRoot ?? true;
551
+ let defaultExpandedDepth = options.defaultExpandedDepth;
552
+ if (defaultExpandedDepth === void 0 && defaultExpandedRoot && !options.defaultExpandedAll) {
553
+ const raw = isJsonDocument(input) ? input.raw : input;
554
+ defaultExpandedDepth = Array.isArray(raw) ? 1 : 0;
555
+ }
556
+ const defaultExpandedAll = options.defaultExpandedAll ?? false;
557
+ const cloneRaw = options.cloneRaw ?? false;
558
+ const identityTree = options.identityTree ?? (isJsonDocument(input) ? getDocumentIdentityTree(input) : void 0);
559
+ const previousExpandedPaths = isJsonDocument(input) ? Array.from(input.state.expandedPaths) : void 0;
560
+ const hasExplicitExpandedPaths = options.expandedPaths !== void 0 || defaultExpandedDepth === void 0 && !defaultExpandedAll && previousExpandedPaths !== void 0;
561
+ return {
562
+ hasExplicitExpandedPaths,
563
+ expandedPaths: options.expandedPaths !== void 0 ? new Set(options.expandedPaths) : hasExplicitExpandedPaths ? new Set(previousExpandedPaths) : /* @__PURE__ */ new Set(),
564
+ defaultExpandedAll,
565
+ defaultExpandedDepth,
566
+ identityTree,
567
+ cloneRaw
568
+ };
569
+ }
570
+ function shouldExpandNode(path, depth, expandable, state) {
571
+ if (!expandable) return false;
572
+ if (state.hasExplicitExpandedPaths && state.expandedPaths.has(path)) return true;
573
+ if (state.defaultExpandedDepth !== void 0) return state.defaultExpandedDepth === true ? true : depth <= state.defaultExpandedDepth;
574
+ return state.defaultExpandedAll;
575
+ }
576
+ function getContainerEntries(value, identity) {
577
+ if (Array.isArray(value)) {
578
+ const children = Array.isArray(identity.children) ? identity.children : [];
579
+ return value.map((item, index) => {
580
+ const childIdentity = children[index];
581
+ if (!childIdentity) throw new Error("Array identity tree is out of sync.");
582
+ return [
583
+ String(index),
584
+ item,
585
+ childIdentity
586
+ ];
587
+ });
588
+ }
589
+ if (!isIdentityRecord(identity.children)) throw new Error("Object identity tree is out of sync.");
590
+ const children = identity.children;
591
+ return Object.entries(value).map(([key, item]) => {
592
+ const childIdentity = children[key];
593
+ if (!childIdentity) throw new Error(`Object identity tree is missing key "${key}".`);
594
+ return [
595
+ key,
596
+ item,
597
+ childIdentity
598
+ ];
599
+ });
600
+ }
601
+ function getNodeType(value) {
602
+ if (value === null) return "null";
603
+ if (Decimal.isDecimal(value)) return "number";
604
+ if (Array.isArray(value)) return "array";
605
+ if (typeof value === "object") return "object";
606
+ switch (typeof value) {
607
+ case "string": return "string";
608
+ case "number": return "number";
609
+ case "boolean": return "boolean";
610
+ default: throw new TypeError("Unsupported JSON value type.");
611
+ }
612
+ }
613
+ function isIdentityRecord(children) {
614
+ return children !== void 0 && !Array.isArray(children);
615
+ }
616
+ //#endregion
617
+ export { createDocument, createJsonNodeId, findByPath, getChildren, getDescendants, getDocumentIdentityTree, getNode, getParent, getPath, getVisibleNodes, isContainerValue, isJsonDocument, isJsonObject, search, stringify, toJsonValue };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@jsonup/core",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "description": "The core JSON parser, state manager, and document builder for the jsonup ecosystem.",
6
+ "author": {
7
+ "name": "Michael Cocova",
8
+ "email": "michael.cocova@gmail.com"
9
+ },
10
+ "license": "MIT",
11
+ "homepage": "https://github.com/michaelcocova/jsonup#readme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/michaelcocova/jsonup.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/michaelcocova/jsonup/issues"
18
+ },
19
+ "exports": {
20
+ ".": "./dist/index.mjs",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "dependencies": {
27
+ "decimal.js": "10.6.0",
28
+ "nanoid": "5.1.15"
29
+ },
30
+ "devDependencies": {
31
+ "tsdown": "^0.22.0",
32
+ "typescript": "^6.0.3"
33
+ },
34
+ "scripts": {
35
+ "build": "tsdown",
36
+ "dev": "tsdown --watch",
37
+ "test": "vitest run"
38
+ }
39
+ }