@symbo.ls/utils 3.2.3 → 3.14.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 +25 -12
- package/README.md +106 -0
- package/array.js +162 -0
- package/assets.js +117 -0
- package/cache.js +7 -0
- package/cdn.js +83 -0
- package/component.js +25 -0
- package/cookie.js +78 -0
- package/{src/detectHeight.js → detectHeight.js} +3 -3
- package/dist/cjs/detectHeight.js +10 -21
- package/dist/cjs/domqlv3hacks.js +3845 -0
- package/dist/cjs/index.js +3910 -251
- package/dist/cjs/scaling.js +3 -149
- package/element.js +148 -0
- package/env.js +15 -0
- package/events.js +19 -0
- package/extends.js +519 -0
- package/function.js +145 -0
- package/globals.js +8 -0
- package/if.js +30 -0
- package/index.js +42 -0
- package/key.js +20 -0
- package/keys.js +176 -0
- package/log.js +12 -0
- package/metadata.js +392 -0
- package/methods.js +684 -0
- package/node.js +37 -0
- package/object.js +837 -0
- package/package.json +38 -22
- package/projectKeys.js +30 -0
- package/props.js +327 -0
- package/{src/scaling.js → scaling.js} +1 -1
- package/scope.js +32 -0
- package/sharedLibraries.js +184 -0
- package/state.js +203 -0
- package/{src/index.js → string-extra.js} +11 -17
- package/string.js +206 -0
- package/tags.js +176 -0
- package/triggerEvent.js +102 -0
- package/types.js +55 -0
- package/update.js +27 -0
- /package/{src/browser.js → browser.js} +0 -0
- /package/{src/date.js → date.js} +0 -0
- /package/{src/fibonacci.js → fibonacci.js} +0 -0
- /package/{src/files.js → files.js} +0 -0
- /package/{src/load.js → load.js} +0 -0
- /package/{src/style.js → style.js} +0 -0
package/object.js
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import { window } from './globals.js'
|
|
4
|
+
import {
|
|
5
|
+
isFunction,
|
|
6
|
+
isObjectLike,
|
|
7
|
+
isObject,
|
|
8
|
+
isArray,
|
|
9
|
+
isString,
|
|
10
|
+
is
|
|
11
|
+
} from './types.js'
|
|
12
|
+
import { unstackArrayOfObjects } from './array.js'
|
|
13
|
+
import { stringIncludesAny } from './string.js'
|
|
14
|
+
import { isDOMNode } from './node.js'
|
|
15
|
+
import { METHODS_EXL } from './keys.js'
|
|
16
|
+
|
|
17
|
+
const ENV = process.env.NODE_ENV
|
|
18
|
+
|
|
19
|
+
const _startsWithDunder = (e) =>
|
|
20
|
+
e.charCodeAt(0) === 95 && e.charCodeAt(1) === 95
|
|
21
|
+
|
|
22
|
+
export const exec = (param, element, state, context) => {
|
|
23
|
+
if (isFunction(param)) {
|
|
24
|
+
if (!element) return
|
|
25
|
+
if (typeof param.call !== 'function') return param
|
|
26
|
+
return param.call(
|
|
27
|
+
element,
|
|
28
|
+
element,
|
|
29
|
+
state || element.state,
|
|
30
|
+
context || element.context
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
// If param is a non-function value and the context has handler-resolving
|
|
34
|
+
// plugins (e.g. funcql), try to resolve it into a callable function.
|
|
35
|
+
// This enables funcql schemas as property values (text, if, style, etc.).
|
|
36
|
+
// Only attempt for array/object params that could be schemas — skip
|
|
37
|
+
// primitives and DOM nodes to avoid overhead on normal DOMQL values.
|
|
38
|
+
if (
|
|
39
|
+
param != null &&
|
|
40
|
+
element?.context?.plugins &&
|
|
41
|
+
(isArray(param) || (isObject(param) && !isDOMNode(param)))
|
|
42
|
+
) {
|
|
43
|
+
const plugins = element.context.plugins
|
|
44
|
+
for (const plugin of plugins) {
|
|
45
|
+
if (plugin.resolveHandler) {
|
|
46
|
+
const resolved = plugin.resolveHandler(param, element)
|
|
47
|
+
if (typeof resolved === 'function') {
|
|
48
|
+
return exec(resolved, element, state, context)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return param
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const map = (obj, extention, element) => {
|
|
57
|
+
for (const e in extention) {
|
|
58
|
+
obj[e] = exec(extention[e], element)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const merge = (element, obj, excludeFrom = []) => {
|
|
63
|
+
const useSet = excludeFrom instanceof Set
|
|
64
|
+
for (const e in obj) {
|
|
65
|
+
if (!Object.prototype.hasOwnProperty.call(obj, e)) continue
|
|
66
|
+
if (_startsWithDunder(e)) continue
|
|
67
|
+
if (useSet ? excludeFrom.has(e) : excludeFrom.includes(e)) continue
|
|
68
|
+
if (element[e] === undefined) {
|
|
69
|
+
element[e] = obj[e]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return element
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const deepMerge = (element, extend, excludeFrom = METHODS_EXL) => {
|
|
76
|
+
return _deepMerge(element, extend, excludeFrom, null)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Internal worker that carries an ancestors stack to break cycles. The stack
|
|
80
|
+
// is allocated lazily on first recursion so non-cyclic merges have zero
|
|
81
|
+
// overhead. We track only the (element, extend) pairs in the *current
|
|
82
|
+
// descent* — once a pair pops off the stack we forget it, so legitimate
|
|
83
|
+
// reuse of shared extend objects across siblings is unaffected.
|
|
84
|
+
//
|
|
85
|
+
// PROTOTYPE POLLUTION GUARD: dunder keys are already skipped via
|
|
86
|
+
// `_startsWithDunder`, but `constructor` and `prototype` are NOT dunder
|
|
87
|
+
// and would otherwise be merged through. JSON.parse('{"constructor":
|
|
88
|
+
// {"prototype": {"isAdmin": true}}}') passed as `extend` would walk into
|
|
89
|
+
// `element.constructor` (the real Object constructor), then recurse and
|
|
90
|
+
// set `element.constructor.prototype.isAdmin = true` — polluting every
|
|
91
|
+
// object globally. Drop those segments.
|
|
92
|
+
const _deepMerge = (element, extend, excludeFrom, ancestors) => {
|
|
93
|
+
if (element === extend) return element
|
|
94
|
+
if (ancestors) {
|
|
95
|
+
for (let i = 0; i < ancestors.length; i += 2) {
|
|
96
|
+
if (ancestors[i] === element && ancestors[i + 1] === extend) return element
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const useSet = excludeFrom instanceof Set
|
|
100
|
+
for (const e in extend) {
|
|
101
|
+
if (!Object.prototype.hasOwnProperty.call(extend, e)) continue
|
|
102
|
+
if (_startsWithDunder(e)) continue
|
|
103
|
+
if (e === 'constructor' || e === 'prototype') continue
|
|
104
|
+
if (useSet ? excludeFrom.has(e) : excludeFrom.includes(e)) continue
|
|
105
|
+
const elementProp = element[e]
|
|
106
|
+
const extendProp = extend[e]
|
|
107
|
+
if (isObjectLike(elementProp) && isObjectLike(extendProp)) {
|
|
108
|
+
const stack = ancestors || []
|
|
109
|
+
stack.push(element, extend)
|
|
110
|
+
_deepMerge(elementProp, extendProp, excludeFrom, stack)
|
|
111
|
+
stack.length -= 2
|
|
112
|
+
} else if (elementProp === undefined) {
|
|
113
|
+
element[e] = extendProp
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return element
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const clone = (obj, excludeFrom = []) => {
|
|
120
|
+
const useSet = excludeFrom instanceof Set
|
|
121
|
+
const o = {}
|
|
122
|
+
for (const prop in obj) {
|
|
123
|
+
if (!Object.prototype.hasOwnProperty.call(obj, prop)) continue
|
|
124
|
+
if (_startsWithDunder(prop)) continue
|
|
125
|
+
if (useSet ? excludeFrom.has(prop) : excludeFrom.includes(prop)) continue
|
|
126
|
+
o[prop] = obj[prop]
|
|
127
|
+
}
|
|
128
|
+
return o
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Enhanced deep clone function that combines features from multiple implementations
|
|
133
|
+
* @param {any} obj - Object to clone
|
|
134
|
+
* @param {Object} options - Configuration options
|
|
135
|
+
* @param {string[]} options.exclude - Properties to exclude from cloning
|
|
136
|
+
* @param {boolean} options.cleanUndefined - Remove undefined values
|
|
137
|
+
* @param {boolean} options.cleanNull - Remove null values
|
|
138
|
+
* @param {Window} options.window - Window object for cross-frame cloning
|
|
139
|
+
* @param {WeakMap} options.visited - WeakMap for tracking circular references
|
|
140
|
+
* @param {boolean} options.handleExtends - Whether to handle 'extends' arrays specially
|
|
141
|
+
* @returns {any} Cloned object
|
|
142
|
+
*/
|
|
143
|
+
export const deepClone = (obj, options = {}) => {
|
|
144
|
+
const {
|
|
145
|
+
exclude = [],
|
|
146
|
+
cleanUndefined = false,
|
|
147
|
+
cleanNull = false,
|
|
148
|
+
window: targetWindow,
|
|
149
|
+
visited = new WeakMap(),
|
|
150
|
+
handleExtends = false
|
|
151
|
+
} = options
|
|
152
|
+
|
|
153
|
+
const contentWindow = targetWindow || window || globalThis
|
|
154
|
+
|
|
155
|
+
// Handle non-object types and special cases
|
|
156
|
+
if (!isObjectLike(obj) || isDOMNode(obj)) {
|
|
157
|
+
return obj
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle circular references
|
|
161
|
+
if (visited.has(obj)) {
|
|
162
|
+
return visited.get(obj)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create appropriate container based on type and window context
|
|
166
|
+
const isArr = isArray(obj)
|
|
167
|
+
const clone = isArr ? [] : {}
|
|
168
|
+
|
|
169
|
+
// Store the clone to handle circular references
|
|
170
|
+
visited.set(obj, clone)
|
|
171
|
+
|
|
172
|
+
// Convert exclude to Set for O(1) lookups when list is non-trivial
|
|
173
|
+
const excludeSet =
|
|
174
|
+
exclude instanceof Set
|
|
175
|
+
? exclude
|
|
176
|
+
: exclude.length > 3
|
|
177
|
+
? new Set(exclude)
|
|
178
|
+
: null
|
|
179
|
+
|
|
180
|
+
// Clone properties
|
|
181
|
+
for (const key in obj) {
|
|
182
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
|
|
183
|
+
|
|
184
|
+
// Skip excluded properties
|
|
185
|
+
if (_startsWithDunder(key) || key === '__proto__') continue
|
|
186
|
+
if (excludeSet ? excludeSet.has(key) : exclude.includes(key)) continue
|
|
187
|
+
|
|
188
|
+
const value = obj[key]
|
|
189
|
+
|
|
190
|
+
// Skip based on cleanup options
|
|
191
|
+
if (cleanUndefined && value === undefined) continue
|
|
192
|
+
if (cleanNull && value === null) continue
|
|
193
|
+
|
|
194
|
+
// Handle special cases
|
|
195
|
+
if (isDOMNode(value)) {
|
|
196
|
+
clone[key] = value
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Handle 'extends' array if enabled
|
|
201
|
+
if (handleExtends && key === 'extends' && isArray(value)) {
|
|
202
|
+
clone[key] = unstackArrayOfObjects(value, exclude)
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handle functions in cross-frame scenario
|
|
207
|
+
// Keep original function references — DOMQL handles window/document
|
|
208
|
+
// isolation via element.context, so eval'ing into iframe scope is
|
|
209
|
+
// unnecessary and breaks closure-dependent functions (e.g. resolveTheme)
|
|
210
|
+
if (isFunction(value)) {
|
|
211
|
+
clone[key] = value
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Recursively clone objects
|
|
216
|
+
if (isObjectLike(value)) {
|
|
217
|
+
clone[key] = deepClone(value, {
|
|
218
|
+
...options,
|
|
219
|
+
visited
|
|
220
|
+
})
|
|
221
|
+
} else {
|
|
222
|
+
clone[key] = value
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return clone
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Stringify object
|
|
231
|
+
*/
|
|
232
|
+
export const deepStringifyFunctions = (obj, stringified = {}) => {
|
|
233
|
+
if (obj.node || obj.__ref || obj.parent || obj.__element || obj.parse) {
|
|
234
|
+
;(obj.__element || obj.parent?.__element).warn(
|
|
235
|
+
'Trying to clone element or state at',
|
|
236
|
+
obj
|
|
237
|
+
)
|
|
238
|
+
obj = obj.parse?.()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const prop in obj) {
|
|
242
|
+
const objProp = obj[prop]
|
|
243
|
+
if (isFunction(objProp)) {
|
|
244
|
+
stringified[prop] = objProp.toString()
|
|
245
|
+
} else if (isObject(objProp)) {
|
|
246
|
+
stringified[prop] = {}
|
|
247
|
+
deepStringifyFunctions(objProp, stringified[prop])
|
|
248
|
+
} else if (isArray(objProp)) {
|
|
249
|
+
const arr = (stringified[prop] = [])
|
|
250
|
+
for (let i = 0; i < objProp.length; i++) {
|
|
251
|
+
const v = objProp[i]
|
|
252
|
+
if (isObject(v)) {
|
|
253
|
+
arr[i] = {}
|
|
254
|
+
deepStringifyFunctions(v, arr[i])
|
|
255
|
+
} else if (isFunction(v)) {
|
|
256
|
+
arr[i] = v.toString()
|
|
257
|
+
} else {
|
|
258
|
+
arr[i] = v
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
stringified[prop] = objProp
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return stringified
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const OBJ_TO_STR_SPECIAL_CHARS = new Set([
|
|
269
|
+
'&',
|
|
270
|
+
'*',
|
|
271
|
+
'-',
|
|
272
|
+
':',
|
|
273
|
+
'%',
|
|
274
|
+
'{',
|
|
275
|
+
'}',
|
|
276
|
+
'>',
|
|
277
|
+
'<',
|
|
278
|
+
'@',
|
|
279
|
+
'.',
|
|
280
|
+
'/',
|
|
281
|
+
'!',
|
|
282
|
+
' '
|
|
283
|
+
])
|
|
284
|
+
|
|
285
|
+
export const objectToString = (obj = {}, indent = 0) => {
|
|
286
|
+
// Handle empty object case
|
|
287
|
+
if (obj === null || typeof obj !== 'object') {
|
|
288
|
+
return String(obj)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Handle empty object case - avoid Object.keys allocation
|
|
292
|
+
let hasKeys = false
|
|
293
|
+
for (const _k in obj) {
|
|
294
|
+
hasKeys = true
|
|
295
|
+
break
|
|
296
|
+
} // eslint-disable-line
|
|
297
|
+
if (!hasKeys) return '{}'
|
|
298
|
+
|
|
299
|
+
const spaces = ' '.repeat(indent)
|
|
300
|
+
let str = '{\n'
|
|
301
|
+
|
|
302
|
+
for (const key in obj) {
|
|
303
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
|
|
304
|
+
const value = obj[key]
|
|
305
|
+
let keyNeedsQuotes = false
|
|
306
|
+
for (let i = 0; i < key.length; i++) {
|
|
307
|
+
if (OBJ_TO_STR_SPECIAL_CHARS.has(key[i])) {
|
|
308
|
+
keyNeedsQuotes = true
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const stringedKey = keyNeedsQuotes ? `'${key}'` : key
|
|
313
|
+
str += `${spaces} ${stringedKey}: `
|
|
314
|
+
|
|
315
|
+
if (isArray(value)) {
|
|
316
|
+
str += '[\n'
|
|
317
|
+
for (const element of value) {
|
|
318
|
+
if (isObjectLike(element) && element !== null) {
|
|
319
|
+
str += `${spaces} ${objectToString(element, indent + 2)},\n`
|
|
320
|
+
} else if (isString(element)) {
|
|
321
|
+
str += `${spaces} '${element}',\n`
|
|
322
|
+
} else {
|
|
323
|
+
str += `${spaces} ${element},\n`
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
str += `${spaces} ]`
|
|
327
|
+
} else if (isObjectLike(value)) {
|
|
328
|
+
str += objectToString(value, indent + 1)
|
|
329
|
+
} else if (isString(value)) {
|
|
330
|
+
str += stringIncludesAny(value, ['\n', "'"])
|
|
331
|
+
? `\`${value}\``
|
|
332
|
+
: `'${value}'`
|
|
333
|
+
} else {
|
|
334
|
+
str += value
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
str += ',\n'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
str += `${spaces}}`
|
|
341
|
+
return str
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const FN_PATTERNS = [
|
|
345
|
+
/^\(\s*\{[^}]*\}\s*\)\s*=>/,
|
|
346
|
+
/^(\([^)]*\)|[^=]*)\s*=>/,
|
|
347
|
+
/^function[\s(]/,
|
|
348
|
+
/^async\s+/,
|
|
349
|
+
/^\(\s*function/,
|
|
350
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=>/
|
|
351
|
+
]
|
|
352
|
+
const RE_JSON_LIKE = /^["[{]/
|
|
353
|
+
|
|
354
|
+
export const hasFunction = (str) => {
|
|
355
|
+
if (!str) return false
|
|
356
|
+
|
|
357
|
+
const trimmed = str.trim().replace(/\n\s*/g, ' ').trim()
|
|
358
|
+
|
|
359
|
+
if (trimmed === '' || trimmed === '{}' || trimmed === '[]') return false
|
|
360
|
+
|
|
361
|
+
const isFn = FN_PATTERNS.some((pattern) => pattern.test(trimmed))
|
|
362
|
+
if (!isFn) return false
|
|
363
|
+
|
|
364
|
+
const firstChar = trimmed.charCodeAt(0)
|
|
365
|
+
const hasArrow = trimmed.includes('=>')
|
|
366
|
+
// '{' = 123, '[' = 91
|
|
367
|
+
if (firstChar === 123 && !hasArrow) return false // object literal
|
|
368
|
+
if (firstChar === 91) return false // array literal
|
|
369
|
+
if (RE_JSON_LIKE.test(trimmed) && !hasArrow) return false
|
|
370
|
+
|
|
371
|
+
return true
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Indirect eval — invokes the global eval, which works in both browser and
|
|
375
|
+
// Node without depending on a `window` object. Required for SSR / Brender
|
|
376
|
+
// passes that hit prepareContext without a real DOM.
|
|
377
|
+
const __globalEval = (src) => (0, eval)(src) // eslint-disable-line no-eval
|
|
378
|
+
|
|
379
|
+
export const deepDestringifyFunctions = (
|
|
380
|
+
obj,
|
|
381
|
+
destringified = {},
|
|
382
|
+
opts = { window: { eval: __globalEval } }
|
|
383
|
+
) => {
|
|
384
|
+
for (const prop in obj) {
|
|
385
|
+
if (!Object.prototype.hasOwnProperty.call(obj, prop)) continue
|
|
386
|
+
|
|
387
|
+
const objProp = obj[prop]
|
|
388
|
+
|
|
389
|
+
if (isString(objProp)) {
|
|
390
|
+
if (hasFunction(objProp)) {
|
|
391
|
+
try {
|
|
392
|
+
destringified[prop] = opts.window.eval(`(${objProp})`)
|
|
393
|
+
} catch (e) {
|
|
394
|
+
// FR-1 (FRANK-RUNNER.md): the silent string-fallback used to
|
|
395
|
+
// mask broken serialization — eight aihouse functions stored
|
|
396
|
+
// as strings instead of functions, `el.call(name)` no-op'd
|
|
397
|
+
// with no error. Surface the failure so consumers can audit
|
|
398
|
+
// what frank produced. Keep the fallback (don't crash boot)
|
|
399
|
+
// but make it loud.
|
|
400
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
401
|
+
console.warn(
|
|
402
|
+
'[smbls] deepDestringifyFunctions: eval failed on "' + prop + '" — ' +
|
|
403
|
+
'function will be left as a string and el.call("' + prop + '") ' +
|
|
404
|
+
'will silently no-op. Reason: ' + (e && e.message ? e.message : String(e)) + '.\n' +
|
|
405
|
+
'First 200 chars of source: ' + String(objProp).slice(0, 200)
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
destringified[prop] = objProp
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
destringified[prop] = objProp
|
|
412
|
+
}
|
|
413
|
+
} else if (isArray(objProp)) {
|
|
414
|
+
const arr = (destringified[prop] = [])
|
|
415
|
+
for (let i = 0; i < objProp.length; i++) {
|
|
416
|
+
const arrProp = objProp[i]
|
|
417
|
+
if (isString(arrProp)) {
|
|
418
|
+
if (hasFunction(arrProp)) {
|
|
419
|
+
try {
|
|
420
|
+
arr.push(opts.window.eval(`(${arrProp})`))
|
|
421
|
+
} catch (e) {
|
|
422
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
423
|
+
console.warn(
|
|
424
|
+
`[smbls] deepDestringifyFunctions: eval failed in array at index ${i} ` +
|
|
425
|
+
`(prop "${prop}"). Reason: ${e && e.message ? e.message : String(e)}.\n` +
|
|
426
|
+
`First 200 chars: ${String(arrProp).slice(0, 200)}`
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
arr.push(arrProp)
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
arr.push(arrProp)
|
|
433
|
+
}
|
|
434
|
+
} else if (isObject(arrProp)) {
|
|
435
|
+
arr.push(deepDestringifyFunctions(arrProp))
|
|
436
|
+
} else {
|
|
437
|
+
arr.push(arrProp)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} else if (isObject(objProp)) {
|
|
441
|
+
destringified[prop] = deepDestringifyFunctions(
|
|
442
|
+
objProp,
|
|
443
|
+
destringified[prop]
|
|
444
|
+
)
|
|
445
|
+
} else {
|
|
446
|
+
destringified[prop] = objProp
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return destringified
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Destringify a globalScope object so that function strings become real functions.
|
|
454
|
+
* All globalScope values are made available as local variables when eval'ing each
|
|
455
|
+
* function, so helpers can reference constants and other helpers naturally.
|
|
456
|
+
*/
|
|
457
|
+
export const destringifyGlobalScope = (gs) => {
|
|
458
|
+
if (!gs || typeof gs !== 'object') return gs
|
|
459
|
+
|
|
460
|
+
// First pass: collect non-function values (constants, arrays, objects)
|
|
461
|
+
const result = {}
|
|
462
|
+
const fnEntries = []
|
|
463
|
+
for (const key of Object.keys(gs)) {
|
|
464
|
+
const val = gs[key]
|
|
465
|
+
if (isString(val) && hasFunction(val)) {
|
|
466
|
+
fnEntries.push([key, val])
|
|
467
|
+
} else {
|
|
468
|
+
result[key] = val
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Second pass: eval functions in a closure with all values in scope
|
|
473
|
+
for (const [key, fnStr] of fnEntries) {
|
|
474
|
+
try {
|
|
475
|
+
// Build a closure that exposes all current globalScope values
|
|
476
|
+
const varDecls = Object.keys(result)
|
|
477
|
+
.map((k) => `var ${k} = __gs__[${JSON.stringify(k)}];`)
|
|
478
|
+
.join('\n')
|
|
479
|
+
result[key] = window.eval(
|
|
480
|
+
`(function(__gs__) { ${varDecls}\n return (${fnStr}); })`
|
|
481
|
+
)(result)
|
|
482
|
+
} catch (e) {
|
|
483
|
+
// Fallback: try plain eval
|
|
484
|
+
try {
|
|
485
|
+
result[key] = window.eval(`(${fnStr})`)
|
|
486
|
+
} catch (_) {
|
|
487
|
+
result[key] = fnStr
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return result
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export const stringToObject = (str, opts = { verbose: true }) => {
|
|
496
|
+
try {
|
|
497
|
+
return str ? window.eval('(' + str + ')') : {} // eslint-disable-line
|
|
498
|
+
} catch (e) {
|
|
499
|
+
if (opts.verbose) console.warn(e)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export const hasOwnProperty = (o, ...args) =>
|
|
504
|
+
Object.prototype.hasOwnProperty.call(o, ...args)
|
|
505
|
+
|
|
506
|
+
export const isEmpty = (o) => {
|
|
507
|
+
for (const _ in o) return false // eslint-disable-line
|
|
508
|
+
return true
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export const isEmptyObject = (o) => isObject(o) && isEmpty(o)
|
|
512
|
+
|
|
513
|
+
export const makeObjectWithoutPrototype = () => Object.create(null)
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Overwrites object properties with another
|
|
517
|
+
*/
|
|
518
|
+
export const overwrite = (element, params, opts = {}) => {
|
|
519
|
+
const excl = opts.exclude || []
|
|
520
|
+
const allowDunder = opts.preventUnderscore
|
|
521
|
+
|
|
522
|
+
for (const e in params) {
|
|
523
|
+
if (excl.includes(e) || (!allowDunder && _startsWithDunder(e))) continue
|
|
524
|
+
// Block prototype-pollution writes (see _deepMerge for rationale).
|
|
525
|
+
if (e === 'constructor' || e === 'prototype') continue
|
|
526
|
+
if (params[e] !== undefined) {
|
|
527
|
+
element[e] = params[e]
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return element
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export const overwriteShallow = (obj, params, excludeFrom = []) => {
|
|
535
|
+
const useSet = excludeFrom instanceof Set
|
|
536
|
+
for (const e in params) {
|
|
537
|
+
if (_startsWithDunder(e)) continue
|
|
538
|
+
if (e === 'constructor' || e === 'prototype') continue
|
|
539
|
+
if (useSet ? excludeFrom.has(e) : excludeFrom.includes(e)) continue
|
|
540
|
+
obj[e] = params[e]
|
|
541
|
+
}
|
|
542
|
+
return obj
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Overwrites DEEPLY object properties with another
|
|
547
|
+
*/
|
|
548
|
+
export const overwriteDeep = (
|
|
549
|
+
obj,
|
|
550
|
+
params,
|
|
551
|
+
opts = {},
|
|
552
|
+
visited = new WeakMap()
|
|
553
|
+
) => {
|
|
554
|
+
if (
|
|
555
|
+
!isObjectLike(obj) ||
|
|
556
|
+
!isObjectLike(params) ||
|
|
557
|
+
isDOMNode(obj) ||
|
|
558
|
+
isDOMNode(params)
|
|
559
|
+
) {
|
|
560
|
+
return params
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (visited.has(obj)) return visited.get(obj)
|
|
564
|
+
visited.set(obj, obj)
|
|
565
|
+
|
|
566
|
+
const excl = opts.exclude
|
|
567
|
+
const exclSet = excl ? (excl instanceof Set ? excl : new Set(excl)) : null
|
|
568
|
+
const forcedExclude = !opts.preventForce
|
|
569
|
+
|
|
570
|
+
for (const e in params) {
|
|
571
|
+
if (!Object.prototype.hasOwnProperty.call(params, e)) continue
|
|
572
|
+
if ((exclSet && exclSet.has(e)) || (forcedExclude && _startsWithDunder(e)))
|
|
573
|
+
continue
|
|
574
|
+
if (e === 'constructor' || e === 'prototype') continue
|
|
575
|
+
|
|
576
|
+
const objProp = obj[e]
|
|
577
|
+
const paramsProp = params[e]
|
|
578
|
+
|
|
579
|
+
if (isDOMNode(paramsProp)) {
|
|
580
|
+
obj[e] = paramsProp
|
|
581
|
+
} else if (isObjectLike(objProp) && isObjectLike(paramsProp)) {
|
|
582
|
+
obj[e] = overwriteDeep(objProp, paramsProp, opts, visited)
|
|
583
|
+
} else if (paramsProp !== undefined) {
|
|
584
|
+
obj[e] = paramsProp
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return obj
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Recursively compares two values to determine if they are deeply equal.
|
|
593
|
+
*/
|
|
594
|
+
export const isEqualDeep = (param, element, visited = new Set()) => {
|
|
595
|
+
if (
|
|
596
|
+
typeof param !== 'object' ||
|
|
597
|
+
typeof element !== 'object' ||
|
|
598
|
+
param === null ||
|
|
599
|
+
element === null
|
|
600
|
+
) {
|
|
601
|
+
return param === element
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (visited.has(param) || visited.has(element)) {
|
|
605
|
+
return true
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
visited.add(param)
|
|
609
|
+
visited.add(element)
|
|
610
|
+
|
|
611
|
+
const keysParam = Object.keys(param)
|
|
612
|
+
const keysElement = Object.keys(element)
|
|
613
|
+
|
|
614
|
+
if (keysParam.length !== keysElement.length) {
|
|
615
|
+
return false
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (let i = 0; i < keysParam.length; i++) {
|
|
619
|
+
const key = keysParam[i]
|
|
620
|
+
if (!Object.prototype.hasOwnProperty.call(element, key)) {
|
|
621
|
+
return false
|
|
622
|
+
}
|
|
623
|
+
if (!isEqualDeep(param[key], element[key], visited)) {
|
|
624
|
+
return false
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return true
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const DEEP_CONTAINS_IGNORED = new Set(['node', '__ref'])
|
|
632
|
+
|
|
633
|
+
export const deepContains = (
|
|
634
|
+
obj1,
|
|
635
|
+
obj2,
|
|
636
|
+
ignoredKeys = DEEP_CONTAINS_IGNORED
|
|
637
|
+
) => {
|
|
638
|
+
if (obj1 === obj2) return true
|
|
639
|
+
if (!isObjectLike(obj1) || !isObjectLike(obj2)) return obj1 === obj2
|
|
640
|
+
if (isDOMNode(obj1) || isDOMNode(obj2)) return obj1 === obj2
|
|
641
|
+
|
|
642
|
+
const ignored =
|
|
643
|
+
ignoredKeys instanceof Set ? ignoredKeys : new Set(ignoredKeys)
|
|
644
|
+
const visited = new WeakSet()
|
|
645
|
+
|
|
646
|
+
function checkContains(target, source) {
|
|
647
|
+
if (visited.has(source)) return true
|
|
648
|
+
visited.add(source)
|
|
649
|
+
|
|
650
|
+
for (const key in source) {
|
|
651
|
+
if (!Object.prototype.hasOwnProperty.call(source, key)) continue
|
|
652
|
+
if (ignored.has(key)) continue
|
|
653
|
+
if (!Object.prototype.hasOwnProperty.call(target, key)) return false
|
|
654
|
+
|
|
655
|
+
const sourceValue = source[key]
|
|
656
|
+
const targetValue = target[key]
|
|
657
|
+
|
|
658
|
+
if (isDOMNode(sourceValue) || isDOMNode(targetValue)) {
|
|
659
|
+
if (sourceValue !== targetValue) return false
|
|
660
|
+
} else if (isObjectLike(sourceValue) && isObjectLike(targetValue)) {
|
|
661
|
+
if (!checkContains(targetValue, sourceValue)) return false
|
|
662
|
+
} else if (sourceValue !== targetValue) {
|
|
663
|
+
return false
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return true
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return checkContains(obj1, obj2)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export const removeFromObject = (obj, props) => {
|
|
674
|
+
if (props === undefined || props === null) return obj
|
|
675
|
+
if (is(props)('string', 'number')) {
|
|
676
|
+
delete obj[props]
|
|
677
|
+
} else if (isArray(props)) {
|
|
678
|
+
for (let i = 0; i < props.length; i++) delete obj[props[i]]
|
|
679
|
+
} else {
|
|
680
|
+
throw new Error(
|
|
681
|
+
'Invalid input: props must be a string or an array of strings'
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
return obj
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export const createObjectWithoutPrototype = (obj) => {
|
|
688
|
+
if (obj === null || typeof obj !== 'object') {
|
|
689
|
+
return obj
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const newObj = Object.create(null)
|
|
693
|
+
|
|
694
|
+
for (const key in obj) {
|
|
695
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
696
|
+
newObj[key] = createObjectWithoutPrototype(obj[key])
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return newObj
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export const createNestedObject = (arr, lastValue) => {
|
|
704
|
+
if (arr.length === 0) return lastValue
|
|
705
|
+
|
|
706
|
+
const nestedObject = {}
|
|
707
|
+
let current = nestedObject
|
|
708
|
+
|
|
709
|
+
for (let i = 0; i < arr.length; i++) {
|
|
710
|
+
if (i === arr.length - 1 && lastValue) {
|
|
711
|
+
current[arr[i]] = lastValue
|
|
712
|
+
} else {
|
|
713
|
+
current[arr[i]] = {}
|
|
714
|
+
current = current[arr[i]]
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return nestedObject
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export const removeNestedKeyByPath = (obj, path) => {
|
|
722
|
+
if (!Array.isArray(path)) {
|
|
723
|
+
throw new Error('Path must be an array.')
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let current = obj
|
|
727
|
+
|
|
728
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
729
|
+
if (current[path[i]] === undefined) return
|
|
730
|
+
current = current[path[i]]
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const lastKey = path[path.length - 1]
|
|
734
|
+
if (current && Object.prototype.hasOwnProperty.call(current, lastKey)) {
|
|
735
|
+
delete current[lastKey]
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export const setInObjectByPath = (obj, path, value) => {
|
|
740
|
+
if (!Array.isArray(path)) {
|
|
741
|
+
throw new Error('Path must be an array.')
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
let current = obj
|
|
745
|
+
|
|
746
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
747
|
+
if (!current[path[i]] || typeof current[path[i]] !== 'object') {
|
|
748
|
+
current[path[i]] = {}
|
|
749
|
+
}
|
|
750
|
+
current = current[path[i]]
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
current[path[path.length - 1]] = value
|
|
754
|
+
|
|
755
|
+
return obj
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export const getInObjectByPath = (obj, path) => {
|
|
759
|
+
if (!Array.isArray(path)) {
|
|
760
|
+
throw new Error('Path must be an array.')
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
let current = obj
|
|
764
|
+
|
|
765
|
+
for (let i = 0; i < path.length; i++) {
|
|
766
|
+
if (current === undefined || current === null) {
|
|
767
|
+
return undefined
|
|
768
|
+
}
|
|
769
|
+
current = current[path[i]]
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return current
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export const detectInfiniteLoop = (arr) => {
|
|
776
|
+
const maxRepeats = 10
|
|
777
|
+
let pattern = []
|
|
778
|
+
let repeatCount = 0
|
|
779
|
+
|
|
780
|
+
for (let i = 0; i < arr.length; i++) {
|
|
781
|
+
if (pattern.length < 2) {
|
|
782
|
+
pattern.push(arr[i])
|
|
783
|
+
} else {
|
|
784
|
+
if (arr[i] === pattern[i % 2]) {
|
|
785
|
+
repeatCount++
|
|
786
|
+
} else {
|
|
787
|
+
pattern = [arr[i - 1], arr[i]]
|
|
788
|
+
repeatCount = 1
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (repeatCount >= maxRepeats * 2) {
|
|
792
|
+
if (ENV === 'test' || ENV === 'development') {
|
|
793
|
+
console.warn(
|
|
794
|
+
'Warning: Potential infinite loop detected due to repeated sequence:',
|
|
795
|
+
pattern
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
return true
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export const isCyclic = (obj) => {
|
|
805
|
+
const seen = new WeakSet()
|
|
806
|
+
|
|
807
|
+
function detect(obj) {
|
|
808
|
+
if (obj && typeof obj === 'object') {
|
|
809
|
+
if (seen.has(obj)) return true
|
|
810
|
+
seen.add(obj)
|
|
811
|
+
for (const key in obj) {
|
|
812
|
+
if (
|
|
813
|
+
Object.prototype.hasOwnProperty.call(obj, key) &&
|
|
814
|
+
detect(obj[key])
|
|
815
|
+
) {
|
|
816
|
+
console.log(obj, 'cycle at ' + key)
|
|
817
|
+
return true
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return false
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return detect(obj)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export const excludeKeysFromObject = (obj, excludedKeys) => {
|
|
828
|
+
const excluded =
|
|
829
|
+
excludedKeys instanceof Set ? excludedKeys : new Set(excludedKeys)
|
|
830
|
+
const result = {}
|
|
831
|
+
for (const key in obj) {
|
|
832
|
+
if (Object.prototype.hasOwnProperty.call(obj, key) && !excluded.has(key)) {
|
|
833
|
+
result[key] = obj[key]
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return result
|
|
837
|
+
}
|