@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 +21 -0
- package/README.md +43 -0
- package/dist/index.d.mts +316 -0
- package/dist/index.mjs +617 -0
- package/package.json +39 -0
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)
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|