app-manager-edge-worker 1.0.6 → 1.0.7

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.
@@ -1,340 +1,340 @@
1
- /**
2
- * Transformation and text extraction utilities for the Edge Worker.
3
- */
4
-
5
- /**
6
- * Safely extracts text from Cheerio elements while preserving block/boundary line breaks (\n).
7
- * Prevents glued text like "678.0013.30 (2.00%)" caused by native $(el).text() concatenation.
8
- *
9
- * @param {cheerio.CheerioAPI} $
10
- * @param {cheerio.Cheerio} el
11
- * @returns {string}
12
- */
13
- export function extractSmartCheerioText($, el) {
14
- if (!el || el.length === 0) return "";
15
- try {
16
- const clone = $(el).clone();
17
- clone.find("br").replaceWith("\n");
18
- clone.find("div, p, span, small, button, li, h1, h2, h3, h4, h5, h6, tr, td, th, section, article, header, footer, nav, aside, dt, dd").before("\n").after("\n");
19
- return clone
20
- .text()
21
- .split("\n")
22
- .map((s) => s.trim())
23
- .filter(Boolean)
24
- .join("\n");
25
- } catch {
26
- return $(el).text().trim();
27
- }
28
- }
29
-
30
- /**
31
- * Executes a single transformation step safely on an input value.
32
- *
33
- * @param {*} value
34
- * @param {Object} step
35
- * @returns {*}
36
- */
37
- export function applyTransformStep(value, step) {
38
- if (!step) return value;
39
-
40
- const op = (step.operation || step.op || "").toLowerCase();
41
- if (!op) return value;
42
-
43
- if (op === "default") {
44
- if (value === null || value === undefined || value === "") {
45
- const defVal = step.value !== undefined ? step.value : step.params?.value;
46
- return defVal !== undefined ? defVal : null;
47
- }
48
- return value;
49
- }
50
-
51
- if (value === null || value === undefined) {
52
- return value;
53
- }
54
-
55
- switch (op) {
56
- case "trim": {
57
- return typeof value === "string" ? value.trim() : String(value).trim();
58
- }
59
-
60
- case "lowercase": {
61
- return typeof value === "string" ? value.toLowerCase() : String(value).toLowerCase();
62
- }
63
-
64
- case "uppercase": {
65
- return typeof value === "string" ? value.toUpperCase() : String(value).toUpperCase();
66
- }
67
-
68
- case "remove": {
69
- const toRemove = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
70
- if (!toRemove) return value;
71
- return String(value).split(toRemove).join("");
72
- }
73
-
74
- case "remove_prefix": {
75
- const prefix = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
76
- if (!prefix) return value;
77
- const strVal = String(value).trim();
78
- if (strVal.startsWith(prefix)) {
79
- return strVal.slice(prefix.length).trim();
80
- }
81
- return strVal;
82
- }
83
-
84
- case "remove_suffix": {
85
- const suffix = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
86
- if (!suffix) return value;
87
- const strVal = String(value).trim();
88
- if (strVal.endsWith(suffix)) {
89
- return strVal.slice(0, strVal.length - suffix.length).trim();
90
- }
91
- return strVal;
92
- }
93
-
94
- case "replace": {
95
- const fromStr = step.from !== undefined ? String(step.from) : step.params?.from !== undefined ? String(step.params.from) : "";
96
- const toStr = step.to !== undefined ? String(step.to) : step.params?.to !== undefined ? String(step.params.to) : "";
97
- return String(value).split(fromStr).join(toStr);
98
- }
99
-
100
- case "regex_extract": {
101
- const pattern = step.pattern || step.params?.pattern;
102
- if (!pattern) return value;
103
- try {
104
- const regex = new RegExp(pattern);
105
- const match = String(value).match(regex);
106
- if (!match) return null;
107
- return match[1] !== undefined ? match[1] : match[0];
108
- } catch {
109
- return value;
110
- }
111
- }
112
-
113
- case "regex_replace": {
114
- const pattern = step.pattern || step.params?.pattern;
115
- if (!pattern) return value;
116
- try {
117
- const regex = new RegExp(pattern, "g");
118
- const toStr = step.to !== undefined ? String(step.to) : step.params?.to !== undefined ? String(step.params.to) : "";
119
- return String(value).replace(regex, toStr);
120
- } catch {
121
- return value;
122
- }
123
- }
124
-
125
- case "split": {
126
- let sep = step.separator !== undefined ? String(step.separator) : step.params?.separator !== undefined ? String(step.params.separator) : step.params?.delimiter !== undefined ? String(step.params.delimiter) : step.value !== undefined ? String(step.value) : "\n";
127
- if (sep === "\\n") sep = "\n";
128
- else if (sep === "\\t") sep = "\t";
129
- const parts = String(value).split(sep).map((s) => s.trim());
130
- const index = step.index !== undefined ? step.index : step.params?.index;
131
- if (index !== undefined && index !== null) {
132
- const idx = parseInt(index, 10);
133
- if (isNaN(idx)) return null;
134
- const targetIdx = idx < 0 ? parts.length + idx : idx;
135
- return targetIdx >= 0 && targetIdx < parts.length ? parts[targetIdx] : null;
136
- }
137
- return parts;
138
- }
139
-
140
- case "first_line": {
141
- const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
142
- return parts.length > 0 ? parts[0] : "";
143
- }
144
-
145
- case "second_line": {
146
- const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
147
- return parts.length > 1 ? parts[1] : "";
148
- }
149
-
150
- case "line": {
151
- const idx = parseInt(step.index ?? step.value ?? step.params?.index ?? step.params?.value ?? 0, 10);
152
- const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
153
- if (isNaN(idx)) return parts[0] || "";
154
- const targetIdx = idx < 0 ? parts.length + idx : idx;
155
- return targetIdx >= 0 && targetIdx < parts.length ? parts[targetIdx] : "";
156
- }
157
-
158
- case "contains":
159
- case "includes":
160
- case "has_badge": {
161
- const searchStr = step.value !== undefined ? String(step.value) : step.pattern !== undefined ? String(step.pattern) : step.params?.badgeKeyword !== undefined ? String(step.params.badgeKeyword) : step.params?.value !== undefined ? String(step.params.value) : "";
162
- if (!searchStr) return false;
163
- const caseSensitive = step.caseSensitive === true || step.params?.caseSensitive === true;
164
- const strVal = String(value);
165
- return caseSensitive
166
- ? strVal.includes(searchStr)
167
- : strVal.toLowerCase().includes(searchStr.toLowerCase());
168
- }
169
-
170
- case "cast": {
171
- const targetType = (step.targetType || step.params?.targetType || step.type || step.value || "string").toLowerCase();
172
- return castValueToType(value, targetType);
173
- }
174
-
175
- case "join": {
176
- const delimiter = step.delimiter !== undefined ? String(step.delimiter) : step.value !== undefined ? String(step.value) : ", ";
177
- return Array.isArray(value) ? value.join(delimiter) : String(value);
178
- }
179
-
180
- case "currency_cleanup": {
181
- if (typeof value === "number") return value;
182
- const str = String(value).trim();
183
- const cleaned = str
184
- .replace(/[\u20B9$€£¥₹]/g, "")
185
- .replace(/\b(USD|EUR|GBP|INR|CAD|AUD|JPY|CHF|AED|Rs\.?|Rupees?)\b/gi, "")
186
- .replace(/\s+/g, " ")
187
- .replace(/,/g, "")
188
- .trim();
189
- const numMatch = cleaned.match(/[-+]?[0-9]+(?:\.[0-9]+)?/);
190
- return numMatch ? parseFloat(numMatch[0]) : null;
191
- }
192
-
193
- case "number":
194
- case "decimal": {
195
- if (typeof value === "number") return value;
196
- const str = String(value).trim().replace(/,/g, "");
197
- const numMatch = str.match(/[-+]?[0-9]+(?:\.[0-9]+)?/);
198
- return numMatch ? parseFloat(numMatch[0]) : null;
199
- }
200
-
201
- case "integer": {
202
- if (typeof value === "number") return Math.round(value);
203
- const str = String(value).trim().replace(/,/g, "");
204
- const numMatch = str.match(/[-+]?[0-9]+/);
205
- return numMatch ? parseInt(numMatch[0], 10) : null;
206
- }
207
-
208
- case "parse_boolean":
209
- case "boolean": {
210
- if (typeof value === "boolean") return value;
211
- const str = String(value).trim().toLowerCase();
212
- if (["true", "yes", "y", "1", "in stock", "available", "on", "active"].includes(str)) return true;
213
- if (["false", "no", "n", "0", "out of stock", "unavailable", "off", "inactive"].includes(str)) return false;
214
- return Boolean(value);
215
- }
216
-
217
- case "has_value":
218
- case "is_not_empty":
219
- case "not_empty":
220
- case "exists": {
221
- if (value === null || value === undefined) return false;
222
- const lineIdx = step.lineIndex !== undefined ? step.lineIndex : step.params?.lineIndex !== undefined ? step.params?.lineIndex : step.index !== undefined ? step.index : step.params?.index;
223
- if (lineIdx !== undefined && lineIdx !== null && lineIdx !== "") {
224
- const idx = parseInt(lineIdx, 10);
225
- const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
226
- const targetIdx = idx > 0 ? idx - 1 : idx; // 1-indexed line number
227
- return targetIdx >= 0 && targetIdx < parts.length && parts[targetIdx].length > 0;
228
- }
229
- const str = String(value).trim();
230
- return str.length > 0 && str.toLowerCase() !== "false" && str !== "0";
231
- }
232
-
233
- case "custom":
234
- case "expression":
235
- case "script": {
236
- const code = step.code || step.expression || step.value || step.script || step.params?.code || step.params?.expression || step.params?.script;
237
- if (!code || typeof code !== "string") return value;
238
- try {
239
- const trimmed = code.trim();
240
- if (/\b(process|require|import|global|Buffer|__dirname|__filename|eval|Function)\b/.test(trimmed)) {
241
- return value;
242
- }
243
-
244
- // Auto-quote unquoted string after == or === or != or !== e.g. == Vol Shocker -> == "Vol Shocker"
245
- let sanitizedCode = trimmed.replace(/([=!]==?\s*)([A-Za-z][A-Za-z0-9 _-]*[A-Za-z0-9])(?=\s*$|\s*[;)])/g, (match, op, str) => {
246
- const trimmedStr = str.trim();
247
- if (['true', 'false', 'null', 'undefined', 'value', 'raw', 'lines'].includes(trimmedStr)) return match;
248
- if (trimmedStr.startsWith('"') || trimmedStr.startsWith("'")) return match;
249
- return `${op}"${trimmedStr}"`;
250
- });
251
-
252
- const lines = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
253
- const line = (n) => {
254
- const idx = parseInt(n, 10);
255
- const targetIdx = idx > 0 ? idx - 1 : idx;
256
- return lines[targetIdx] || "";
257
- };
258
- const hasLine = (n) => {
259
- const idx = parseInt(n, 10);
260
- const targetIdx = idx > 0 ? idx - 1 : idx;
261
- return targetIdx >= 0 && targetIdx < lines.length && lines[targetIdx].length > 0;
262
- };
263
- const has = (k) => String(value).toLowerCase().includes(String(k).toLowerCase());
264
- const contains = has;
265
- const hasValue = (v = value) => v !== null && v !== undefined && String(v).trim().length > 0;
266
- const notEmpty = hasValue;
267
-
268
- const fn = sanitizedCode.includes("return ")
269
- ? new Function("value", "raw", "lines", "line", "hasLine", "has", "contains", "hasValue", "notEmpty", sanitizedCode)
270
- : new Function("value", "raw", "lines", "line", "hasLine", "has", "contains", "hasValue", "notEmpty", `return (${sanitizedCode});`);
271
- const result = fn(value, value, lines, line, hasLine, has, contains, hasValue, notEmpty);
272
- return result !== undefined ? result : value;
273
- } catch {
274
- return value;
275
- }
276
- }
277
-
278
- default:
279
- return value;
280
- }
281
- }
282
-
283
- /**
284
- * Runs an array of transform steps sequentially on a raw extracted value.
285
- *
286
- * @param {*} rawValue
287
- * @param {Array<Object>} pipeline
288
- * @returns {*}
289
- */
290
- export function runTransformPipeline(rawValue, pipeline = []) {
291
- if (!Array.isArray(pipeline) || pipeline.length === 0) {
292
- return rawValue;
293
- }
294
-
295
- let current = rawValue;
296
- for (const step of pipeline) {
297
- current = applyTransformStep(current, step);
298
- }
299
- return current;
300
- }
301
-
302
- /**
303
- * Casts a transformed value to the target data type.
304
- *
305
- * @param {*} val
306
- * @param {string} type
307
- * @returns {*}
308
- */
309
- export function castValueToType(val, type = "string") {
310
- if (val === null || val === undefined) return val;
311
-
312
- switch (type) {
313
- case "integer": {
314
- if (typeof val === "number") return Math.round(val);
315
- const str = String(val).replace(/,/g, "").trim();
316
- const num = parseInt(str, 10);
317
- return isNaN(num) ? null : num;
318
- }
319
- case "number":
320
- case "decimal": {
321
- if (typeof val === "number") return val;
322
- const str = String(val).replace(/[\u20B9$€£¥₹]/g, "").replace(/,/g, "").trim();
323
- const num = parseFloat(str);
324
- return isNaN(num) ? null : num;
325
- }
326
- case "boolean": {
327
- if (typeof val === "boolean") return val;
328
- const str = String(val).trim().toLowerCase();
329
- return ["true", "yes", "1", "in stock", "available"].includes(str);
330
- }
331
- case "date": {
332
- if (val instanceof Date) return val.toISOString();
333
- const parsed = Date.parse(String(val));
334
- return !isNaN(parsed) ? new Date(parsed).toISOString() : String(val);
335
- }
336
- case "string":
337
- default:
338
- return typeof val === "object" ? JSON.stringify(val) : String(val);
339
- }
340
- }
1
+ /**
2
+ * Transformation and text extraction utilities for the Edge Worker.
3
+ */
4
+
5
+ /**
6
+ * Safely extracts text from Cheerio elements while preserving block/boundary line breaks (\n).
7
+ * Prevents glued text like "678.0013.30 (2.00%)" caused by native $(el).text() concatenation.
8
+ *
9
+ * @param {cheerio.CheerioAPI} $
10
+ * @param {cheerio.Cheerio} el
11
+ * @returns {string}
12
+ */
13
+ export function extractSmartCheerioText($, el) {
14
+ if (!el || el.length === 0) return "";
15
+ try {
16
+ const clone = $(el).clone();
17
+ clone.find("br").replaceWith("\n");
18
+ clone.find("div, p, span, small, button, li, h1, h2, h3, h4, h5, h6, tr, td, th, section, article, header, footer, nav, aside, dt, dd").before("\n").after("\n");
19
+ return clone
20
+ .text()
21
+ .split("\n")
22
+ .map((s) => s.trim())
23
+ .filter(Boolean)
24
+ .join("\n");
25
+ } catch {
26
+ return $(el).text().trim();
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Executes a single transformation step safely on an input value.
32
+ *
33
+ * @param {*} value
34
+ * @param {Object} step
35
+ * @returns {*}
36
+ */
37
+ export function applyTransformStep(value, step) {
38
+ if (!step) return value;
39
+
40
+ const op = (step.operation || step.op || "").toLowerCase();
41
+ if (!op) return value;
42
+
43
+ if (op === "default") {
44
+ if (value === null || value === undefined || value === "") {
45
+ const defVal = step.value !== undefined ? step.value : step.params?.value;
46
+ return defVal !== undefined ? defVal : null;
47
+ }
48
+ return value;
49
+ }
50
+
51
+ if (value === null || value === undefined) {
52
+ return value;
53
+ }
54
+
55
+ switch (op) {
56
+ case "trim": {
57
+ return typeof value === "string" ? value.trim() : String(value).trim();
58
+ }
59
+
60
+ case "lowercase": {
61
+ return typeof value === "string" ? value.toLowerCase() : String(value).toLowerCase();
62
+ }
63
+
64
+ case "uppercase": {
65
+ return typeof value === "string" ? value.toUpperCase() : String(value).toUpperCase();
66
+ }
67
+
68
+ case "remove": {
69
+ const toRemove = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
70
+ if (!toRemove) return value;
71
+ return String(value).split(toRemove).join("");
72
+ }
73
+
74
+ case "remove_prefix": {
75
+ const prefix = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
76
+ if (!prefix) return value;
77
+ const strVal = String(value).trim();
78
+ if (strVal.startsWith(prefix)) {
79
+ return strVal.slice(prefix.length).trim();
80
+ }
81
+ return strVal;
82
+ }
83
+
84
+ case "remove_suffix": {
85
+ const suffix = step.value !== undefined ? String(step.value) : step.params?.value !== undefined ? String(step.params.value) : "";
86
+ if (!suffix) return value;
87
+ const strVal = String(value).trim();
88
+ if (strVal.endsWith(suffix)) {
89
+ return strVal.slice(0, strVal.length - suffix.length).trim();
90
+ }
91
+ return strVal;
92
+ }
93
+
94
+ case "replace": {
95
+ const fromStr = step.from !== undefined ? String(step.from) : step.params?.from !== undefined ? String(step.params.from) : "";
96
+ const toStr = step.to !== undefined ? String(step.to) : step.params?.to !== undefined ? String(step.params.to) : "";
97
+ return String(value).split(fromStr).join(toStr);
98
+ }
99
+
100
+ case "regex_extract": {
101
+ const pattern = step.pattern || step.params?.pattern;
102
+ if (!pattern) return value;
103
+ try {
104
+ const regex = new RegExp(pattern);
105
+ const match = String(value).match(regex);
106
+ if (!match) return null;
107
+ return match[1] !== undefined ? match[1] : match[0];
108
+ } catch {
109
+ return value;
110
+ }
111
+ }
112
+
113
+ case "regex_replace": {
114
+ const pattern = step.pattern || step.params?.pattern;
115
+ if (!pattern) return value;
116
+ try {
117
+ const regex = new RegExp(pattern, "g");
118
+ const toStr = step.to !== undefined ? String(step.to) : step.params?.to !== undefined ? String(step.params.to) : "";
119
+ return String(value).replace(regex, toStr);
120
+ } catch {
121
+ return value;
122
+ }
123
+ }
124
+
125
+ case "split": {
126
+ let sep = step.separator !== undefined ? String(step.separator) : step.params?.separator !== undefined ? String(step.params.separator) : step.params?.delimiter !== undefined ? String(step.params.delimiter) : step.value !== undefined ? String(step.value) : "\n";
127
+ if (sep === "\\n") sep = "\n";
128
+ else if (sep === "\\t") sep = "\t";
129
+ const parts = String(value).split(sep).map((s) => s.trim());
130
+ const index = step.index !== undefined ? step.index : step.params?.index;
131
+ if (index !== undefined && index !== null) {
132
+ const idx = parseInt(index, 10);
133
+ if (isNaN(idx)) return null;
134
+ const targetIdx = idx < 0 ? parts.length + idx : idx;
135
+ return targetIdx >= 0 && targetIdx < parts.length ? parts[targetIdx] : null;
136
+ }
137
+ return parts;
138
+ }
139
+
140
+ case "first_line": {
141
+ const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
142
+ return parts.length > 0 ? parts[0] : "";
143
+ }
144
+
145
+ case "second_line": {
146
+ const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
147
+ return parts.length > 1 ? parts[1] : "";
148
+ }
149
+
150
+ case "line": {
151
+ const idx = parseInt(step.index ?? step.value ?? step.params?.index ?? step.params?.value ?? 0, 10);
152
+ const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
153
+ if (isNaN(idx)) return parts[0] || "";
154
+ const targetIdx = idx < 0 ? parts.length + idx : idx;
155
+ return targetIdx >= 0 && targetIdx < parts.length ? parts[targetIdx] : "";
156
+ }
157
+
158
+ case "contains":
159
+ case "includes":
160
+ case "has_badge": {
161
+ const searchStr = step.value !== undefined ? String(step.value) : step.pattern !== undefined ? String(step.pattern) : step.params?.badgeKeyword !== undefined ? String(step.params.badgeKeyword) : step.params?.value !== undefined ? String(step.params.value) : "";
162
+ if (!searchStr) return false;
163
+ const caseSensitive = step.caseSensitive === true || step.params?.caseSensitive === true;
164
+ const strVal = String(value);
165
+ return caseSensitive
166
+ ? strVal.includes(searchStr)
167
+ : strVal.toLowerCase().includes(searchStr.toLowerCase());
168
+ }
169
+
170
+ case "cast": {
171
+ const targetType = (step.targetType || step.params?.targetType || step.type || step.value || "string").toLowerCase();
172
+ return castValueToType(value, targetType);
173
+ }
174
+
175
+ case "join": {
176
+ const delimiter = step.delimiter !== undefined ? String(step.delimiter) : step.value !== undefined ? String(step.value) : ", ";
177
+ return Array.isArray(value) ? value.join(delimiter) : String(value);
178
+ }
179
+
180
+ case "currency_cleanup": {
181
+ if (typeof value === "number") return value;
182
+ const str = String(value).trim();
183
+ const cleaned = str
184
+ .replace(/[\u20B9$€£¥₹]/g, "")
185
+ .replace(/\b(USD|EUR|GBP|INR|CAD|AUD|JPY|CHF|AED|Rs\.?|Rupees?)\b/gi, "")
186
+ .replace(/\s+/g, " ")
187
+ .replace(/,/g, "")
188
+ .trim();
189
+ const numMatch = cleaned.match(/[-+]?[0-9]+(?:\.[0-9]+)?/);
190
+ return numMatch ? parseFloat(numMatch[0]) : null;
191
+ }
192
+
193
+ case "number":
194
+ case "decimal": {
195
+ if (typeof value === "number") return value;
196
+ const str = String(value).trim().replace(/,/g, "");
197
+ const numMatch = str.match(/[-+]?[0-9]+(?:\.[0-9]+)?/);
198
+ return numMatch ? parseFloat(numMatch[0]) : null;
199
+ }
200
+
201
+ case "integer": {
202
+ if (typeof value === "number") return Math.round(value);
203
+ const str = String(value).trim().replace(/,/g, "");
204
+ const numMatch = str.match(/[-+]?[0-9]+/);
205
+ return numMatch ? parseInt(numMatch[0], 10) : null;
206
+ }
207
+
208
+ case "parse_boolean":
209
+ case "boolean": {
210
+ if (typeof value === "boolean") return value;
211
+ const str = String(value).trim().toLowerCase();
212
+ if (["true", "yes", "y", "1", "in stock", "available", "on", "active"].includes(str)) return true;
213
+ if (["false", "no", "n", "0", "out of stock", "unavailable", "off", "inactive"].includes(str)) return false;
214
+ return Boolean(value);
215
+ }
216
+
217
+ case "has_value":
218
+ case "is_not_empty":
219
+ case "not_empty":
220
+ case "exists": {
221
+ if (value === null || value === undefined) return false;
222
+ const lineIdx = step.lineIndex !== undefined ? step.lineIndex : step.params?.lineIndex !== undefined ? step.params?.lineIndex : step.index !== undefined ? step.index : step.params?.index;
223
+ if (lineIdx !== undefined && lineIdx !== null && lineIdx !== "") {
224
+ const idx = parseInt(lineIdx, 10);
225
+ const parts = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
226
+ const targetIdx = idx > 0 ? idx - 1 : idx; // 1-indexed line number
227
+ return targetIdx >= 0 && targetIdx < parts.length && parts[targetIdx].length > 0;
228
+ }
229
+ const str = String(value).trim();
230
+ return str.length > 0 && str.toLowerCase() !== "false" && str !== "0";
231
+ }
232
+
233
+ case "custom":
234
+ case "expression":
235
+ case "script": {
236
+ const code = step.code || step.expression || step.value || step.script || step.params?.code || step.params?.expression || step.params?.script;
237
+ if (!code || typeof code !== "string") return value;
238
+ try {
239
+ const trimmed = code.trim();
240
+ if (/\b(process|require|import|global|Buffer|__dirname|__filename|eval|Function)\b/.test(trimmed)) {
241
+ return value;
242
+ }
243
+
244
+ // Auto-quote unquoted string after == or === or != or !== e.g. == Vol Shocker -> == "Vol Shocker"
245
+ let sanitizedCode = trimmed.replace(/([=!]==?\s*)([A-Za-z][A-Za-z0-9 _-]*[A-Za-z0-9])(?=\s*$|\s*[;)])/g, (match, op, str) => {
246
+ const trimmedStr = str.trim();
247
+ if (['true', 'false', 'null', 'undefined', 'value', 'raw', 'lines'].includes(trimmedStr)) return match;
248
+ if (trimmedStr.startsWith('"') || trimmedStr.startsWith("'")) return match;
249
+ return `${op}"${trimmedStr}"`;
250
+ });
251
+
252
+ const lines = String(value).split("\n").map((s) => s.trim()).filter(Boolean);
253
+ const line = (n) => {
254
+ const idx = parseInt(n, 10);
255
+ const targetIdx = idx > 0 ? idx - 1 : idx;
256
+ return lines[targetIdx] || "";
257
+ };
258
+ const hasLine = (n) => {
259
+ const idx = parseInt(n, 10);
260
+ const targetIdx = idx > 0 ? idx - 1 : idx;
261
+ return targetIdx >= 0 && targetIdx < lines.length && lines[targetIdx].length > 0;
262
+ };
263
+ const has = (k) => String(value).toLowerCase().includes(String(k).toLowerCase());
264
+ const contains = has;
265
+ const hasValue = (v = value) => v !== null && v !== undefined && String(v).trim().length > 0;
266
+ const notEmpty = hasValue;
267
+
268
+ const fn = sanitizedCode.includes("return ")
269
+ ? new Function("value", "raw", "lines", "line", "hasLine", "has", "contains", "hasValue", "notEmpty", sanitizedCode)
270
+ : new Function("value", "raw", "lines", "line", "hasLine", "has", "contains", "hasValue", "notEmpty", `return (${sanitizedCode});`);
271
+ const result = fn(value, value, lines, line, hasLine, has, contains, hasValue, notEmpty);
272
+ return result !== undefined ? result : value;
273
+ } catch {
274
+ return value;
275
+ }
276
+ }
277
+
278
+ default:
279
+ return value;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Runs an array of transform steps sequentially on a raw extracted value.
285
+ *
286
+ * @param {*} rawValue
287
+ * @param {Array<Object>} pipeline
288
+ * @returns {*}
289
+ */
290
+ export function runTransformPipeline(rawValue, pipeline = []) {
291
+ if (!Array.isArray(pipeline) || pipeline.length === 0) {
292
+ return rawValue;
293
+ }
294
+
295
+ let current = rawValue;
296
+ for (const step of pipeline) {
297
+ current = applyTransformStep(current, step);
298
+ }
299
+ return current;
300
+ }
301
+
302
+ /**
303
+ * Casts a transformed value to the target data type.
304
+ *
305
+ * @param {*} val
306
+ * @param {string} type
307
+ * @returns {*}
308
+ */
309
+ export function castValueToType(val, type = "string") {
310
+ if (val === null || val === undefined) return val;
311
+
312
+ switch (type) {
313
+ case "integer": {
314
+ if (typeof val === "number") return Math.round(val);
315
+ const str = String(val).replace(/,/g, "").trim();
316
+ const num = parseInt(str, 10);
317
+ return isNaN(num) ? null : num;
318
+ }
319
+ case "number":
320
+ case "decimal": {
321
+ if (typeof val === "number") return val;
322
+ const str = String(val).replace(/[\u20B9$€£¥₹]/g, "").replace(/,/g, "").trim();
323
+ const num = parseFloat(str);
324
+ return isNaN(num) ? null : num;
325
+ }
326
+ case "boolean": {
327
+ if (typeof val === "boolean") return val;
328
+ const str = String(val).trim().toLowerCase();
329
+ return ["true", "yes", "1", "in stock", "available"].includes(str);
330
+ }
331
+ case "date": {
332
+ if (val instanceof Date) return val.toISOString();
333
+ const parsed = Date.parse(String(val));
334
+ return !isNaN(parsed) ? new Date(parsed).toISOString() : String(val);
335
+ }
336
+ case "string":
337
+ default:
338
+ return typeof val === "object" ? JSON.stringify(val) : String(val);
339
+ }
340
+ }