structured-fw 0.7.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/Config.ts +47 -0
- package/LICENSE +21 -0
- package/README.md +332 -0
- package/app/Types.ts +1 -0
- package/app/models/README.md +9 -0
- package/app/routes/README.md +19 -0
- package/app/views/README.md +1 -0
- package/app/views/layout.html +1 -0
- package/bin/structured +114 -0
- package/build/Config.d.ts +2 -0
- package/build/Config.js +31 -0
- package/build/app/Types.d.ts +1 -0
- package/build/app/Types.js +1 -0
- package/build/app/models/Users.d.ts +0 -0
- package/build/app/models/Users.js +1 -0
- package/build/app/routes/Auth.d.ts +0 -0
- package/build/app/routes/Auth.js +1 -0
- package/build/app/routes/Test.d.ts +2 -0
- package/build/app/routes/Test.js +101 -0
- package/build/app/routes/Todo.d.ts +0 -0
- package/build/app/routes/Todo.js +1 -0
- package/build/app/routes/Upload.d.ts +0 -0
- package/build/app/routes/Upload.js +1 -0
- package/build/app/routes/Validation.d.ts +2 -0
- package/build/app/routes/Validation.js +34 -0
- package/build/app/views/components/ClientImport/ClientImport.client.d.ts +2 -0
- package/build/app/views/components/ClientImport/ClientImport.client.js +4 -0
- package/build/app/views/components/ClientImport/Export.d.ts +1 -0
- package/build/app/views/components/ClientImport/Export.js +1 -0
- package/build/app/views/components/Conditionals/Conditionals.client.d.ts +2 -0
- package/build/app/views/components/Conditionals/Conditionals.client.js +43 -0
- package/build/app/views/components/FormTest/FormTestNested/FormTestNested.d.ts +8 -0
- package/build/app/views/components/FormTest/FormTestNested/FormTestNested.js +7 -0
- package/build/app/views/components/ModelsTest/ModelsTest.client.d.ts +2 -0
- package/build/app/views/components/ModelsTest/ModelsTest.client.js +5 -0
- package/build/app/views/components/MultipartForm/MultipartForm.client.d.ts +0 -0
- package/build/app/views/components/MultipartForm/MultipartForm.client.js +1 -0
- package/build/app/views/components/PassObject/PassObject.d.ts +10 -0
- package/build/app/views/components/PassObject/PassObject.js +10 -0
- package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.d.ts +6 -0
- package/build/app/views/components/PassObject/ReceiveObj/ReceiveObj.js +6 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.client.d.ts +2 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.client.js +6 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.d.ts +8 -0
- package/build/app/views/components/RedrawAbort/RedrawAbort.js +8 -0
- package/build/app/views/components/ServerSideContext/ServerSideContext.d.ts +7 -0
- package/build/app/views/components/ServerSideContext/ServerSideContext.js +10 -0
- package/build/assets/ts/Export.d.ts +1 -0
- package/build/assets/ts/Export.js +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +3 -0
- package/build/system/Helpers.d.ts +3 -0
- package/build/system/Helpers.js +72 -0
- package/build/system/Symbols.d.ts +3 -0
- package/build/system/Symbols.js +3 -0
- package/build/system/Types.d.ts +171 -0
- package/build/system/Types.js +1 -0
- package/build/system/Util.d.ts +20 -0
- package/build/system/Util.js +336 -0
- package/build/system/client/App.d.ts +7 -0
- package/build/system/client/App.js +8 -0
- package/build/system/client/Client.d.ts +6 -0
- package/build/system/client/Client.js +9 -0
- package/build/system/client/ClientComponent.d.ts +68 -0
- package/build/system/client/ClientComponent.js +734 -0
- package/build/system/client/DataStore.d.ts +22 -0
- package/build/system/client/DataStore.js +64 -0
- package/build/system/client/DataStoreView.d.ts +19 -0
- package/build/system/client/DataStoreView.js +56 -0
- package/build/system/client/EventEmitter.d.ts +7 -0
- package/build/system/client/EventEmitter.js +31 -0
- package/build/system/client/Net.d.ts +13 -0
- package/build/system/client/Net.js +39 -0
- package/build/system/client/NetRequest.d.ts +13 -0
- package/build/system/client/NetRequest.js +45 -0
- package/build/system/server/Application.d.ts +31 -0
- package/build/system/server/Application.js +171 -0
- package/build/system/server/Component.d.ts +27 -0
- package/build/system/server/Component.js +249 -0
- package/build/system/server/Components.d.ts +12 -0
- package/build/system/server/Components.js +77 -0
- package/build/system/server/Cookies.d.ts +6 -0
- package/build/system/server/Cookies.js +19 -0
- package/build/system/server/Document.d.ts +24 -0
- package/build/system/server/Document.js +107 -0
- package/build/system/server/DocumentHead.d.ts +32 -0
- package/build/system/server/DocumentHead.js +118 -0
- package/build/system/server/FormValidation.d.ts +16 -0
- package/build/system/server/FormValidation.js +197 -0
- package/build/system/server/Handlebars.d.ts +11 -0
- package/build/system/server/Handlebars.js +34 -0
- package/build/system/server/Request.d.ts +21 -0
- package/build/system/server/Request.js +356 -0
- package/build/system/server/Session.d.ts +23 -0
- package/build/system/server/Session.js +114 -0
- package/build/system/server/dom/DOMFragment.d.ts +4 -0
- package/build/system/server/dom/DOMFragment.js +6 -0
- package/build/system/server/dom/DOMNode.d.ts +31 -0
- package/build/system/server/dom/DOMNode.js +110 -0
- package/build/system/server/dom/HTMLParser.d.ts +21 -0
- package/build/system/server/dom/HTMLParser.js +204 -0
- package/index.ts +4 -0
- package/package.json +31 -0
- package/system/Helpers.ts +97 -0
- package/system/Symbols.ts +6 -0
- package/system/Types.ts +234 -0
- package/system/Util.ts +488 -0
- package/system/client/App.ts +11 -0
- package/system/client/Client.ts +9 -0
- package/system/client/ClientComponent.ts +1117 -0
- package/system/client/DataStore.ts +101 -0
- package/system/client/DataStoreView.ts +82 -0
- package/system/client/EventEmitter.ts +38 -0
- package/system/client/Net.ts +58 -0
- package/system/client/NetRequest.ts +64 -0
- package/system/server/Application.ts +230 -0
- package/system/server/Component.ts +404 -0
- package/system/server/Components.ts +111 -0
- package/system/server/Cookies.ts +29 -0
- package/system/server/Document.ts +163 -0
- package/system/server/DocumentHead.ts +150 -0
- package/system/server/FormValidation.ts +231 -0
- package/system/server/Handlebars.ts +51 -0
- package/system/server/Request.ts +497 -0
- package/system/server/Session.ts +151 -0
- package/system/server/dom/DOMFragment.ts +7 -0
- package/system/server/dom/DOMNode.ts +140 -0
- package/system/server/dom/HTMLParser.ts +238 -0
- package/tsconfig.json +35 -0
package/system/Util.ts
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { LooseObject, PostedDataDecoded } from "./Types.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// process given query string into an object
|
|
5
|
+
// optionally accepts initialValue
|
|
6
|
+
export function queryStringDecode(queryString: string, initialValue: PostedDataDecoded = {}, trimValues: boolean = true): PostedDataDecoded {
|
|
7
|
+
// replace + with space and split string at & to produce key=value pairs
|
|
8
|
+
const pairsRaw = queryString.replaceAll('+', ' ').split('&');
|
|
9
|
+
|
|
10
|
+
// produce an array of {key, value, isArray, isObject, keyRaw}
|
|
11
|
+
let pairs = pairsRaw.map((pair) => {
|
|
12
|
+
|
|
13
|
+
// parts could have:
|
|
14
|
+
// 1 element - key with no value
|
|
15
|
+
// 2 elements - key & value
|
|
16
|
+
// more than 2 elements if the value had "=" in it
|
|
17
|
+
let parts = pair.split('=');
|
|
18
|
+
if (parts.length > 2) {
|
|
19
|
+
// value contained a "=", join all elements 1 and above with "="
|
|
20
|
+
// this makes parts have exactly 2 elements (key and value)
|
|
21
|
+
parts = [parts[0]].concat(parts.slice(1).join('='));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// part now holds 1 (key without value) or 2 elements (key and value)
|
|
25
|
+
const hasValue = parts.length > 1;
|
|
26
|
+
|
|
27
|
+
// decode key
|
|
28
|
+
const keyRaw = decodeURIComponent(parts[0]);
|
|
29
|
+
|
|
30
|
+
// if key has a value, decode it, otherwise value is true (as in, key is set)
|
|
31
|
+
const value = hasValue ? (trimValues ? decodeURIComponent(parts[1]).trim() : decodeURIComponent(parts[1])) : true;
|
|
32
|
+
|
|
33
|
+
// if key includes "[.*]" then it is an array or object
|
|
34
|
+
const arrayOrObject = /\[[^\[\]]*\]/.test(keyRaw);
|
|
35
|
+
|
|
36
|
+
// pathStart is "[.*]" or null if the key contains no [.*]
|
|
37
|
+
const pathStart = arrayOrObject ? (/\[(.*?)\]/.exec(keyRaw) as RegExpExecArray)[1] : null;
|
|
38
|
+
|
|
39
|
+
// to be an object it has to contain a string within [], otherwise it is an array
|
|
40
|
+
const isObject = pathStart !== null && /[^\[\]]+/.test(pathStart) && ! /^\s+$/.test(pathStart) && ! /^\d+$/.test(pathStart);
|
|
41
|
+
|
|
42
|
+
// it is an array if it's not an object
|
|
43
|
+
const isArray = pathStart !== null && ! isObject;
|
|
44
|
+
|
|
45
|
+
// the actual key is, if array or object - anything before "[", if not then same as keyRaw
|
|
46
|
+
const key = isArray || isObject ? keyRaw.substring(0, keyRaw.indexOf('[')) : keyRaw;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
key,
|
|
50
|
+
value,
|
|
51
|
+
isArray,
|
|
52
|
+
isObject,
|
|
53
|
+
path: pathStart,
|
|
54
|
+
keyRaw
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
while (pairs.length > 0) {
|
|
59
|
+
const item = pairs.shift();
|
|
60
|
+
if (item) {
|
|
61
|
+
if (! item.isArray && ! item.isObject) {
|
|
62
|
+
// simple value
|
|
63
|
+
initialValue[item.key] = item.value;
|
|
64
|
+
} else if (item.isObject) {
|
|
65
|
+
// object
|
|
66
|
+
// return all properties of this object
|
|
67
|
+
const properties = [item].concat(pairs.filter((pair) => {
|
|
68
|
+
return pair.isObject && pair.key === item.key;
|
|
69
|
+
}));
|
|
70
|
+
// remove properties of this same object from pairs
|
|
71
|
+
pairs = pairs.filter((pair) => {
|
|
72
|
+
return ! (pair.isObject && pair.key === item.key);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let obj: Record<string, string | boolean > = {}
|
|
76
|
+
const simpleProperties = properties.filter((prop) => {
|
|
77
|
+
return prop.keyRaw === `${prop.key}[${prop.path}]`;
|
|
78
|
+
});
|
|
79
|
+
const complexProperties = properties.filter((prop) => {
|
|
80
|
+
return prop.keyRaw !== `${prop.key}[${prop.path}]`;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// handle simple properties
|
|
84
|
+
for (const property of simpleProperties) {
|
|
85
|
+
if (! property.path) {continue;}
|
|
86
|
+
obj[property.path] = property.value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// handle complex properties
|
|
90
|
+
const complexPropertyPathsResolved: Array<string> = [];
|
|
91
|
+
for (const property of complexProperties) {
|
|
92
|
+
if (! property.path) {continue;}
|
|
93
|
+
|
|
94
|
+
// property done
|
|
95
|
+
if (complexPropertyPathsResolved.includes(property.path)) {continue;}
|
|
96
|
+
|
|
97
|
+
const objectProperties = complexProperties.filter((prop) => {
|
|
98
|
+
return prop.path === property.path;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// mark as solved
|
|
102
|
+
complexPropertyPathsResolved.push(property.path as string);
|
|
103
|
+
|
|
104
|
+
obj[property.path] = queryStringDecode(objectProperties.map((prop) => {
|
|
105
|
+
const pathRemaining = prop.keyRaw.substring(prop.key.length + (prop.path?.length || 0) + 3);
|
|
106
|
+
const val = prop.value === true ? '' : `=${encodeURIComponent(prop.value)}`;
|
|
107
|
+
return `value[${pathRemaining.substring(0, pathRemaining.length - 1)}]${val}`;
|
|
108
|
+
}).join('&')).value as any;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
initialValue[item.key] = obj;
|
|
112
|
+
} else if (item.isArray) {
|
|
113
|
+
// array
|
|
114
|
+
// gather values
|
|
115
|
+
let arrayValues = [item].concat(pairs.filter((pair) => {
|
|
116
|
+
return pair.isArray && pair.key === item.key;
|
|
117
|
+
}));
|
|
118
|
+
// remove values gathered from pairs
|
|
119
|
+
pairs = pairs.filter((pair) => {
|
|
120
|
+
return !(pair.isArray && pair.key === item.key);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// order the array values, this only has effect if it is an ordered array (arr[0]=val&arr[1]=val)
|
|
124
|
+
arrayValues.sort((a, b) => {
|
|
125
|
+
if (a.path === b.path) {return 0;}
|
|
126
|
+
if (a.path && b.path && a.path.trim().length > 0 && b.path.trim().length > 0) {
|
|
127
|
+
const aIndex = parseInt(a.path);
|
|
128
|
+
const bIndex = parseInt(b.path);
|
|
129
|
+
return aIndex - bIndex;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return 0;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// arrayValues is either filled with simple values, or filled with object properties
|
|
136
|
+
const complexPropertyPathsResolved: Array<string> = [];
|
|
137
|
+
const arrayItems = arrayValues.map((value) => {
|
|
138
|
+
const simpleValue = value.keyRaw === `${value.key}[${value.path}]`;
|
|
139
|
+
if (simpleValue) {
|
|
140
|
+
return value.value;
|
|
141
|
+
}
|
|
142
|
+
// complex value (object)
|
|
143
|
+
if (complexPropertyPathsResolved.includes(value.path as string)) {
|
|
144
|
+
// already solved
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const objectProperties = arrayValues.filter((prop) => {
|
|
148
|
+
return prop.path === value.path;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// mark as solved
|
|
152
|
+
complexPropertyPathsResolved.push(value.path as string);
|
|
153
|
+
|
|
154
|
+
return queryStringDecode(objectProperties.map((prop) => {
|
|
155
|
+
const pathRemaining = prop.keyRaw.substring(prop.key.length + (prop.path?.length || 0) + 3);
|
|
156
|
+
return `value${pathRemaining.length > 0 ? `[${pathRemaining.substring(0, pathRemaining.length - 1)}]` : ''}=${encodeURIComponent(prop.value)}`;
|
|
157
|
+
}).join('&')).value as PostedDataDecoded;
|
|
158
|
+
}).filter((val) => {
|
|
159
|
+
return val !== null;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// console.log(arrayValues);
|
|
163
|
+
|
|
164
|
+
initialValue[item.key] = arrayItems;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return initialValue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// sometimes we want to use queryStringDeocde simply to parse a complex query string
|
|
173
|
+
// with nested keys into an object, with intent to assign a non-primitive value to it later
|
|
174
|
+
// for example decoding "obj[nested][key]" would produce { obj: { nested: { key: true } } }
|
|
175
|
+
// we might want to set the "key" to something other than true, or another primitive value
|
|
176
|
+
// so doing obj[nested][key]=[desiredValue] is impossible
|
|
177
|
+
// as the type of the value can't be coerced to string
|
|
178
|
+
// this function takes an object produced by queryStringDecode or a query string itself
|
|
179
|
+
// and the value to be populated, populates the key with value = true with given value
|
|
180
|
+
// and returns the populated object
|
|
181
|
+
export function queryStringDecodedSetValue(obj: PostedDataDecoded | string, value: any): LooseObject {
|
|
182
|
+
// obj given as string, decode it first to PostedDataDecoded
|
|
183
|
+
if (typeof obj === 'string') {
|
|
184
|
+
obj = queryStringDecode(obj);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// obj is either { key: true } or a nested object eventually ending with a value = true
|
|
188
|
+
// replace true with given value
|
|
189
|
+
// recursively search the object and find value = true, replace it with given file
|
|
190
|
+
const setValue = (obj: LooseObject) => {
|
|
191
|
+
for (const key in obj) {
|
|
192
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
193
|
+
// not here, resume recursively
|
|
194
|
+
setValue(obj[key]);
|
|
195
|
+
} else if (obj[key] === true) {
|
|
196
|
+
// found the place for the file, populate it
|
|
197
|
+
obj[key] = value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setValue(obj);
|
|
203
|
+
return obj;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// loop through given object, for each key, runs callbackEach providing the key and value to it
|
|
207
|
+
// this is a basic for ... in loop, but makes it more TS friendly
|
|
208
|
+
// object keys are always a string according to TS, which requires type casting
|
|
209
|
+
export function objectEach<T>(obj: T, callbackEach: (key: keyof T, value: T[keyof T]) => void): void {
|
|
210
|
+
for (const key in obj) {
|
|
211
|
+
callbackEach(key, obj[key]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// converts string-with-dashes to stringWithDashes
|
|
216
|
+
export function toCamelCase(dataKey: string, separator: string = '-'): string {
|
|
217
|
+
let index: number;
|
|
218
|
+
do {
|
|
219
|
+
index = dataKey.indexOf(separator);
|
|
220
|
+
if (index > -1) {
|
|
221
|
+
dataKey = dataKey.substring(0, index) + dataKey.substring(index + 1, index + 2).toUpperCase() + dataKey.substring(index + 2);
|
|
222
|
+
}
|
|
223
|
+
} while(index > -1);
|
|
224
|
+
return dataKey;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// camelCase to snake_case
|
|
228
|
+
export function toSnakeCase(str: string, joinWith: string = '_'): string {
|
|
229
|
+
let start = 0;
|
|
230
|
+
const parts = [];
|
|
231
|
+
if (str.length < 2) {
|
|
232
|
+
return str.toLowerCase();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// split in parts at capital letters
|
|
236
|
+
for (let i = 1; i < str.length; i++) {
|
|
237
|
+
if (str[i] !== str[i].toLowerCase()) {
|
|
238
|
+
// a capital letter
|
|
239
|
+
parts.push(str.substring(start, i).toLowerCase());
|
|
240
|
+
start = i;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// didn't do anything useful
|
|
245
|
+
if (start === 0) {
|
|
246
|
+
return str.toLowerCase();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// add last part
|
|
250
|
+
parts.push(str.substring(start).toLowerCase());
|
|
251
|
+
|
|
252
|
+
return parts.join(joinWith);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function capitalize(str: string): string {
|
|
256
|
+
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function isAsync(fn: Function): boolean {
|
|
260
|
+
return fn.constructor.name === 'AsyncFunction';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function randomString(len: number): string {
|
|
264
|
+
const charCodes: Uint8Array = new Uint8Array(len);
|
|
265
|
+
const generators = [
|
|
266
|
+
// uppercase letters
|
|
267
|
+
function(): number {
|
|
268
|
+
return 65 + Math.floor(Math.random() * 26);
|
|
269
|
+
},
|
|
270
|
+
// lowercase letters
|
|
271
|
+
function(): number {
|
|
272
|
+
return 97 + Math.floor(Math.random() * 26);
|
|
273
|
+
},
|
|
274
|
+
// numbers
|
|
275
|
+
function(): number {
|
|
276
|
+
return 48 + Math.floor(Math.random() * 10);
|
|
277
|
+
}
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
for (let i = 0; i < len; i++) {
|
|
281
|
+
charCodes[i] = generators[Math.floor(Math.random() * generators.length)]();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return String.fromCodePoint(...charCodes);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function unique<T>(arr: Array<T>): Array<T> {
|
|
288
|
+
return arr.reduce((prev, curr) => {
|
|
289
|
+
if (! prev.includes(curr)) {
|
|
290
|
+
prev.push(curr);
|
|
291
|
+
}
|
|
292
|
+
return prev;
|
|
293
|
+
}, [] as Array<T>);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function stripTags(contentWithHTML: string, keepTags: Array<string> = []): string {
|
|
297
|
+
if (contentWithHTML === undefined) {return ''};
|
|
298
|
+
return contentWithHTML.replaceAll(/<\s*\/?\s*[a-zA-Z]+[^>]*?>/g, (sub, index) => {
|
|
299
|
+
const keep = keepTags.some((kept) => {
|
|
300
|
+
const match = new RegExp(`^<\s*\/?\s*${kept}`);
|
|
301
|
+
return match.test(sub);
|
|
302
|
+
});
|
|
303
|
+
if (keep) {
|
|
304
|
+
return sub;
|
|
305
|
+
}
|
|
306
|
+
return sub.replaceAll('<', '<').replaceAll('>', '>');
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function base64ToBytes(base64: string) {
|
|
311
|
+
const binString = atob(base64);
|
|
312
|
+
// @ts-ignore
|
|
313
|
+
return Uint8Array.from(binString, (m) => m.codePointAt(0));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function bytesToBase64(bytes: Uint8Array) {
|
|
317
|
+
const binString = String.fromCodePoint(...bytes);
|
|
318
|
+
return btoa(binString);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function attributeValueToString(key: string, value: any): string {
|
|
322
|
+
return 'base64:' + bytesToBase64(new TextEncoder().encode(JSON.stringify({key, value})));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function attributeValueFromString(attributeValue: string): string|{
|
|
326
|
+
key: string,
|
|
327
|
+
value: any
|
|
328
|
+
} {
|
|
329
|
+
|
|
330
|
+
if (attributeValue.indexOf('base64:') === 0) {
|
|
331
|
+
try {
|
|
332
|
+
const decoded = new TextDecoder().decode(base64ToBytes(attributeValue.substring(7)));
|
|
333
|
+
|
|
334
|
+
if (decoded.indexOf('{') !== 0) {
|
|
335
|
+
// expected to start with "{", if not return as is
|
|
336
|
+
return attributeValue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const valObj = JSON.parse(decoded);
|
|
340
|
+
|
|
341
|
+
if (! ('value' in valObj) || ! ('key' in valObj)) {
|
|
342
|
+
// unrecognized object
|
|
343
|
+
return decoded;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return valObj;
|
|
347
|
+
|
|
348
|
+
} catch (e) {
|
|
349
|
+
return attributeValue;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return attributeValue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function attributeValueEscape(str: string): string {
|
|
356
|
+
return str.replaceAll('"', '"');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function isObject(item: any): boolean {
|
|
360
|
+
if (typeof window === 'undefined') {
|
|
361
|
+
return (item && typeof item === 'object' && !Array.isArray(item)) && ! Buffer.isBuffer(item);
|
|
362
|
+
}
|
|
363
|
+
return (item && typeof item === 'object' && !Array.isArray(item));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// deep comparison of 2 objects
|
|
367
|
+
export function equalDeep(a: LooseObject, b: LooseObject): boolean {
|
|
368
|
+
if (a === b) {
|
|
369
|
+
// same object
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const keysA = Object.keys(a);
|
|
374
|
+
const keysB = Object.keys(b);
|
|
375
|
+
|
|
376
|
+
if (keysA.length !== keysB.length) {
|
|
377
|
+
// key count different, not same
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// same number of keys
|
|
382
|
+
const keyDifferent = keysA.some((keyA) => {
|
|
383
|
+
return ! (keyA in b);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (keyDifferent) {
|
|
387
|
+
// some of the keys are different, not same
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// objects have the same keys, make sure values are also the same
|
|
392
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
393
|
+
const key = keysA[i];
|
|
394
|
+
const valA = a[key];
|
|
395
|
+
const valB = b[key];
|
|
396
|
+
const typeA = typeof valA;
|
|
397
|
+
const typeB = typeof valB;
|
|
398
|
+
|
|
399
|
+
if (valA === valB) {
|
|
400
|
+
// same value, continue
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (typeA !== typeB) {
|
|
405
|
+
// value type different, not same
|
|
406
|
+
return false;
|
|
407
|
+
} else {
|
|
408
|
+
// same type, if primitive, comapre values
|
|
409
|
+
if (typeA !== 'object') {
|
|
410
|
+
if (valA !== valB) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// values have the same type, if they were exact same they would already pass this iteration
|
|
417
|
+
// at this point primitives have been compared in the first check in the itration
|
|
418
|
+
// both types should be "object" at this point (object, array or null)
|
|
419
|
+
// null is also an object, make sure either both are null, or none is null
|
|
420
|
+
if ((valA === null && valB !== null) || (valA !== null && valB === null)) {
|
|
421
|
+
// one is null, other is not
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
// make sure either both are array, or none is array
|
|
427
|
+
const isArrayA = Array.isArray(valA);
|
|
428
|
+
const isArrayB = Array.isArray(valB);
|
|
429
|
+
if ((isArrayA && ! isArrayB) || (! isArrayA && isArrayB)) {
|
|
430
|
+
// one is array, other is not
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// at this point both values are either array or object
|
|
435
|
+
if (! isArrayA && ! isArrayB) {
|
|
436
|
+
// neither is an array, both are objects
|
|
437
|
+
// pass the values through equalsDeep, if not the same, objects are not the same either
|
|
438
|
+
if (! equalDeep(valA, valB)) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (isArrayA && isArrayB) {
|
|
444
|
+
// both are arrays
|
|
445
|
+
if (valA.length !== valB.length) {
|
|
446
|
+
// arrays of different length
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
for (let j = 0; j < valA.length; j++) {
|
|
452
|
+
if (! equalDeep({
|
|
453
|
+
value: valA[j]
|
|
454
|
+
}, {
|
|
455
|
+
value: valB[j]
|
|
456
|
+
})) {
|
|
457
|
+
// different value at index, not the same array, objects not same
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// all checks passed, same objects
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function mergeDeep(target: any, ...sources: Array<any>): LooseObject {
|
|
469
|
+
if (!sources.length) return target;
|
|
470
|
+
const source = sources.shift();
|
|
471
|
+
|
|
472
|
+
if (isObject(target) && isObject(source)) {
|
|
473
|
+
for (const key in source) {
|
|
474
|
+
if (isObject(source[key])) {
|
|
475
|
+
if (!target[key]) Object.assign(target, { [key]: {} });
|
|
476
|
+
mergeDeep(target[key], source[key]);
|
|
477
|
+
} else {
|
|
478
|
+
Object.assign(target, { [key]: source[key] });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
484
|
+
return target.concat(source);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return mergeDeep(target, ...sources);
|
|
488
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DataStore } from './DataStore.js';
|
|
2
|
+
import { ClientComponent } from './ClientComponent.js';
|
|
3
|
+
|
|
4
|
+
export class App {
|
|
5
|
+
root: ClientComponent;
|
|
6
|
+
store: DataStore = new DataStore();
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.root = new ClientComponent(null, 'root', document.body, this.store);
|
|
10
|
+
}
|
|
11
|
+
}
|