frontbacked-svg 5.0.0

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/src/utils.ts ADDED
@@ -0,0 +1,758 @@
1
+
2
+ import { Doc, Field, Fields, FieldsData, FileMap, MapWithName, MaskMap, TemplateData } from "./types.ts";
3
+ import { mergeImages, removeUpdateQueryString } from "./images-processor.ts";
4
+ import { textGenCodeParser } from "./textGenCodeParser.ts";
5
+ import { getCanvas } from "./imageHelper.ts";
6
+
7
+ const isNode = typeof window === 'undefined';
8
+
9
+ export const isVoid = (value: any) => {
10
+ return value === undefined || value === null
11
+ }
12
+ export const nullOrEmpty = (value: any) => {
13
+ return isVoid(value) || value.length == 0
14
+ }
15
+
16
+ /**
17
+ * Converts a Base64-encoded SVG string to a plain SVG string using modern APIs.
18
+ *
19
+ * @param {string} base64String - The Base64-encoded SVG string.
20
+ * @returns {string} The decoded SVG string.
21
+ */
22
+ export function base64ToSvg(base64String: string): string {
23
+ // Check if the base64 string starts with the SVG data URI scheme
24
+ const svgPrefix = 'data:image/svg+xml;base64,';
25
+ if (base64String.startsWith(svgPrefix)) {
26
+ base64String = base64String.slice(svgPrefix.length);
27
+ }
28
+
29
+ // Decode the base64 string to a byte array
30
+ const byteCharacters = atobLegacy(base64String);
31
+
32
+ // Convert the byte array to a typed array
33
+ const byteNumbers = new Array(byteCharacters.length);
34
+ for (let i = 0; i < byteCharacters.length; i++) {
35
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
36
+ }
37
+ const byteArray = new Uint8Array(byteNumbers);
38
+
39
+ // Convert the byte array to a string using TextDecoder
40
+ const decoder = new TextDecoder('utf-8');
41
+ const svgString = decoder.decode(byteArray);
42
+
43
+ return svgString;
44
+ }
45
+
46
+ /**
47
+ * Legacy atob function to convert Base64-encoded string to binary string.
48
+ * This is used for compatibility with older browsers.
49
+ *
50
+ * @param {string} base64String - The Base64-encoded string.
51
+ * @returns {string} The binary string.
52
+ */
53
+ function atobLegacy(base64String: string): string {
54
+ if (typeof Buffer !== 'undefined') {
55
+ // Node.js environment
56
+ return Buffer.from(base64String, 'base64').toString('binary');
57
+ } else if (typeof window !== 'undefined' && typeof window.atob === 'function') {
58
+ // Browser environment
59
+ return window.atob(base64String);
60
+ } else {
61
+ throw new Error('Base64 decoding not supported in this environment');
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Generates a random alphanumeric string of the specified length.
67
+ *
68
+ * @param {number} length - The desired length of the generated string.
69
+ * @returns {string} A random alphanumeric string of the specified length.
70
+ */
71
+ export function genId(length: number): string {
72
+ const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
73
+ let result = '';
74
+
75
+ for (let i = 0; i < length; i++) {
76
+ const randomIndex = Math.floor(Math.random() * characters.length);
77
+ result += characters.charAt(randomIndex);
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * Converts a Base64 string to a File (Browser) or Buffer (Node.js).
85
+ * @param base64String - The Base64 encoded string.
86
+ * @param fileName - The name of the output file.
87
+ * @returns File (Browser) or Buffer (Node.js)
88
+ */
89
+ export function base64ToFile(id: string, base64String: string): File | Buffer {
90
+ const matches = base64String.match(/^data:(.+);base64,(.+)$/);
91
+ if (!matches) {
92
+ throw new Error('Invalid Base64 string: ' + base64String? base64String.substring(0, base64String.length > 10? 9 : base64String.length - 1) : "NO base64String");
93
+ }
94
+
95
+ const mimeType = matches[1]; // Extract MIME type
96
+ const buffer = Buffer.from(matches[2], 'base64');
97
+
98
+ if (typeof window !== 'undefined') {
99
+ // Browser: Create a File object
100
+ // Create File object from Blob
101
+ const fileName = `${cleanFilename(id)}.${mimeType.split('/')[1]}`;
102
+ return new File([buffer], fileName, { type: mimeType });
103
+ } else {
104
+ // Node.js: Return Buffer
105
+ return buffer;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Converts an array of objects to a map using a specified attribute as the key.
111
+ *
112
+ * @param {Array<Object>} arr - The array of objects to be converted.
113
+ * @param {string} attr - The attribute to use as the key for the mapping.
114
+ * @returns {Object} - An object mapping each unique attribute value to its corresponding item.
115
+ */
116
+ export function arrayToMap(attr: string, arr: {[x: string]: any}[], keyParser?: (key: string) => string): {[x: string]: {[x: string]: any}} {
117
+ return arr.reduce((map, item) => {
118
+ var key = item[attr];
119
+ if(keyParser) key = keyParser(key)
120
+ if (key !== undefined) {
121
+ map[key] = item;
122
+ }
123
+ return map;
124
+ }, {});
125
+ }
126
+
127
+ /**
128
+ * Converts a JSON object into a File object.
129
+ *
130
+ * @param jsonObject - The JSON object to convert.
131
+ * @param fileName - The desired name for the File. Defaults to 'data.json'.
132
+ * @returns The File object containing the JSON data.
133
+ */
134
+ function jsonToFile(jsonObject: object, fileName: string = 'data.json'): File {
135
+ // Convert JSON object to string
136
+ const jsonString: string = JSON.stringify(jsonObject, null, 2);
137
+
138
+ // Create a Blob with the JSON string
139
+ const blob: Blob = new Blob([jsonString], { type: 'application/json' });
140
+
141
+ // Create a File from the Blob
142
+ const file: File = new File([blob], fileName, { type: 'application/json' });
143
+
144
+ return file;
145
+ }
146
+
147
+ // Function to order an object with Field values
148
+ export function orderOptions(obj: { [x: string]: Field }): { [x: string]: Field } {
149
+ // Convert object to an array of [key, value] pairs
150
+ const entries = Object.entries(obj);
151
+
152
+ // Sort the entries array based on the 'index' property of the values
153
+ entries.sort(([, a], [, b]) => {
154
+ const indexAStr = (a.index || "").toString();
155
+ const indexBStr = (b.index || "").toString();
156
+ return indexAStr.localeCompare(indexBStr);
157
+ });
158
+
159
+ // Convert the sorted entries back to an object
160
+ const sortedObj: { [x: string]: Field } = {};
161
+ for (const [key, value] of entries) {
162
+ sortedObj[key] = value;
163
+ }
164
+
165
+ return sortedObj;
166
+ }
167
+
168
+ export const orderByName = (dataMap: MapWithName): MapWithName => {
169
+ // Convert the map into an array of key-value pairs
170
+ const entries = Object.entries(dataMap);
171
+
172
+ // Sort the array by the `name` attribute of the values
173
+ entries.sort((a, b) => {
174
+ const nameA = a[1].name.toUpperCase(); // Ignore case while sorting
175
+ const nameB = b[1].name.toUpperCase(); // Ignore case while sorting
176
+
177
+ if (nameA < nameB) {
178
+ return -1;
179
+ }
180
+ if (nameA > nameB) {
181
+ return 1;
182
+ }
183
+ return 0; // Names must be equal
184
+ });
185
+
186
+ // Convert the sorted array back into an object
187
+ const sortedDataMap: MapWithName = {};
188
+ entries.forEach(([key, value]) => {
189
+ sortedDataMap[key] = value;
190
+ });
191
+
192
+ return sortedDataMap;
193
+ }
194
+
195
+ /**
196
+ * Updates the query string in a URL, adding or incrementing the 'updates' parameter.
197
+ * @param {string} url - The URL to update.
198
+ * @returns {string} - The updated URL.
199
+ */
200
+ export function isTheSameUrl(url?: string, url2?: string): boolean {
201
+ if(!url || !url2) return false
202
+ return isUploadedFileUrl(url) && removeUpdateQueryString(url).toLowerCase() === removeUpdateQueryString(url2).toLowerCase()
203
+ }
204
+
205
+ export function expandImagesMap(
206
+ imagesMap: FileMap,
207
+ onFile: (id: string, image: string) => Promise<FileMap | null>
208
+ ): Promise<FileMap> {
209
+ return new Promise(async (resolve, reject) => {
210
+ try {
211
+ const expandedMap: FileMap = { ...imagesMap }; // Start with a copy of the original map
212
+
213
+ for (const [id, image] of Object.entries(imagesMap)) {
214
+ if (image && isImageDataUrl(image)) {
215
+ const newFiles = await onFile(id, image);
216
+ if (newFiles) {
217
+ Object.assign(expandedMap, newFiles);
218
+ }
219
+ }
220
+ }
221
+
222
+ resolve(expandedMap)
223
+ } catch(e) {
224
+ reject(e)
225
+ }
226
+ })
227
+ }
228
+
229
+ export const mergeTemplateData = (storageHost: string, newTemplateData: TemplateData, oldTemplateData?: TemplateData | null): Promise<TemplateData> => {
230
+ return new Promise(async (resolve, reject) => {
231
+ //If this is not an update to an existing template. Such as in a new template upload.
232
+ if(!oldTemplateData) return resolve(newTemplateData)
233
+
234
+ const data: TemplateData = { } as TemplateData
235
+ data.svg = newTemplateData.svg
236
+
237
+ const errors: string[] = []
238
+
239
+ //Checking for fields merge errors
240
+ for(const [id, field] of Object.entries(oldTemplateData.fields)) {
241
+ const newField = newTemplateData.fields[id]
242
+ //Error when a field is missing in the new template
243
+ if(!newField) {
244
+ errors.push(`The ${field.type.split("_").join(" ")} field, "${field.name}" is missing in the new template.`)
245
+
246
+ } //Error when a field type has changed in the new template
247
+ else if(newField.type != field.type) {
248
+ errors.push(`The ${field.type.split("_").join(" ")} field, "${field.name}" is misrepresented as "${newField.type.split("_").join(" ")}" in the new template.`)
249
+
250
+ } //Error when a field is a select field, but it's missing options in the new template
251
+ else if(field.options && !newField?.options) {
252
+ errors.push(`The ${field.type.split("_").join(" ")} field, "${field.name}" is missing options in the new template.`)
253
+
254
+ } //Validating each option in the new template
255
+ else if(field.options && newField.options) {
256
+ for(const option of Object.values(field.options)) {
257
+ //Error when an option is missing in the new template
258
+ if(!newField.options[option.id]) {
259
+ errors.push(`The ${field.type.split("_").join(" ")} ${field.name} option, "${option.name}" is missing in the new template.`)
260
+
261
+ } //Error when a option type has changed in the new template
262
+ else if(newField.options[option.id].type != option.type) {
263
+ errors.push(`The ${field.type.split("_").join(" ")} ${field.name} option, "${option.name}" is misrepresented as "${newField.options[option.id].type.split("_").join(" ")}" in the new template.`)
264
+
265
+ }
266
+ }
267
+
268
+ }
269
+ }
270
+
271
+ //Checking for images merge errors
272
+ if(Object.keys(oldTemplateData.images || {}).length > 0 && Object.keys(newTemplateData.images || {}).length == 0) {
273
+ errors.push(`Images are missing in missing in the new template.`)
274
+
275
+ } else if(Object.keys(oldTemplateData.images || {}).length > 0) {
276
+ for(const id of Object.keys(oldTemplateData.images)) {
277
+ //Error when a an image is missing in the new template, and the is of the missing image is not a select option,
278
+ //since a new image might have been added from the user interface without its existence in the svg
279
+ if(!newTemplateData.images[id] && !id.includes(".select")) {
280
+ errors.push(`The image with the id, ${id} is missing in the new template.`)
281
+ }
282
+ }
283
+
284
+ }
285
+
286
+ //merge the fields
287
+ data.fields = { ...oldTemplateData.fields, ...newTemplateData.fields }
288
+ //Iterate the merged fields to order each select fields' options by name
289
+ for(const [id, field] of Object.entries(data.fields)) {
290
+ if(field.options) {
291
+ data.fields[id].options = orderOptions(field.options)
292
+ }
293
+ }
294
+
295
+ //merge the images
296
+ data.images = await mergeImages(storageHost, oldTemplateData.images, { ...oldTemplateData.images, ...newTemplateData.images })
297
+
298
+
299
+ if(errors.length > 0) {
300
+ return reject(new Error(errors.join("\n")))
301
+ }
302
+
303
+ resolve(data)
304
+ })
305
+ }
306
+
307
+ export const isUploadedFileUrl = ( url: string ) => {
308
+ return url.startsWith("http")
309
+ }
310
+
311
+ export const isImageDataUrl = ( url: string ) => {
312
+ return url.startsWith("data:image") || url.startsWith("data:img")
313
+ }
314
+
315
+ export const getImage = (id: string, images: FileMap, suffix?: string | null) => {
316
+ //suffix examples "_thumbnail", "_728", "_512"
317
+ //console.log("parsedSvg.geImage: ", id, images, suffix)
318
+ if(!id) return ""
319
+ var image = images[id]
320
+
321
+ if(id.includes(".")) {
322
+ //console.log("parsedSvg.randImages:1", id, suffix, image)
323
+ }
324
+ //If the image value is a reference to another image
325
+ if(image && !isUploadedFileUrl(image) && !isImageDataUrl(image)) {
326
+ //set the image as the value of the referenced image
327
+ image = images[`${images[id]}${suffix || ""}`]
328
+ if(id.includes(".")) {
329
+ //console.log("parsedSvg.randImages:2", id, suffix, image)
330
+ }
331
+
332
+ } else if(image) {
333
+ image = images[`${id}${suffix || ""}`]
334
+ if(id.includes(".")) {
335
+ //console.log("parsedSvg.randImages:3", id, suffix, `${id}${suffix || ""}`, !image? "--" : image.substring(0, 10))
336
+ }
337
+ }
338
+
339
+ //set the image as the fallback image if the specified resolution does not exist
340
+ if(!image && suffix) {
341
+ const idDeviceNumberSplit = id.split("_")
342
+ const idNumber = idDeviceNumberSplit[idDeviceNumberSplit.length - 1]
343
+ //Only fallback if the id is not a reference to a resolution
344
+ //This is done by checking the number at the end of the id
345
+ //If there's no number, then the id is not a resolution image
346
+ //It it is, it might be the id of a duplicated image. where the number stands for
347
+ // the duplication number instead of the resolution size
348
+ if(isNaN(Number(idNumber.trim())) || Number(idNumber.trim()) < 512) image = images[id]
349
+ }
350
+ if(id.includes(".")) {
351
+ //console.log("parsedSvg.randImages:4", id, suffix, image)
352
+ }
353
+
354
+ return image
355
+ }
356
+
357
+ export const getImageMask = (id: string, masks?: MaskMap | null) => {
358
+ if(!masks) return null
359
+ //suffix examples "_thumbnail", "_728", "_512"
360
+ var mask = masks[id]
361
+ //If the image value is a reference to another image mask
362
+ if(mask && typeof mask === "string") {
363
+ //set the image as the value of the referenced image mask
364
+ mask = masks[mask]
365
+ }
366
+
367
+ return mask
368
+ }
369
+
370
+ export function cleanFilename(text: string) {
371
+ // Remove or replace characters not safe for filenames
372
+ return text
373
+ .trim() // Remove whitespace from the beginning and end
374
+ .replace(/[/\\?%*:|"<>]/g, '') // Remove invalid characters for filenames
375
+ .replace(/\s+/g, '-'); // Replace spaces (or whitespace) with underscores
376
+ }
377
+
378
+ /**
379
+ * Converts a map of base64 images, converting PNG images to JPG format.
380
+ *
381
+ * @param {Map<string, string>} base64Map - A map where the key is a string and the value is a base64 image.
382
+ * @returns {Promise<Map<string, string>>} - A new map with PNG images converted to JPG.
383
+ */
384
+ export async function convertPngBase64ImagesToJpeg(base64Map: {[x: string]: string}): Promise<{[x: string]: string}> {
385
+ const updatedMap: {[x: string]: string} = {};
386
+
387
+ for (const [key, base64Image] of Object.entries(base64Map)) {
388
+ if (base64Image.startsWith("data:image/png") || base64Image.startsWith("data:img/png")) {
389
+ const jpgBase64 = await convertPngToJpg(base64Image);
390
+ updatedMap[key] = jpgBase64;
391
+ } else {
392
+ updatedMap[key] = base64Image;
393
+ }
394
+ }
395
+
396
+ return updatedMap;
397
+ }
398
+
399
+ export async function convertPngBase64ImagesWithUPNG(base64Map: {[x: string]: string}): Promise<{[x: string]: string}> {
400
+ const updatedMap: {[x: string]: string} = {};
401
+
402
+ for (const [key, base64Image] of Object.entries(base64Map)) {
403
+ if (base64Image.startsWith("data:image/png") || base64Image.startsWith("data:img/png")) {
404
+ const jpgBase64 = await convertPngWithUPNG(base64Image);
405
+ updatedMap[key] = jpgBase64;
406
+ } else {
407
+ updatedMap[key] = base64Image;
408
+ }
409
+ }
410
+
411
+ return updatedMap;
412
+ }
413
+
414
+ /**
415
+ * Converts a base64 PNG image to a base64 JPG image.
416
+ *
417
+ * @param {string} base64Png - The base64 string of the PNG image.
418
+ * @returns {Promise<string>} - A promise resolving to the base64 string of the converted JPG image.
419
+ */
420
+ function convertPngToJpg(base64Png: string): Promise<string> {
421
+ return new Promise((resolve, reject) => {
422
+ const img = new Image();
423
+ img.onload = () => {
424
+ const canvas = getCanvas(img.width, img.height);
425
+ const ctx = canvas.getContext("2d") as any;
426
+ canvas.width = img.width;
427
+ canvas.height = img.height;
428
+
429
+ if(!ctx) return reject(new Error("No canvas context"))
430
+
431
+ // Fill canvas with a white background to avoid transparency.
432
+ //ctx.fillStyle = "#fff";
433
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
434
+
435
+ // Draw the PNG image onto the canvas.
436
+ ctx.drawImage(img, 0, 0);
437
+
438
+ // Convert the canvas content to JPG base64.
439
+ const jpgBase64 = canvas.toDataURL("image/jpeg", 0.8); // 0.8 for quality adjustment.
440
+ resolve(jpgBase64);
441
+ };
442
+ img.onerror = reject;
443
+ img.src = base64Png;
444
+ });
445
+ }
446
+
447
+ function convertPngWithUPNG(base64Png: string): Promise<string> {
448
+ return new Promise((resolve, reject) => {
449
+ reject(new Error(""))/*
450
+ const img = new Image();
451
+ img.onload = () => {
452
+ const canvas = document.createElement("canvas");
453
+ const ctx = canvas.getContext("2d");
454
+ canvas.width = img.width;
455
+ canvas.height = img.height;
456
+
457
+ if (!ctx) return reject(new Error("No canvas context"));
458
+
459
+ // Draw the PNG image onto the canvas.
460
+ ctx.drawImage(img, 0, 0);
461
+
462
+ // Extract raw image data from the canvas.
463
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
464
+ const rgbaData = imageData.data;
465
+
466
+ // Encode the raw image data to PNG using UPNG.
467
+ const pngArrayBuffer = UPNG.encode([new Uint8Array(rgbaData.buffer)], canvas.width, canvas.height, 0);
468
+ const pngBase64 = `data:image/png;base64,${btoa(Array.from(new Uint8Array(pngArrayBuffer)).map(byte => String.fromCharCode(byte)).join(""))}`;
469
+
470
+ resolve(pngBase64);
471
+ };
472
+ img.onerror = reject;
473
+ img.src = base64Png;*/
474
+ });
475
+ }
476
+
477
+ export const buildImageSelectName = (fieldName: string, name: string) => {
478
+ if(!name) return name
479
+ var optionName = name.substring(`${fieldName} `.length)
480
+ if (isNaN(Number(optionName))) {
481
+ return `${optionName}, ${fieldName}`;
482
+ }
483
+ var lastDigitPos = optionName.substring(optionName.length - 1)
484
+ switch (lastDigitPos) {
485
+ case "1":
486
+ return `${optionName}st ${fieldName}`
487
+ case "2":
488
+ return `${optionName}nd ${fieldName}`
489
+ case "3":
490
+ return `${optionName}rd ${fieldName}`
491
+ default:
492
+ return `${optionName}th ${fieldName}`
493
+ }
494
+ }
495
+
496
+ export const buildImageSelectNameReverse = (fieldName: string, formattedName: string) => {
497
+ if (!formattedName) return formattedName;
498
+
499
+
500
+ // Handle the case with ", fieldName"
501
+ if (formattedName.endsWith(`, ${fieldName}`)) {
502
+ const index = formattedName.lastIndexOf(`, ${fieldName}`)
503
+ return `${fieldName} ${formattedName.substring(0, index)}`;
504
+ }
505
+
506
+ // Check if the formattedName ends with the fieldName
507
+ if (formattedName.endsWith(` ${fieldName}`)) {
508
+ const withoutFieldName = formattedName.substring(0, formattedName.length - fieldName.length - 1);
509
+
510
+ // Check for ordinal suffix
511
+ const ordinalMatch = withoutFieldName.match(/(\d+)(st|nd|rd|th)$/);
512
+ if (ordinalMatch) {
513
+ return `${fieldName} ${ordinalMatch[1]}`;
514
+ }
515
+
516
+ // Handle the default case without ordinal suffix
517
+ return `${fieldName} ${withoutFieldName}`;
518
+ }
519
+
520
+
521
+ // If no match, return null or undefined as it doesn't fit the expected format
522
+ return formattedName;
523
+ };
524
+
525
+ // Utility function to determine input type based on the layer name
526
+ export const breakName = (name: string) => {
527
+ return name.replace(/_/g, " ").split("@")[0]
528
+ }
529
+
530
+ export const showField = (field: Field, fields: Fields, data: Doc) => {
531
+ const visibilityCode = field?.visibility_code;
532
+ if (visibilityCode && visibilityCode.length > 0 && (visibilityCode.includes("==") || visibilityCode.includes("!="))) {
533
+ let codeAndTarget = visibilityCode.split("==");
534
+ let isNot;
535
+ if(visibilityCode.includes("==")) {
536
+ codeAndTarget = visibilityCode.split("==");
537
+ isNot = false;
538
+
539
+ } else {
540
+ codeAndTarget = visibilityCode.split("!=");
541
+ isNot = true
542
+ }
543
+
544
+ const code = codeAndTarget[0].trim();
545
+ const targetExpression = codeAndTarget[1].trim();
546
+
547
+ // Helper function to parse code result
548
+ const parseCodeResult = (parser: (dataKey: string, dataAsKey: string) => string) => {
549
+ return textGenCodeParser(code, data, useDataForRandSeed, parser);
550
+ };
551
+
552
+ const codeResultForTextSelectName = parseCodeResult((dataKey, dataAsKey) => {
553
+ if (fields[dataKey]?.type === "text_select" && fields[dataKey]?.selections?.[dataAsKey]) {
554
+ return (fields[dataKey].selections[dataAsKey]?.name || "").trim();
555
+
556
+ }
557
+ return dataAsKey;
558
+ });
559
+
560
+ const codeResultForTextSelectValue = parseCodeResult((dataKey, dataAsKey) => {
561
+ if (fields[dataKey]?.type === "text_select" && fields[dataKey]?.selections?.[dataAsKey]) {
562
+ return (fields[dataKey].selections[dataAsKey]?.value || "").trim();
563
+
564
+ } else if (fields[dataKey]?.type === "image_select") {
565
+ const optionName = (fields[dataKey]?.options || {})[dataAsKey]?.name || ""
566
+ //console.log("showField", dataKey, "::", dataAsKey, "-1-", optionName)
567
+ return optionName;
568
+ }
569
+ return dataAsKey;
570
+ });
571
+
572
+ const codeResultForImageSelectTransformed = parseCodeResult((dataKey, dataAsKey) => {
573
+ if (fields[dataKey]?.type === "image_select") {
574
+ const transformed = buildImageSelectName(fields[dataKey].name, (fields[dataKey]?.options || {})[dataAsKey]?.name || "")
575
+ //console.log("showField", dataKey, "::", dataAsKey, "-2-", transformed)
576
+ return transformed;
577
+ }
578
+ return dataAsKey;
579
+ });
580
+
581
+ const codeResult = [codeResultForTextSelectName, codeResultForTextSelectValue, codeResultForImageSelectTransformed];
582
+
583
+ // Evaluate the target expression with logical operators
584
+ const evaluateExpression = (expression: string): boolean => {
585
+ const operands = expression.split(/\s*(\|\||&&)\s*/); // Split by `||` and `&&`
586
+
587
+ if(expression.includes("5th")) {
588
+ //console.log("showField", expression, "-1-", operands)
589
+ }
590
+
591
+ let result = checkOperand(operands[0]);
592
+
593
+ for (let i = 2; i < operands.length; i++) {
594
+ const currentOperand = checkOperand(operands[i]);
595
+ const operator = operands[i - 1];
596
+ if (operator === "||") {
597
+ result = result || currentOperand;
598
+ } else if (operator === "&&") {
599
+ result = result && currentOperand;
600
+ }
601
+ }
602
+
603
+ return result;
604
+ };
605
+
606
+ const checkOperand = (operand: string): boolean => {
607
+ const trimmedOperand = operand.trim();
608
+
609
+ if(!isNot) {
610
+ //return codeResult.some(result => result === trimmedOperand);
611
+ return codeResult.some(result => result.toLowerCase() === trimmedOperand.toLowerCase());
612
+
613
+ } else {
614
+ //return codeResult.some(result => result !== trimmedOperand);
615
+ //console.log("checkOperand", codeResult, `${codeResult[0].toLowerCase()} !== ${trimmedOperand.toLowerCase()}`, codeResult[0].toLowerCase() !== trimmedOperand.toLowerCase())
616
+ return codeResult.some(result => result.toLowerCase() !== trimmedOperand.toLowerCase());
617
+ }
618
+ };
619
+
620
+ return evaluateExpression(targetExpression);
621
+ }
622
+
623
+ return true;
624
+ };
625
+
626
+ export const splitSvgElementId = (id: string) => {
627
+ const [ name, typeAndSelectIndex ] = id.split(".", 2)
628
+
629
+ if(!typeAndSelectIndex) return { name }
630
+
631
+ const [ type, selectIndex ] = [
632
+ typeAndSelectIndex.split("_")[0],
633
+ typeAndSelectIndex.split("_").slice(1).join(" ")
634
+ ]
635
+
636
+ return { name, type, selectIndex, typeAndSelectIndex }
637
+ }
638
+
639
+ export const useDataForRandSeed = (key: string): boolean => {
640
+ const { type } = splitSvgElementId(key)
641
+ return type && ["text", "textarea"].includes(type)? true : false
642
+ }
643
+
644
+ export const valueOfParseValue = (key: string, value: string, data?: FieldsData, fields?: Fields) => {
645
+ if(fields && data && ["gen", "date"].includes(fields[key]?.type)) {
646
+ value = textGenCodeParser(fields[key].code, data, useDataForRandSeed, (dataKey, dataAsKey) => {
647
+ if(fields[dataKey]?.type == "text_select") {
648
+ return ((fields[dataKey].selections || {})[dataAsKey]?.value || "").trim()
649
+
650
+ }
651
+ return dataAsKey
652
+ })
653
+ }
654
+
655
+ //console.log("formatDate.value:", key, value, data, fields)
656
+
657
+ return value
658
+ }
659
+
660
+ export const actionsStorageKey = (collectionName: string, id: string) => {
661
+ return `/${collectionName}/${id}`.toLowerCase()
662
+ }
663
+ export const fileFieldStorageKey = (collectionName: string, id: string, fieldName: string) => {
664
+ return `/${collectionName}/${id}/${fieldName}`.toLowerCase()
665
+ }
666
+ export const fileDocStorageKey = (collectionName: string, id: string) => {
667
+ return `/${collectionName}/${id}`.toLowerCase()
668
+ }
669
+ export const fileCollectionStorageKey = (collectionName: string) => {
670
+ return `/${collectionName}`.toLowerCase()
671
+ }
672
+
673
+ export const isBrowser = () => {
674
+ return typeof window !== "undefined"
675
+ }
676
+
677
+ export const saveFileFieldFile = (collectionName: string, id: string, fieldName: string, value: string) => {
678
+ if(!isBrowser() || !window?.localStorage) return
679
+
680
+ try {
681
+ window?.localStorage.setItem(fileFieldStorageKey(collectionName, id, fieldName), value)
682
+
683
+ } catch(e) {
684
+ window?.localStorage.clear()
685
+ window?.localStorage.setItem(fileFieldStorageKey(collectionName, id, fieldName), value)
686
+ }
687
+ }
688
+
689
+ export interface FileFieldsFiles {
690
+ [x: string]: string
691
+ }
692
+
693
+ export const getFileFieldFile = (collectionName: string, id: string, fieldName: string, fileFieldTypesDefaultFiles?: FileFieldsFiles): string | null => {
694
+ if(!isBrowser() || !window?.localStorage) {
695
+ const { type, name } = splitSvgElementId(fieldName)
696
+ if(!type || !fileFieldTypesDefaultFiles) return null
697
+
698
+ if(type == "upload" && name.toLowerCase().split(" ").includes("logo")) return fileFieldTypesDefaultFiles.logo
699
+ return fileFieldTypesDefaultFiles[`${type}`]
700
+
701
+ }
702
+ return window?.localStorage.getItem(fileFieldStorageKey(collectionName, id, fieldName))
703
+ }
704
+
705
+ export const moveFileFieldFile = (collectionName: string, oldId: string, newId: string, fieldName: string, fileFieldTypesDefaultFiles?: FileFieldsFiles): Promise<void> => {
706
+ return new Promise((resolve, reject) => {
707
+ if(!isBrowser() || !window?.localStorage) return resolve()
708
+ const file = getFileFieldFile(collectionName, oldId, fieldName, fileFieldTypesDefaultFiles)
709
+ if(file) {
710
+ saveFileFieldFile(collectionName, newId, fieldName, file)
711
+ window?.localStorage.removeItem(fileFieldStorageKey(collectionName, oldId, fieldName))
712
+ resolve()
713
+
714
+ } else {
715
+ reject(new Error("No file at old ID"))
716
+ }
717
+ })
718
+ }
719
+
720
+ export const deleteFileFieldFile = (collectionName: string, id: string, fieldName: string, fileFieldTypesDefaultFiles?: FileFieldsFiles): Promise<void> => {
721
+ return new Promise((resolve, reject) => {
722
+ if(!isBrowser() || !window?.localStorage) return resolve()
723
+ const file = getFileFieldFile(collectionName, id, fieldName, fileFieldTypesDefaultFiles)
724
+ if(file) {
725
+ window?.localStorage.removeItem(fileFieldStorageKey(collectionName, id, fieldName))
726
+ resolve()
727
+
728
+ } else {
729
+ resolve()
730
+ }
731
+ })
732
+ }
733
+
734
+ export const deleteDataWithKeyPrefix = (keyPrefix: string) => {
735
+ if(!isBrowser() || !window?.localStorage) return
736
+ for (let key in window?.localStorage) {
737
+ if (key.startsWith(keyPrefix)) {
738
+ window?.localStorage.removeItem(key);
739
+ }
740
+ }
741
+ }
742
+
743
+ export const setR2Host = (url: string, r2Domain: string) => {
744
+ if(!url) return url
745
+ if (url.startsWith('/')) {
746
+ return `https://${r2Domain}${url}`;
747
+ }
748
+
749
+ try {
750
+ const parsedUrl = new URL(url);
751
+ parsedUrl.host = r2Domain;
752
+ parsedUrl.protocol = 'https:'; // force https
753
+ return parsedUrl.toString();
754
+ } catch (err) {
755
+ console.error('Invalid URL:', url);
756
+ return url; // fallback
757
+ }
758
+ };