-
-
Notifications
You must be signed in to change notification settings - Fork 630
/
utils.js
256 lines (227 loc) · 7.15 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// http://lea.verou.me/2016/12/resolve-promises-externally-with-this-one-weird-trick/
export function defer() {
let res;
let rej;
const promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
promise.resolve = res;
promise.reject = rej;
return promise;
}
export function makeString(object) {
if (object == null) return '';
/* eslint prefer-template: 0 */
return '' + object;
}
export function copy(a, s, t) {
a.forEach((m) => {
if (s[m]) t[m] = s[m];
});
}
// We extract out the RegExp definition to improve performance with React Native Android, which has poor RegExp
// initialization performance
const lastOfPathSeparatorRegExp = /###/g;
function getLastOfPath(object, path, Empty) {
function cleanKey(key) {
return key && key.indexOf('###') > -1 ? key.replace(lastOfPathSeparatorRegExp, '.') : key;
}
function canNotTraverseDeeper() {
return !object || typeof object === 'string';
}
const stack = typeof path !== 'string' ? path : path.split('.');
let stackIndex = 0;
// iterate through the stack, but leave the last item
while (stackIndex < stack.length - 1) {
if (canNotTraverseDeeper()) return {};
const key = cleanKey(stack[stackIndex]);
if (!object[key] && Empty) object[key] = new Empty();
// prevent prototype pollution
if (Object.prototype.hasOwnProperty.call(object, key)) {
object = object[key];
} else {
object = {};
}
++stackIndex;
}
if (canNotTraverseDeeper()) return {};
return {
obj: object,
k: cleanKey(stack[stackIndex]),
};
}
export function setPath(object, path, newValue) {
const { obj, k } = getLastOfPath(object, path, Object);
if (obj !== undefined || path.length === 1) {
obj[k] = newValue;
return;
}
let e = path[path.length - 1];
let p = path.slice(0, path.length - 1);
let last = getLastOfPath(object, p, Object);
while (last.obj === undefined && p.length) {
e = `${p[p.length - 1]}.${e}`;
p = p.slice(0, p.length - 1);
last = getLastOfPath(object, p, Object);
if (last && last.obj && typeof last.obj[`${last.k}.${e}`] !== 'undefined') {
last.obj = undefined;
}
}
last.obj[`${last.k}.${e}`] = newValue;
}
export function pushPath(object, path, newValue, concat) {
const { obj, k } = getLastOfPath(object, path, Object);
obj[k] = obj[k] || [];
if (concat) obj[k] = obj[k].concat(newValue);
if (!concat) obj[k].push(newValue);
}
export function getPath(object, path) {
const { obj, k } = getLastOfPath(object, path);
if (!obj) return undefined;
return obj[k];
}
export function getPathWithDefaults(data, defaultData, key) {
const value = getPath(data, key);
if (value !== undefined) {
return value;
}
// Fallback to default values
return getPath(defaultData, key);
}
export function deepExtend(target, source, overwrite) {
/* eslint no-restricted-syntax: 0 */
for (const prop in source) {
if (prop !== '__proto__' && prop !== 'constructor') {
if (prop in target) {
// If we reached a leaf string in target or source then replace with source or skip depending on the 'overwrite' switch
if (
typeof target[prop] === 'string' ||
target[prop] instanceof String ||
typeof source[prop] === 'string' ||
source[prop] instanceof String
) {
if (overwrite) target[prop] = source[prop];
} else {
deepExtend(target[prop], source[prop], overwrite);
}
} else {
target[prop] = source[prop];
}
}
}
return target;
}
export function regexEscape(str) {
/* eslint no-useless-escape: 0 */
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}
/* eslint-disable */
var _entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
/* eslint-enable */
export function escape(data) {
if (typeof data === 'string') {
return data.replace(/[&<>"'\/]/g, (s) => _entityMap[s]);
}
return data;
}
/**
* This is a reusable regular expression cache class. Given a certain maximum number of regular expressions we're
* allowed to store in the cache, it provides a way to avoid recreating regular expression objects over and over.
* When it needs to evict something, it evicts the oldest one.
*/
class RegExpCache {
constructor(capacity) {
this.capacity = capacity;
this.regExpMap = new Map();
// Since our capacity tends to be fairly small, `.shift()` will be fairly quick despite being O(n). We just use a
// normal array to keep it simple.
this.regExpQueue = [];
}
getRegExp(pattern) {
const regExpFromCache = this.regExpMap.get(pattern);
if (regExpFromCache !== undefined) {
return regExpFromCache;
}
const regExpNew = new RegExp(pattern);
if (this.regExpQueue.length === this.capacity) {
this.regExpMap.delete(this.regExpQueue.shift());
}
this.regExpMap.set(pattern, regExpNew);
this.regExpQueue.push(pattern);
return regExpNew;
}
}
const chars = [' ', ',', '?', '!', ';'];
// We cache RegExps to improve performance with React Native Android, which has poor RegExp initialization performance.
// Capacity of 20 should be plenty, as nsSeparator/keySeparator don't tend to vary much across calls.
const looksLikeObjectPathRegExpCache = new RegExpCache(20);
export function looksLikeObjectPath(key, nsSeparator, keySeparator) {
nsSeparator = nsSeparator || '';
keySeparator = keySeparator || '';
const possibleChars = chars.filter(
(c) => nsSeparator.indexOf(c) < 0 && keySeparator.indexOf(c) < 0,
);
if (possibleChars.length === 0) return true;
const r = looksLikeObjectPathRegExpCache.getRegExp(
`(${possibleChars.map((c) => (c === '?' ? '\\?' : c)).join('|')})`,
);
let matched = !r.test(key);
if (!matched) {
const ki = key.indexOf(keySeparator);
if (ki > 0 && !r.test(key.substring(0, ki))) {
matched = true;
}
}
return matched;
}
/**
* Given
*
* 1. a top level object obj, and
* 2. a path to a deeply nested string or object within it
*
* Find and return that deeply nested string or object. The caveat is that the keys of objects within the nesting chain
* may contain period characters. Therefore, we need to DFS and explore all possible keys at each step until we find the
* deeply nested string or object.
*/
export function deepFind(obj, path, keySeparator = '.') {
if (!obj) return undefined;
if (obj[path]) return obj[path];
const tokens = path.split(keySeparator);
let current = obj;
for (let i = 0; i < tokens.length; ) {
if (!current || typeof current !== 'object') {
return undefined;
}
let next;
let nextPath = '';
for (let j = i; j < tokens.length; ++j) {
if (j !== i) {
nextPath += keySeparator;
}
nextPath += tokens[j];
next = current[nextPath];
if (next !== undefined) {
if (['string', 'number', 'boolean'].indexOf(typeof next) > -1 && j < tokens.length - 1) {
continue;
}
i += j - i + 1;
break;
}
}
current = next;
}
return current;
}
export function getCleanedCode(code) {
if (code && code.indexOf('_') > 0) return code.replace('_', '-');
return code;
}