@qezor/structkit 1.0.0 → 1.0.2
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/README.md +81 -29
- package/index.d.ts +100 -12
- package/index.js +6 -21
- package/index.mjs +38 -5
- package/lib/array.js +139 -0
- package/lib/deep.js +435 -0
- package/lib/insert.js +62 -0
- package/lib/object.js +44 -0
- package/lib/range.js +55 -0
- package/lib/shared.js +64 -0
- package/lib/string.js +195 -0
- package/package.json +4 -11
- package/test.js +125 -6
- package/bridge.d.ts +0 -14
- package/bridge.js +0 -3
- package/bridge.mjs +0 -11
- package/lib/bridge.js +0 -49
package/lib/deep.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isObjectLike,
|
|
5
|
+
isPlainObject,
|
|
6
|
+
tokenizePath,
|
|
7
|
+
getValueByPath,
|
|
8
|
+
cloneDeep,
|
|
9
|
+
stringifyPathTokens,
|
|
10
|
+
getValueByTokens,
|
|
11
|
+
setValueByTokens,
|
|
12
|
+
deleteByTokens,
|
|
13
|
+
} = require("./shared.js")
|
|
14
|
+
const { isEqual } = require("./compare.js")
|
|
15
|
+
const { normalizeCount } = require("./range.js")
|
|
16
|
+
const { insertValue } = require("./insert.js")
|
|
17
|
+
|
|
18
|
+
function isTraversable(value) {
|
|
19
|
+
return Array.isArray(value) || isPlainObject(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getNodeType(value) {
|
|
23
|
+
if (value === null) return "null"
|
|
24
|
+
if (Array.isArray(value)) return "array"
|
|
25
|
+
if (value instanceof Date) return "date"
|
|
26
|
+
if (value instanceof RegExp) return "regexp"
|
|
27
|
+
if (value instanceof Map) return "map"
|
|
28
|
+
if (value instanceof Set) return "set"
|
|
29
|
+
return typeof value === "object" ? "object" : typeof value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toPositiveLimit(value, fallback) {
|
|
33
|
+
if (value == null) return fallback
|
|
34
|
+
const next = normalizeCount(value, fallback)
|
|
35
|
+
return next === 0 ? 0 : next
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeDeepOptions(options = {}) {
|
|
39
|
+
return {
|
|
40
|
+
fromPath: options.fromPath ?? options.scope ?? [],
|
|
41
|
+
minDepth: normalizeCount(options.minDepth, 0),
|
|
42
|
+
maxDepth: options.maxDepth == null ? Number.POSITIVE_INFINITY : normalizeCount(options.maxDepth, 0),
|
|
43
|
+
includeRoot: options.includeRoot !== false,
|
|
44
|
+
includeContainers: options.includeContainers !== false,
|
|
45
|
+
includeLeaves: options.includeLeaves !== false,
|
|
46
|
+
order: options.order === "bfs" ? "bfs" : "dfs",
|
|
47
|
+
greedy: options.greedy ?? false,
|
|
48
|
+
limit: options.limit == null ? Number.POSITIVE_INFINITY : toPositiveLimit(options.limit, Number.POSITIVE_INFINITY),
|
|
49
|
+
maxNodes: options.maxNodes == null ? 100000 : toPositiveLimit(options.maxNodes, 100000),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createDeepMatcher(predicate) {
|
|
54
|
+
if (typeof predicate === "function") return predicate
|
|
55
|
+
|
|
56
|
+
if (typeof predicate === "string") {
|
|
57
|
+
const expected = predicate
|
|
58
|
+
return (entry) => entry.path === expected || entry.key === expected
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (predicate && typeof predicate === "object" && !Array.isArray(predicate)) {
|
|
62
|
+
const query = predicate
|
|
63
|
+
return (entry) => {
|
|
64
|
+
if (query.path != null && entry.path !== String(query.path)) return false
|
|
65
|
+
if (query.key != null && entry.key !== String(query.key)) return false
|
|
66
|
+
if (query.type != null && entry.type !== String(query.type)) return false
|
|
67
|
+
if (query.depth != null && entry.depth !== Number(query.depth)) return false
|
|
68
|
+
if (query.pathDepth != null && entry.pathDepth !== Number(query.pathDepth)) return false
|
|
69
|
+
if (query.absoluteDepth != null && entry.absoluteDepth !== Number(query.absoluteDepth)) return false
|
|
70
|
+
if (query.minDepth != null && entry.depth < Number(query.minDepth)) return false
|
|
71
|
+
if (query.maxDepth != null && entry.depth > Number(query.maxDepth)) return false
|
|
72
|
+
if (query.minPathDepth != null && entry.pathDepth < Number(query.minPathDepth)) return false
|
|
73
|
+
if (query.maxPathDepth != null && entry.pathDepth > Number(query.maxPathDepth)) return false
|
|
74
|
+
if (Object.prototype.hasOwnProperty.call(query, "value") && !isEqual(entry.value, query.value)) return false
|
|
75
|
+
|
|
76
|
+
if (query.includes != null) {
|
|
77
|
+
const needle = String(query.includes).toLowerCase()
|
|
78
|
+
const keyHit = typeof entry.key === "string" && entry.key.toLowerCase().includes(needle)
|
|
79
|
+
const pathHit = entry.path.toLowerCase().includes(needle)
|
|
80
|
+
const valueHit = typeof entry.value === "string" && entry.value.toLowerCase().includes(needle)
|
|
81
|
+
if (!keyHit && !pathHit && !valueHit) return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (predicate === undefined) return () => true
|
|
89
|
+
return (entry) => Object.is(entry.value, predicate)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createEntry(frame) {
|
|
93
|
+
const value = frame.value
|
|
94
|
+
const isContainer = isTraversable(value)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
value,
|
|
98
|
+
parent: frame.parent,
|
|
99
|
+
key: frame.key == null ? null : String(frame.key),
|
|
100
|
+
tokens: frame.tokens.slice(),
|
|
101
|
+
path: stringifyPathTokens(frame.tokens),
|
|
102
|
+
depth: frame.depth,
|
|
103
|
+
pathDepth: frame.pathDepth,
|
|
104
|
+
absoluteDepth: frame.absoluteDepth,
|
|
105
|
+
type: getNodeType(value),
|
|
106
|
+
isContainer,
|
|
107
|
+
isLeaf: !isContainer,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pushChildFrames(stack, frame, options, enqueue) {
|
|
112
|
+
const parentValue = frame.value
|
|
113
|
+
const isArrayParent = Array.isArray(parentValue)
|
|
114
|
+
|
|
115
|
+
const pushChild = (key, childValue) => {
|
|
116
|
+
const childIsContainer = isTraversable(childValue)
|
|
117
|
+
const childDepth = childIsContainer ? frame.depth + 1 : frame.depth
|
|
118
|
+
if (childDepth > options.maxDepth) return
|
|
119
|
+
|
|
120
|
+
enqueue({
|
|
121
|
+
value: childValue,
|
|
122
|
+
parent: parentValue,
|
|
123
|
+
key,
|
|
124
|
+
tokens: frame.tokens.concat(key),
|
|
125
|
+
depth: childDepth,
|
|
126
|
+
pathDepth: frame.pathDepth + 1,
|
|
127
|
+
absoluteDepth: frame.absoluteDepth + 1,
|
|
128
|
+
parentIsArray: isArrayParent,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Array.isArray(parentValue)) {
|
|
133
|
+
stack(parentValue.length, (index) => pushChild(index, parentValue[index]))
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const keys = Object.keys(parentValue)
|
|
138
|
+
stack(keys.length, (index) => {
|
|
139
|
+
const key = keys[index]
|
|
140
|
+
pushChild(key, parentValue[key])
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function* iterateDepthFirst(root, options) {
|
|
145
|
+
const scopeTokens = tokenizePath(options.fromPath)
|
|
146
|
+
const scopeValue = scopeTokens.length ? getValueByPath(root, scopeTokens) : root
|
|
147
|
+
|
|
148
|
+
if (scopeTokens.length && scopeValue === undefined) return
|
|
149
|
+
|
|
150
|
+
const initialParent = scopeTokens.length ? getValueByPath(root, scopeTokens.slice(0, -1)) : undefined
|
|
151
|
+
const initialKey = scopeTokens.length ? scopeTokens[scopeTokens.length - 1] : null
|
|
152
|
+
const stack = [{
|
|
153
|
+
value: scopeValue,
|
|
154
|
+
parent: initialParent,
|
|
155
|
+
key: initialKey,
|
|
156
|
+
tokens: scopeTokens,
|
|
157
|
+
depth: 0,
|
|
158
|
+
pathDepth: 0,
|
|
159
|
+
absoluteDepth: scopeTokens.length,
|
|
160
|
+
}]
|
|
161
|
+
|
|
162
|
+
let visited = 0
|
|
163
|
+
let yielded = 0
|
|
164
|
+
|
|
165
|
+
while (stack.length) {
|
|
166
|
+
const frame = stack.pop()
|
|
167
|
+
visited += 1
|
|
168
|
+
|
|
169
|
+
if (visited > options.maxNodes) {
|
|
170
|
+
const error = new Error(`deep traversal exceeded maxNodes=${options.maxNodes}`)
|
|
171
|
+
error.code = "EMAXNODES"
|
|
172
|
+
throw error
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const entry = createEntry(frame)
|
|
176
|
+
const shouldYield = (frame.depth > 0 || options.includeRoot)
|
|
177
|
+
&& frame.depth >= options.minDepth
|
|
178
|
+
&& ((entry.isContainer && options.includeContainers) || (entry.isLeaf && options.includeLeaves))
|
|
179
|
+
|
|
180
|
+
if (shouldYield) {
|
|
181
|
+
yield entry
|
|
182
|
+
yielded += 1
|
|
183
|
+
if (yielded >= options.limit) return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!entry.isContainer) continue
|
|
187
|
+
|
|
188
|
+
pushChildFrames(
|
|
189
|
+
(length, iteratee) => {
|
|
190
|
+
for (let index = length - 1; index >= 0; index -= 1) {
|
|
191
|
+
iteratee(index)
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
frame,
|
|
195
|
+
options,
|
|
196
|
+
(nextFrame) => stack.push(nextFrame)
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function* iterateBreadthFirst(root, options) {
|
|
202
|
+
const scopeTokens = tokenizePath(options.fromPath)
|
|
203
|
+
const scopeValue = scopeTokens.length ? getValueByPath(root, scopeTokens) : root
|
|
204
|
+
|
|
205
|
+
if (scopeTokens.length && scopeValue === undefined) return
|
|
206
|
+
|
|
207
|
+
const queue = [{
|
|
208
|
+
value: scopeValue,
|
|
209
|
+
parent: scopeTokens.length ? getValueByPath(root, scopeTokens.slice(0, -1)) : undefined,
|
|
210
|
+
key: scopeTokens.length ? scopeTokens[scopeTokens.length - 1] : null,
|
|
211
|
+
tokens: scopeTokens,
|
|
212
|
+
depth: 0,
|
|
213
|
+
pathDepth: 0,
|
|
214
|
+
absoluteDepth: scopeTokens.length,
|
|
215
|
+
}]
|
|
216
|
+
|
|
217
|
+
let cursor = 0
|
|
218
|
+
let visited = 0
|
|
219
|
+
let yielded = 0
|
|
220
|
+
|
|
221
|
+
while (cursor < queue.length) {
|
|
222
|
+
const frame = queue[cursor]
|
|
223
|
+
cursor += 1
|
|
224
|
+
visited += 1
|
|
225
|
+
|
|
226
|
+
if (visited > options.maxNodes) {
|
|
227
|
+
const error = new Error(`deep traversal exceeded maxNodes=${options.maxNodes}`)
|
|
228
|
+
error.code = "EMAXNODES"
|
|
229
|
+
throw error
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const entry = createEntry(frame)
|
|
233
|
+
const shouldYield = (frame.depth > 0 || options.includeRoot)
|
|
234
|
+
&& frame.depth >= options.minDepth
|
|
235
|
+
&& ((entry.isContainer && options.includeContainers) || (entry.isLeaf && options.includeLeaves))
|
|
236
|
+
|
|
237
|
+
if (shouldYield) {
|
|
238
|
+
yield entry
|
|
239
|
+
yielded += 1
|
|
240
|
+
if (yielded >= options.limit) return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!entry.isContainer) continue
|
|
244
|
+
|
|
245
|
+
pushChildFrames(
|
|
246
|
+
(length, iteratee) => {
|
|
247
|
+
for (let index = 0; index < length; index += 1) {
|
|
248
|
+
iteratee(index)
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
frame,
|
|
252
|
+
options,
|
|
253
|
+
(nextFrame) => queue.push(nextFrame)
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function iterateDeep(value, options = {}) {
|
|
259
|
+
const normalized = normalizeDeepOptions(options)
|
|
260
|
+
return normalized.order === "bfs"
|
|
261
|
+
? iterateBreadthFirst(value, normalized)
|
|
262
|
+
: iterateDepthFirst(value, normalized)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isBetterGreedyMatch(current, next, mode) {
|
|
266
|
+
if (!current) return true
|
|
267
|
+
|
|
268
|
+
if (mode === true || mode === "last") return true
|
|
269
|
+
|
|
270
|
+
if (mode === "deepest") {
|
|
271
|
+
if (next.depth !== current.depth) return next.depth > current.depth
|
|
272
|
+
if (next.pathDepth !== current.pathDepth) return next.pathDepth > current.pathDepth
|
|
273
|
+
return next.path >= current.path
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (mode === "path" || mode === "deepest-path") {
|
|
277
|
+
if (next.pathDepth !== current.pathDepth) return next.pathDepth > current.pathDepth
|
|
278
|
+
if (next.depth !== current.depth) return next.depth > current.depth
|
|
279
|
+
return next.path >= current.path
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function findDeep(value, predicate, options = {}) {
|
|
286
|
+
const normalized = normalizeDeepOptions(options)
|
|
287
|
+
const match = createDeepMatcher(predicate)
|
|
288
|
+
let selected
|
|
289
|
+
|
|
290
|
+
for (const entry of iterateDeep(value, options)) {
|
|
291
|
+
if (!match(entry)) continue
|
|
292
|
+
if (!normalized.greedy) return entry
|
|
293
|
+
if (isBetterGreedyMatch(selected, entry, normalized.greedy)) {
|
|
294
|
+
selected = entry
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return selected
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function filterDeep(value, predicate, options = {}) {
|
|
302
|
+
const match = createDeepMatcher(predicate)
|
|
303
|
+
const output = []
|
|
304
|
+
for (const entry of iterateDeep(value, options)) {
|
|
305
|
+
if (!match(entry)) continue
|
|
306
|
+
output.push(entry)
|
|
307
|
+
}
|
|
308
|
+
return output
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function findPaths(value, predicate, options = {}) {
|
|
312
|
+
const output = []
|
|
313
|
+
for (const entry of filterDeep(value, predicate, options)) {
|
|
314
|
+
output.push(entry.path)
|
|
315
|
+
}
|
|
316
|
+
return output
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function compareTokenPathsDescending(left, right) {
|
|
320
|
+
if (left.tokens.length !== right.tokens.length) {
|
|
321
|
+
return right.tokens.length - left.tokens.length
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const length = Math.max(left.tokens.length, right.tokens.length)
|
|
325
|
+
for (let index = 0; index < length; index += 1) {
|
|
326
|
+
const leftToken = left.tokens[index]
|
|
327
|
+
const rightToken = right.tokens[index]
|
|
328
|
+
if (leftToken === rightToken) continue
|
|
329
|
+
|
|
330
|
+
if (typeof leftToken === "number" && typeof rightToken === "number") {
|
|
331
|
+
return rightToken - leftToken
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return String(rightToken).localeCompare(String(leftToken))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return 0
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function mapDeep(value, predicate, updater, options = {}) {
|
|
341
|
+
if (typeof updater !== "function") {
|
|
342
|
+
throw new TypeError("mapDeep requires an updater function")
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let output = cloneDeep(value)
|
|
346
|
+
const match = createDeepMatcher(predicate)
|
|
347
|
+
const matches = []
|
|
348
|
+
|
|
349
|
+
for (const entry of iterateDeep(output, options)) {
|
|
350
|
+
if (!match(entry)) continue
|
|
351
|
+
matches.push({
|
|
352
|
+
tokens: entry.tokens.slice(),
|
|
353
|
+
path: entry.path,
|
|
354
|
+
depth: entry.depth,
|
|
355
|
+
absoluteDepth: entry.absoluteDepth,
|
|
356
|
+
type: entry.type,
|
|
357
|
+
key: entry.key,
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
matches.sort(compareTokenPathsDescending)
|
|
362
|
+
|
|
363
|
+
for (const matchEntry of matches) {
|
|
364
|
+
const current = getValueByTokens(output, matchEntry.tokens)
|
|
365
|
+
const next = updater(current, {
|
|
366
|
+
...matchEntry,
|
|
367
|
+
value: current,
|
|
368
|
+
})
|
|
369
|
+
output = setValueByTokens(output, matchEntry.tokens, next)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return output
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function removeDeep(value, predicate, options = {}) {
|
|
376
|
+
let output = cloneDeep(value)
|
|
377
|
+
const matches = filterDeep(output, predicate, options)
|
|
378
|
+
.map((entry) => ({
|
|
379
|
+
tokens: entry.tokens.slice(),
|
|
380
|
+
path: entry.path,
|
|
381
|
+
}))
|
|
382
|
+
|
|
383
|
+
matches.sort(compareTokenPathsDescending)
|
|
384
|
+
|
|
385
|
+
for (const match of matches) {
|
|
386
|
+
output = deleteByTokens(output, match.tokens)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return output
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function insertDeep(value, predicate, insertion, options = {}) {
|
|
393
|
+
let output = cloneDeep(value)
|
|
394
|
+
const match = createDeepMatcher(predicate)
|
|
395
|
+
const matches = []
|
|
396
|
+
|
|
397
|
+
for (const entry of iterateDeep(output, options)) {
|
|
398
|
+
if (!match(entry)) continue
|
|
399
|
+
matches.push({
|
|
400
|
+
tokens: entry.tokens.slice(),
|
|
401
|
+
path: entry.path,
|
|
402
|
+
depth: entry.depth,
|
|
403
|
+
pathDepth: entry.pathDepth,
|
|
404
|
+
absoluteDepth: entry.absoluteDepth,
|
|
405
|
+
type: entry.type,
|
|
406
|
+
key: entry.key,
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
matches.sort(compareTokenPathsDescending)
|
|
411
|
+
|
|
412
|
+
for (const matchEntry of matches) {
|
|
413
|
+
const current = getValueByTokens(output, matchEntry.tokens)
|
|
414
|
+
const resolvedInsertion = typeof insertion === "function"
|
|
415
|
+
? insertion(current, {
|
|
416
|
+
...matchEntry,
|
|
417
|
+
value: current,
|
|
418
|
+
})
|
|
419
|
+
: insertion
|
|
420
|
+
const next = insertValue(current, resolvedInsertion, options)
|
|
421
|
+
output = setValueByTokens(output, matchEntry.tokens, next)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return output
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = {
|
|
428
|
+
iterateDeep,
|
|
429
|
+
findDeep,
|
|
430
|
+
filterDeep,
|
|
431
|
+
findPaths,
|
|
432
|
+
mapDeep,
|
|
433
|
+
removeDeep,
|
|
434
|
+
insertDeep,
|
|
435
|
+
}
|
package/lib/insert.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const { insertText } = require("./string.js")
|
|
4
|
+
const { insertAt } = require("./array.js")
|
|
5
|
+
const { isPlainObject, cloneDeep } = require("./shared.js")
|
|
6
|
+
|
|
7
|
+
function cloneInsertion(value) {
|
|
8
|
+
if (value == null) return value
|
|
9
|
+
if (typeof value !== "object") return value
|
|
10
|
+
return cloneDeep(value)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getObjectInsertKey(options = {}) {
|
|
14
|
+
const key = options.key ?? options.property
|
|
15
|
+
if (key == null || key === "") return null
|
|
16
|
+
return String(key)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createMissingSeed(options = {}) {
|
|
20
|
+
if (options.kind === "array") return []
|
|
21
|
+
if (options.kind === "string") return ""
|
|
22
|
+
if (options.kind === "object") return {}
|
|
23
|
+
if (getObjectInsertKey(options)) return {}
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function insertValue(current, insertion, options = {}) {
|
|
28
|
+
let target = current
|
|
29
|
+
|
|
30
|
+
if (target === undefined && options.createMissing === true) {
|
|
31
|
+
target = createMissingSeed(options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(target)) {
|
|
35
|
+
return insertAt(target, options.at ?? options.index ?? target.length, cloneInsertion(insertion))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof target === "string") {
|
|
39
|
+
return insertText(target, insertion, options)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (isPlainObject(target)) {
|
|
43
|
+
const key = getObjectInsertKey(options)
|
|
44
|
+
if (!key) {
|
|
45
|
+
if (options.merge === true && isPlainObject(insertion)) {
|
|
46
|
+
return { ...target, ...cloneDeep(insertion) }
|
|
47
|
+
}
|
|
48
|
+
throw new TypeError("object insertion requires options.key or options.property")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...target,
|
|
53
|
+
[key]: cloneInsertion(insertion),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new TypeError("insertValue only supports arrays, strings, and plain objects")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
insertValue,
|
|
62
|
+
}
|
package/lib/object.js
CHANGED
|
@@ -4,11 +4,13 @@ const {
|
|
|
4
4
|
isObjectLike,
|
|
5
5
|
isPlainObject,
|
|
6
6
|
tokenizePath,
|
|
7
|
+
stringifyPathTokens,
|
|
7
8
|
getValueByPath,
|
|
8
9
|
createIteratee,
|
|
9
10
|
cloneShallow,
|
|
10
11
|
cloneDeep,
|
|
11
12
|
} = require("./shared.js")
|
|
13
|
+
const { insertValue } = require("./insert.js")
|
|
12
14
|
|
|
13
15
|
function get(value, path, defaultValue) {
|
|
14
16
|
const resolved = getValueByPath(value, path)
|
|
@@ -52,6 +54,46 @@ function set(target, path, nextValue) {
|
|
|
52
54
|
return target
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function update(target, path, updater, options = {}) {
|
|
58
|
+
if (typeof updater !== "function") {
|
|
59
|
+
throw new TypeError("update requires an updater function")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tokens = tokenizePath(path)
|
|
63
|
+
if (!tokens.length) {
|
|
64
|
+
return updater(target, {
|
|
65
|
+
value: target,
|
|
66
|
+
path: "",
|
|
67
|
+
tokens: [],
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isObjectLike(target) && !Array.isArray(target)) return target
|
|
72
|
+
|
|
73
|
+
const current = getValueByPath(target, tokens)
|
|
74
|
+
const next = updater(
|
|
75
|
+
current === undefined && Object.prototype.hasOwnProperty.call(options, "defaultValue")
|
|
76
|
+
? options.defaultValue
|
|
77
|
+
: current,
|
|
78
|
+
{
|
|
79
|
+
value: current,
|
|
80
|
+
path: stringifyPathTokens(tokens),
|
|
81
|
+
tokens: tokens.slice(),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return set(target, tokens, next)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function insertAtPath(target, path, insertion, options = {}) {
|
|
89
|
+
const tokens = tokenizePath(path)
|
|
90
|
+
const current = tokens.length ? getValueByPath(target, tokens) : target
|
|
91
|
+
const next = insertValue(current, insertion, options)
|
|
92
|
+
|
|
93
|
+
if (!tokens.length) return next
|
|
94
|
+
return set(target, tokens, next)
|
|
95
|
+
}
|
|
96
|
+
|
|
55
97
|
function unset(target, path) {
|
|
56
98
|
if (!isObjectLike(target) && !Array.isArray(target)) return false
|
|
57
99
|
|
|
@@ -190,6 +232,8 @@ module.exports = {
|
|
|
190
232
|
get,
|
|
191
233
|
has,
|
|
192
234
|
set,
|
|
235
|
+
update,
|
|
236
|
+
insertAtPath,
|
|
193
237
|
unset,
|
|
194
238
|
pick,
|
|
195
239
|
omit,
|
package/lib/range.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
function toFiniteInteger(value, fallback) {
|
|
4
|
+
const number = Number(value)
|
|
5
|
+
if (!Number.isFinite(number)) return fallback
|
|
6
|
+
return Math.trunc(number)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function clampIndex(length, value, fallback) {
|
|
10
|
+
const size = Math.max(0, toFiniteInteger(length, 0))
|
|
11
|
+
let index = toFiniteInteger(value, fallback)
|
|
12
|
+
if (index == null) index = fallback
|
|
13
|
+
if (index < 0) index = size + index
|
|
14
|
+
if (index < 0) return 0
|
|
15
|
+
if (index > size) return size
|
|
16
|
+
return index
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeRange(length, options = {}) {
|
|
20
|
+
const size = Math.max(0, toFiniteInteger(length, 0))
|
|
21
|
+
const start = clampIndex(size, options.from ?? options.start, 0)
|
|
22
|
+
|
|
23
|
+
let end
|
|
24
|
+
if (options.count != null) {
|
|
25
|
+
const count = Math.max(0, toFiniteInteger(options.count, 0))
|
|
26
|
+
end = Math.min(size, start + count)
|
|
27
|
+
} else {
|
|
28
|
+
end = clampIndex(size, options.to ?? options.end, size)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (end < start) end = start
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
start,
|
|
35
|
+
end,
|
|
36
|
+
length: end - start,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeInsertIndex(length, value, fallback) {
|
|
41
|
+
const size = Math.max(0, toFiniteInteger(length, 0))
|
|
42
|
+
return clampIndex(size, value, fallback == null ? size : fallback)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeCount(value, fallback = 0) {
|
|
46
|
+
return Math.max(0, toFiniteInteger(value, fallback))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
toFiniteInteger,
|
|
51
|
+
clampIndex,
|
|
52
|
+
normalizeRange,
|
|
53
|
+
normalizeInsertIndex,
|
|
54
|
+
normalizeCount,
|
|
55
|
+
}
|
package/lib/shared.js
CHANGED
|
@@ -103,6 +103,10 @@ function tokenizePath(path) {
|
|
|
103
103
|
|
|
104
104
|
function getValueByPath(value, path) {
|
|
105
105
|
const tokens = tokenizePath(path)
|
|
106
|
+
return getValueByTokens(value, tokens)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getValueByTokens(value, tokens) {
|
|
106
110
|
let current = value
|
|
107
111
|
|
|
108
112
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
@@ -119,6 +123,24 @@ function createIteratee(iteratee) {
|
|
|
119
123
|
return (value) => getValueByPath(value, iteratee)
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
function stringifyPathTokens(tokens) {
|
|
127
|
+
if (!Array.isArray(tokens) || !tokens.length) return ""
|
|
128
|
+
|
|
129
|
+
let output = ""
|
|
130
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
131
|
+
const token = tokens[index]
|
|
132
|
+
if (typeof token === "number") {
|
|
133
|
+
output += `[${token}]`
|
|
134
|
+
} else if (!output) {
|
|
135
|
+
output = token
|
|
136
|
+
} else {
|
|
137
|
+
output += `.${token}`
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return output
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
function cloneShallow(value) {
|
|
123
145
|
if (Array.isArray(value)) return value.slice()
|
|
124
146
|
if (isPlainObject(value)) return { ...value }
|
|
@@ -237,13 +259,55 @@ function cloneDeep(value) {
|
|
|
237
259
|
return root
|
|
238
260
|
}
|
|
239
261
|
|
|
262
|
+
function setValueByTokens(target, tokens, nextValue) {
|
|
263
|
+
if (!Array.isArray(tokens) || !tokens.length) return nextValue
|
|
264
|
+
if (!isObjectLike(target) && !Array.isArray(target)) return target
|
|
265
|
+
|
|
266
|
+
let current = target
|
|
267
|
+
|
|
268
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
269
|
+
if (current == null) return target
|
|
270
|
+
current = current[tokens[index]]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (current == null) return target
|
|
274
|
+
current[tokens[tokens.length - 1]] = nextValue
|
|
275
|
+
return target
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function deleteByTokens(target, tokens) {
|
|
279
|
+
if (!Array.isArray(tokens) || !tokens.length) return undefined
|
|
280
|
+
if (!isObjectLike(target) && !Array.isArray(target)) return target
|
|
281
|
+
|
|
282
|
+
let current = target
|
|
283
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
284
|
+
if (current == null) return target
|
|
285
|
+
current = current[tokens[index]]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (current == null) return target
|
|
289
|
+
|
|
290
|
+
const last = tokens[tokens.length - 1]
|
|
291
|
+
if (Array.isArray(current) && typeof last === "number") {
|
|
292
|
+
current.splice(last, 1)
|
|
293
|
+
} else {
|
|
294
|
+
delete current[last]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return target
|
|
298
|
+
}
|
|
299
|
+
|
|
240
300
|
module.exports = {
|
|
241
301
|
isObjectLike,
|
|
242
302
|
isPlainObject,
|
|
243
303
|
isTypedArray,
|
|
244
304
|
tokenizePath,
|
|
305
|
+
stringifyPathTokens,
|
|
245
306
|
getValueByPath,
|
|
307
|
+
getValueByTokens,
|
|
246
308
|
createIteratee,
|
|
247
309
|
cloneShallow,
|
|
248
310
|
cloneDeep,
|
|
311
|
+
setValueByTokens,
|
|
312
|
+
deleteByTokens,
|
|
249
313
|
}
|