mcp-server-sfmc 2.0.0 → 2.1.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/README.md +8 -4
- package/dist/cli/reviewDiff.d.ts +1 -17
- package/dist/cli/reviewDiff.d.ts.map +1 -1
- package/dist/cli/reviewDiff.js +25 -53
- package/dist/cli/reviewDiff.js.map +1 -1
- package/dist/cli/reviewSeverity.d.ts +23 -0
- package/dist/cli/reviewSeverity.d.ts.map +1 -0
- package/dist/cli/reviewSeverity.js +36 -0
- package/dist/cli/reviewSeverity.js.map +1 -0
- package/dist/conversion-rules.d.ts.map +1 -1
- package/dist/conversion-rules.js +255 -249
- package/dist/conversion-rules.js.map +1 -1
- package/dist/index.js +185 -152
- package/dist/index.js.map +1 -1
- package/dist/mce-help-search.js +5 -5
- package/dist/mce-help-search.js.map +1 -1
- package/dist/mcn-help-search.js +5 -5
- package/dist/mcn-help-search.js.map +1 -1
- package/package.json +23 -12
package/dist/conversion-rules.js
CHANGED
|
@@ -207,6 +207,17 @@ export const NON_MIGRATABLE_SSJS_PATTERNS = [
|
|
|
207
207
|
reason: 'Query string / form field access requires CloudPages context (not available in MCN)',
|
|
208
208
|
},
|
|
209
209
|
];
|
|
210
|
+
/**
|
|
211
|
+
* Returns the first non-migratable SSJS pattern that matches the given code, or undefined.
|
|
212
|
+
* @param code single line of trimmed SSJS source
|
|
213
|
+
*/
|
|
214
|
+
function findNonMigratableSsjsPattern(code) {
|
|
215
|
+
return NON_MIGRATABLE_SSJS_PATTERNS.find(({ pattern }) => {
|
|
216
|
+
// Reset lastIndex for global regexes
|
|
217
|
+
pattern.lastIndex = 0;
|
|
218
|
+
return pattern.test(code);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
210
221
|
// ---------------------------------------------------------------------------
|
|
211
222
|
// SSJS → AMPscript conversion
|
|
212
223
|
// ---------------------------------------------------------------------------
|
|
@@ -237,8 +248,8 @@ export function ssjsToAmpscript(code) {
|
|
|
237
248
|
.trim();
|
|
238
249
|
const rawLines = inner.split('\n');
|
|
239
250
|
const outputLines = [];
|
|
240
|
-
for (const [
|
|
241
|
-
const
|
|
251
|
+
for (const [index, original] of rawLines.entries()) {
|
|
252
|
+
const lineNumber = index + 1;
|
|
242
253
|
const trimmed = original.trim();
|
|
243
254
|
// Skip blank lines, Platform.Load(), var-only declarations with no value
|
|
244
255
|
if (!trimmed) {
|
|
@@ -248,111 +259,107 @@ export function ssjsToAmpscript(code) {
|
|
|
248
259
|
// Skip Platform.Load() — no AMPscript equivalent, not needed in MCN
|
|
249
260
|
if (/^Platform\.Load\s*\(/i.test(trimmed)) {
|
|
250
261
|
changes.push({
|
|
251
|
-
line:
|
|
262
|
+
line: lineNumber,
|
|
252
263
|
description: 'Removed Platform.Load() (not needed in AMPscript)',
|
|
253
264
|
});
|
|
254
265
|
continue;
|
|
255
266
|
}
|
|
256
267
|
// Check for non-migratable patterns first
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
outputLines.push(`%%-- MANUAL_REWRITE_REQUIRED: ${reason} --%%`, `%%-- Original: ${trimmed} --%%`);
|
|
263
|
-
flaggedSections.push({ line: lineNum, code: trimmed, reason });
|
|
264
|
-
isFlagged = true;
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (isFlagged) {
|
|
268
|
+
const nonMigratable = findNonMigratableSsjsPattern(trimmed);
|
|
269
|
+
if (nonMigratable) {
|
|
270
|
+
const { reason } = nonMigratable;
|
|
271
|
+
outputLines.push(`%%-- MANUAL_REWRITE_REQUIRED: ${reason} --%%`, `%%-- Original: ${trimmed} --%%`);
|
|
272
|
+
flaggedSections.push({ line: lineNumber, code: trimmed, reason });
|
|
269
273
|
continue;
|
|
270
274
|
}
|
|
271
275
|
let line = original;
|
|
272
276
|
// Platform.Variable.GetValue("name") → @name
|
|
273
277
|
line = line.replaceAll(/Platform\.Variable\.GetValue\s*\(\s*["']([^"']+)["']\s*\)/gi, '@$1');
|
|
274
278
|
// Platform.Variable.SetValue("name", value) → SET @name = value (strip ; at end if present)
|
|
275
|
-
line = line.replaceAll(/Platform\.Variable\.SetValue\s*\(\s*["']([^"']+)["']\s*,\s*([^)]+)\)\s*;?/gi, (_,
|
|
279
|
+
line = line.replaceAll(/Platform\.Variable\.SetValue\s*\(\s*["']([^"']+)["']\s*,\s*([^)]+)\)\s*;?/gi, (_, variableName, value) => {
|
|
276
280
|
changes.push({
|
|
277
|
-
line:
|
|
278
|
-
description: `Platform.Variable.SetValue → SET @${
|
|
281
|
+
line: lineNumber,
|
|
282
|
+
description: `Platform.Variable.SetValue → SET @${variableName}`,
|
|
279
283
|
});
|
|
280
|
-
return `SET @${
|
|
284
|
+
return `SET @${variableName} = ${value.trim()}`;
|
|
281
285
|
});
|
|
282
286
|
// Platform.Response.Write(expr) → OutputLine(expr)
|
|
283
287
|
line = line.replaceAll(/Platform\.Response\.Write\s*\(/gi, () => {
|
|
284
|
-
changes.push({ line:
|
|
288
|
+
changes.push({ line: lineNumber, description: 'Platform.Response.Write → OutputLine' });
|
|
285
289
|
return 'OutputLine(';
|
|
286
290
|
});
|
|
287
291
|
// Platform.Function.X(args) → X(args) using known function map
|
|
288
|
-
line = line.replaceAll(/Platform\.Function\.(\w+)\s*\(/gi, (_,
|
|
289
|
-
const key =
|
|
292
|
+
line = line.replaceAll(/Platform\.Function\.(\w+)\s*\(/gi, (_, functionName) => {
|
|
293
|
+
const key = functionName.toLowerCase();
|
|
290
294
|
if (SSJS_ONLY_FUNCTIONS.has(key)) {
|
|
291
295
|
flaggedSections.push({
|
|
292
|
-
line:
|
|
293
|
-
code: `Platform.Function.${
|
|
294
|
-
reason: `Platform.Function.${
|
|
296
|
+
line: lineNumber,
|
|
297
|
+
code: `Platform.Function.${functionName}(...)`,
|
|
298
|
+
reason: `Platform.Function.${functionName} has no AMPscript equivalent`,
|
|
295
299
|
});
|
|
296
|
-
return `/* MANUAL_REWRITE_REQUIRED: Platform.Function.${
|
|
300
|
+
return `/* MANUAL_REWRITE_REQUIRED: Platform.Function.${functionName} has no AMPscript equivalent */ ${functionName}(`;
|
|
297
301
|
}
|
|
298
302
|
const ampName = PLATFORM_FUNCTION_TO_AMP[key];
|
|
299
303
|
if (!ampName) {
|
|
300
304
|
flaggedSections.push({
|
|
301
|
-
line:
|
|
302
|
-
code: `Platform.Function.${
|
|
303
|
-
reason: `Platform.Function.${
|
|
305
|
+
line: lineNumber,
|
|
306
|
+
code: `Platform.Function.${functionName}(...)`,
|
|
307
|
+
reason: `Platform.Function.${functionName} not found in ssjs-data catalog`,
|
|
304
308
|
});
|
|
305
|
-
return `/* MANUAL_REWRITE_REQUIRED: unknown Platform.Function.${
|
|
309
|
+
return `/* MANUAL_REWRITE_REQUIRED: unknown Platform.Function.${functionName} */ ${functionName}(`;
|
|
306
310
|
}
|
|
307
311
|
changes.push({
|
|
308
|
-
line:
|
|
309
|
-
description: `Platform.Function.${
|
|
312
|
+
line: lineNumber,
|
|
313
|
+
description: `Platform.Function.${functionName} → ${ampName}`,
|
|
310
314
|
});
|
|
311
315
|
return `${ampName}(`;
|
|
312
316
|
});
|
|
313
317
|
// var x = expr; → SET @x = expr
|
|
314
|
-
line = line.replace(/\bvar\s+([A-Za-z_]\w*)\s*=\s*(.+?)\s*;?\s*$/, (_,
|
|
318
|
+
line = line.replace(/\bvar\s+([A-Za-z_]\w*)\s*=\s*(.+?)\s*;?\s*$/, (_, variableName, value) => {
|
|
315
319
|
changes.push({
|
|
316
|
-
line:
|
|
317
|
-
description: `var ${
|
|
320
|
+
line: lineNumber,
|
|
321
|
+
description: `var ${variableName} = ... → SET @${variableName}`,
|
|
318
322
|
});
|
|
319
|
-
return `SET @${
|
|
323
|
+
return `SET @${variableName} = ${value.trim()}`;
|
|
320
324
|
});
|
|
321
325
|
// var x; → VAR @x
|
|
322
|
-
line = line.replace(/\bvar\s+([A-Za-z_]\w*)\s*;?\s*$/, (_,
|
|
323
|
-
changes.push({
|
|
324
|
-
|
|
326
|
+
line = line.replace(/\bvar\s+([A-Za-z_]\w*)\s*;?\s*$/, (_, variableName) => {
|
|
327
|
+
changes.push({
|
|
328
|
+
line: lineNumber,
|
|
329
|
+
description: `var ${variableName} → VAR @${variableName}`,
|
|
330
|
+
});
|
|
331
|
+
return `VAR @${variableName}`;
|
|
325
332
|
});
|
|
326
333
|
// Control flow: if (cond) { → IF cond THEN
|
|
327
334
|
line = line.replace(/^\s*if\s*\((.+)\)\s*\{\s*$/, (_, cond) => {
|
|
328
335
|
const ampCond = ssjsCondToAmp(cond.trim());
|
|
329
|
-
changes.push({ line:
|
|
336
|
+
changes.push({ line: lineNumber, description: 'if (...) { → IF ... THEN' });
|
|
330
337
|
return `IF ${ampCond} THEN`;
|
|
331
338
|
});
|
|
332
339
|
// } else if (cond) { → ELSEIF cond THEN
|
|
333
340
|
line = line.replace(/^\s*\}\s*else\s+if\s*\((.+)\)\s*\{\s*$/, (_, cond) => {
|
|
334
341
|
const ampCond = ssjsCondToAmp(cond.trim());
|
|
335
|
-
changes.push({ line:
|
|
342
|
+
changes.push({ line: lineNumber, description: '} else if (...) { → ELSEIF ... THEN' });
|
|
336
343
|
return `ELSEIF ${ampCond} THEN`;
|
|
337
344
|
});
|
|
338
345
|
// } else { → ELSE
|
|
339
346
|
line = line.replace(/^\s*\}\s*else\s*\{\s*$/, () => {
|
|
340
|
-
changes.push({ line:
|
|
347
|
+
changes.push({ line: lineNumber, description: '} else { → ELSE' });
|
|
341
348
|
return 'ELSE';
|
|
342
349
|
});
|
|
343
350
|
// for (var i = start; i <= end; i++) { → FOR @i = start TO end DO
|
|
344
351
|
const forMatch = /^\s*for\s*\(\s*var\s+(\w+)\s*=\s*(\S+?)\s*;\s*\w+\s*<=?\s*(\S+?)\s*;\s*\w+\+\+\s*\)\s*\{\s*$/.exec(line);
|
|
345
352
|
if (forMatch) {
|
|
346
|
-
const [,
|
|
353
|
+
const [, iterVariable, start, end] = forMatch;
|
|
347
354
|
changes.push({
|
|
348
|
-
line:
|
|
349
|
-
description: `for (var ${
|
|
355
|
+
line: lineNumber,
|
|
356
|
+
description: `for (var ${iterVariable}...) → FOR @${iterVariable} = ${start} TO ${end} DO`,
|
|
350
357
|
});
|
|
351
|
-
line = `FOR @${
|
|
358
|
+
line = `FOR @${iterVariable} = ${start} TO ${end} DO`;
|
|
352
359
|
}
|
|
353
360
|
// Standalone closing brace } → ENDIF (best-effort; may not always be correct)
|
|
354
361
|
if (/^\s*\}\s*$/.test(line) && !/^\s*\}\s*(else|catch|finally)/.test(line)) {
|
|
355
|
-
changes.push({ line:
|
|
362
|
+
changes.push({ line: lineNumber, description: '} → ENDIF' });
|
|
356
363
|
line = 'ENDIF';
|
|
357
364
|
}
|
|
358
365
|
// Strip trailing semicolons from non-SET lines (AMPscript doesn't use them)
|
|
@@ -408,8 +415,8 @@ export function ampscriptToSsjs(code) {
|
|
|
408
415
|
const normalized = normalizeAmpscriptBlocks(code);
|
|
409
416
|
const lines = normalized.split('\n');
|
|
410
417
|
const lineOffset = 0;
|
|
411
|
-
for (const [
|
|
412
|
-
const
|
|
418
|
+
for (const [index, line] of lines.entries()) {
|
|
419
|
+
const lineNumber = index + 1 + lineOffset;
|
|
413
420
|
const trimmed = line.trim();
|
|
414
421
|
if (!trimmed) {
|
|
415
422
|
outputLines.push('');
|
|
@@ -418,51 +425,52 @@ export function ampscriptToSsjs(code) {
|
|
|
418
425
|
// %%=Output(@x)=%% or %%=OutputLine(@x)=%%
|
|
419
426
|
const inlineOutputMatch = /^%%=\s*(?:Output|OutputLine)\s*\((.+)\)\s*=%%$/i.exec(trimmed);
|
|
420
427
|
if (inlineOutputMatch) {
|
|
421
|
-
const
|
|
428
|
+
const expression = stripAmpVars(inlineOutputMatch[1].trim());
|
|
422
429
|
changes.push({
|
|
423
|
-
line:
|
|
430
|
+
line: lineNumber,
|
|
424
431
|
description: '%%=Output(...)=%% → Platform.Response.Write(...)',
|
|
425
432
|
});
|
|
426
|
-
outputLines.push(`Platform.Response.Write(${
|
|
433
|
+
outputLines.push(`Platform.Response.Write(${expression});`);
|
|
427
434
|
continue;
|
|
428
435
|
}
|
|
429
436
|
// %%=FunctionName(args)=%% → Platform.Response.Write(Platform.Function.FunctionName(args))
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const key =
|
|
437
|
+
const inlineFunctionMatch = /^%%=\s*(\w+)\s*\((.*)?\)\s*=%%$/i.exec(trimmed);
|
|
438
|
+
if (inlineFunctionMatch) {
|
|
439
|
+
const functionName = inlineFunctionMatch[1];
|
|
440
|
+
const arguments_ = inlineFunctionMatch[2]?.trim() ?? '';
|
|
441
|
+
const key = functionName.toLowerCase();
|
|
435
442
|
const ssName = AMP_TO_PLATFORM_FUNCTION[key];
|
|
436
|
-
const
|
|
443
|
+
const nativeHint = AMP_NATIVE_JS_HINTS[key];
|
|
444
|
+
const argumentsConverted = stripAmpVars(arguments_);
|
|
437
445
|
if (ssName) {
|
|
438
446
|
changes.push({
|
|
439
|
-
line:
|
|
440
|
-
description: `%%=${
|
|
447
|
+
line: lineNumber,
|
|
448
|
+
description: `%%=${functionName}(...)=%% → Platform.Response.Write(Platform.Function.${ssName}(...))`,
|
|
441
449
|
});
|
|
442
|
-
outputLines.push(`Platform.Response.Write(Platform.Function.${ssName}(${
|
|
450
|
+
outputLines.push(`Platform.Response.Write(Platform.Function.${ssName}(${argumentsConverted}));`);
|
|
443
451
|
}
|
|
444
|
-
else if (
|
|
452
|
+
else if (nativeHint) {
|
|
445
453
|
changes.push({
|
|
446
|
-
line:
|
|
447
|
-
description: `%%=${
|
|
454
|
+
line: lineNumber,
|
|
455
|
+
description: `%%=${functionName}(...)=%% → native JS hint`,
|
|
448
456
|
});
|
|
449
|
-
outputLines.push(`Platform.Response.Write(${
|
|
457
|
+
outputLines.push(`Platform.Response.Write(${nativeHint} /* AMPscript: ${functionName}(${arguments_}) */);`);
|
|
450
458
|
}
|
|
451
459
|
else if (CLOUDPAGES_ONLY_FUNCTIONS.has(key)) {
|
|
452
460
|
// CloudPages-only function
|
|
453
|
-
outputLines.push(`/* MANUAL_REWRITE_REQUIRED: %%=${
|
|
461
|
+
outputLines.push(`/* MANUAL_REWRITE_REQUIRED: %%=${functionName}(${arguments_})=%% */`);
|
|
454
462
|
flaggedSections.push({
|
|
455
|
-
line:
|
|
463
|
+
line: lineNumber,
|
|
456
464
|
code: trimmed,
|
|
457
|
-
reason: `AMPscript function '${
|
|
465
|
+
reason: `AMPscript function '${functionName}' is CloudPages-only — no SSJS equivalent`,
|
|
458
466
|
});
|
|
459
467
|
}
|
|
460
468
|
else {
|
|
461
469
|
// AMP-only function with no native hint → polyfill
|
|
462
470
|
polyfillUsed.value = true;
|
|
463
471
|
changes.push({
|
|
464
|
-
line:
|
|
465
|
-
description: `%%=${
|
|
472
|
+
line: lineNumber,
|
|
473
|
+
description: `%%=${functionName}(...)=%% → _ampScript polyfill`,
|
|
466
474
|
});
|
|
467
475
|
outputLines.push(`Platform.Response.Write(_ampScript('${trimmed.replaceAll("'", String.raw `\'`)}'));`);
|
|
468
476
|
}
|
|
@@ -472,9 +480,9 @@ export function ampscriptToSsjs(code) {
|
|
|
472
480
|
const blockMatch = /^%%\[\s*([\s\S]*?)\s*\]%%$/i.exec(trimmed);
|
|
473
481
|
if (blockMatch) {
|
|
474
482
|
const blockContent = blockMatch[1].trim();
|
|
475
|
-
const
|
|
476
|
-
for (const
|
|
477
|
-
const converted = convertAmpStatement(
|
|
483
|
+
const statements = blockContent.split(/\n+/);
|
|
484
|
+
for (const statement of statements) {
|
|
485
|
+
const converted = convertAmpStatement(statement.trim(), lineNumber, changes, flaggedSections, polyfillUsed);
|
|
478
486
|
if (converted !== null) {
|
|
479
487
|
outputLines.push(converted);
|
|
480
488
|
}
|
|
@@ -483,7 +491,7 @@ export function ampscriptToSsjs(code) {
|
|
|
483
491
|
}
|
|
484
492
|
// Bare AMPscript statement (already stripped of delimiters from normalizeAmpscriptBlocks)
|
|
485
493
|
if (/^(SET|VAR|IF|ELSEIF|ELSE|ENDIF|FOR|NEXT|OUTPUT|OUTPUTLINE)\b/i.test(trimmed)) {
|
|
486
|
-
const converted = convertAmpStatement(trimmed,
|
|
494
|
+
const converted = convertAmpStatement(trimmed, lineNumber, changes, flaggedSections, polyfillUsed);
|
|
487
495
|
if (converted !== null) {
|
|
488
496
|
outputLines.push(converted);
|
|
489
497
|
}
|
|
@@ -491,7 +499,7 @@ export function ampscriptToSsjs(code) {
|
|
|
491
499
|
}
|
|
492
500
|
// Bare function call or other AMPscript statement inside a normalized block
|
|
493
501
|
if (/^\w+\s*\(/.test(trimmed)) {
|
|
494
|
-
const converted = convertAmpStatement(trimmed,
|
|
502
|
+
const converted = convertAmpStatement(trimmed, lineNumber, changes, flaggedSections, polyfillUsed);
|
|
495
503
|
if (converted !== null) {
|
|
496
504
|
outputLines.push(converted);
|
|
497
505
|
}
|
|
@@ -547,40 +555,42 @@ function convertAmpStatement(stmt, lineNum, changes, flaggedSections, polyfillUs
|
|
|
547
555
|
// SET @x = expr → var x = expr;
|
|
548
556
|
const setMatch = /^SET\s+@(\w+)\s*=\s*(.+)$/i.exec(stmt);
|
|
549
557
|
if (setMatch) {
|
|
550
|
-
const [,
|
|
551
|
-
const
|
|
558
|
+
const [, variableName, expression] = setMatch;
|
|
559
|
+
const expressionTrimmed = expression.trim();
|
|
552
560
|
// Check if the expression is a single AMP-only function call with no Platform.Function equivalent
|
|
553
561
|
// and no native JS hint — if so, emit _ampScript polyfill rather than a broken expression.
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
556
|
-
const key =
|
|
562
|
+
const singleFunctionMatch = /^(\w+)\s*\(/.exec(expressionTrimmed);
|
|
563
|
+
if (singleFunctionMatch) {
|
|
564
|
+
const key = singleFunctionMatch[1].toLowerCase();
|
|
557
565
|
const hasSsjsEquiv = AMP_TO_PLATFORM_FUNCTION[key] !== undefined;
|
|
558
566
|
const hasNativeHint = AMP_NATIVE_JS_HINTS[key] !== undefined;
|
|
559
567
|
if (!hasSsjsEquiv && !hasNativeHint && !CLOUDPAGES_ONLY_FUNCTIONS.has(key)) {
|
|
560
568
|
polyfillUsed.value = true;
|
|
561
569
|
changes.push({
|
|
562
570
|
line: lineNum,
|
|
563
|
-
description: `SET @${
|
|
571
|
+
description: `SET @${variableName} = ${singleFunctionMatch[1]}(...) → _ampScript polyfill`,
|
|
564
572
|
});
|
|
565
|
-
return `var ${
|
|
573
|
+
return `var ${variableName} = _ampScript('${expressionTrimmed.replaceAll("'", String.raw `\'`)}');`;
|
|
566
574
|
}
|
|
567
575
|
}
|
|
568
|
-
const
|
|
576
|
+
const ssExpression = convertAmpExpression(expressionTrimmed);
|
|
569
577
|
changes.push({
|
|
570
578
|
line: lineNum,
|
|
571
|
-
description: `SET @${
|
|
579
|
+
description: `SET @${variableName} = ... → var ${variableName} = ...`,
|
|
572
580
|
});
|
|
573
|
-
return `var ${
|
|
581
|
+
return `var ${variableName} = ${ssExpression};`;
|
|
574
582
|
}
|
|
575
583
|
// VAR @x, @y → var x, y;
|
|
576
|
-
const
|
|
577
|
-
if (
|
|
578
|
-
const
|
|
584
|
+
const variableMatch = /^VAR\s+(.+)$/i.exec(stmt);
|
|
585
|
+
if (variableMatch) {
|
|
586
|
+
const variables = variableMatch[1]
|
|
587
|
+
.split(',')
|
|
588
|
+
.map((v) => v.trim().replace(/^@/, ''));
|
|
579
589
|
changes.push({
|
|
580
590
|
line: lineNum,
|
|
581
|
-
description: `VAR @${
|
|
591
|
+
description: `VAR @${variables.join(', @')} → var ${variables.join(', ')}`,
|
|
582
592
|
});
|
|
583
|
-
return `var ${
|
|
593
|
+
return `var ${variables.join(', ')};`;
|
|
584
594
|
}
|
|
585
595
|
// IF cond THEN → if (cond) {
|
|
586
596
|
const ifMatch = /^IF\s+(.+?)\s+THEN$/i.exec(stmt);
|
|
@@ -609,14 +619,14 @@ function convertAmpStatement(stmt, lineNum, changes, flaggedSections, polyfillUs
|
|
|
609
619
|
// FOR @i = start TO end DO → for (var i = start; i <= end; i++) {
|
|
610
620
|
const forMatch = /^FOR\s+@(\w+)\s*=\s*(\S+?)\s+TO\s+(\S+?)(?:\s+STEP\s+\S+)?\s+DO$/i.exec(stmt);
|
|
611
621
|
if (forMatch) {
|
|
612
|
-
const [,
|
|
622
|
+
const [, iterVariable, start, end] = forMatch;
|
|
613
623
|
const ssStart = stripAmpVars(start);
|
|
614
624
|
const ssEnd = stripAmpVars(end);
|
|
615
625
|
changes.push({
|
|
616
626
|
line: lineNum,
|
|
617
|
-
description: `FOR @${
|
|
627
|
+
description: `FOR @${iterVariable} = ${start} TO ${end} DO → for loop`,
|
|
618
628
|
});
|
|
619
|
-
return `for (var ${
|
|
629
|
+
return `for (var ${iterVariable} = ${ssStart}; ${iterVariable} <= ${ssEnd}; ${iterVariable}++) {`;
|
|
620
630
|
}
|
|
621
631
|
// NEXT @i → }
|
|
622
632
|
if (/^NEXT\s+@\w+$/i.test(stmt)) {
|
|
@@ -626,23 +636,23 @@ function convertAmpStatement(stmt, lineNum, changes, flaggedSections, polyfillUs
|
|
|
626
636
|
// OUTPUT(expr) / OUTPUTLINE(expr) → Platform.Response.Write(expr)
|
|
627
637
|
const outputMatch = /^(?:OUTPUT|OUTPUTLINE)\s*\((.+)\)$/i.exec(stmt);
|
|
628
638
|
if (outputMatch) {
|
|
629
|
-
const
|
|
639
|
+
const expression = convertAmpExpression(outputMatch[1].trim());
|
|
630
640
|
changes.push({ line: lineNum, description: 'Output/OutputLine → Platform.Response.Write' });
|
|
631
|
-
return `Platform.Response.Write(${
|
|
641
|
+
return `Platform.Response.Write(${expression});`;
|
|
632
642
|
}
|
|
633
643
|
// Known AMPscript function call → Platform.Function.X(args)
|
|
634
|
-
const
|
|
635
|
-
if (
|
|
636
|
-
const [,
|
|
637
|
-
const key =
|
|
644
|
+
const functionCallMatch = /^(\w+)\s*\((.*)?\)$/i.exec(stmt);
|
|
645
|
+
if (functionCallMatch) {
|
|
646
|
+
const [, functionName, arguments_] = functionCallMatch;
|
|
647
|
+
const key = functionName.toLowerCase();
|
|
638
648
|
const ssName = AMP_TO_PLATFORM_FUNCTION[key];
|
|
639
649
|
if (ssName) {
|
|
640
|
-
const
|
|
650
|
+
const argumentsConverted = arguments_ ? convertAmpExpression(arguments_.trim()) : '';
|
|
641
651
|
changes.push({
|
|
642
652
|
line: lineNum,
|
|
643
|
-
description: `${
|
|
653
|
+
description: `${functionName}(…) → Platform.Function.${ssName}(…)`,
|
|
644
654
|
});
|
|
645
|
-
return `Platform.Function.${ssName}(${
|
|
655
|
+
return `Platform.Function.${ssName}(${argumentsConverted});`;
|
|
646
656
|
}
|
|
647
657
|
// CloudPages-only functions have no SSJS/MCN equivalent
|
|
648
658
|
if (CLOUDPAGES_ONLY_FUNCTIONS.has(key)) {
|
|
@@ -658,7 +668,7 @@ function convertAmpStatement(stmt, lineNum, changes, flaggedSections, polyfillUs
|
|
|
658
668
|
if (hint) {
|
|
659
669
|
changes.push({
|
|
660
670
|
line: lineNum,
|
|
661
|
-
description: `${
|
|
671
|
+
description: `${functionName}(…) → native JS equivalent`,
|
|
662
672
|
});
|
|
663
673
|
return `${hint} /* AMPscript: ${stmt} */`;
|
|
664
674
|
}
|
|
@@ -666,7 +676,7 @@ function convertAmpStatement(stmt, lineNum, changes, flaggedSections, polyfillUs
|
|
|
666
676
|
polyfillUsed.value = true;
|
|
667
677
|
changes.push({
|
|
668
678
|
line: lineNum,
|
|
669
|
-
description: `${
|
|
679
|
+
description: `${functionName}(…) → _ampScript polyfill (no direct SSJS equivalent)`,
|
|
670
680
|
});
|
|
671
681
|
return `_ampScript('${stmt.replaceAll("'", String.raw `\'`)}');`;
|
|
672
682
|
}
|
|
@@ -697,21 +707,21 @@ function convertAmpStatement(stmt, lineNum, changes, flaggedSections, polyfillUs
|
|
|
697
707
|
* @param expr - AMPscript expression string.
|
|
698
708
|
* @returns {string} SSJS expression string.
|
|
699
709
|
*/
|
|
700
|
-
function
|
|
710
|
+
function convertAmpExpression(expr) {
|
|
701
711
|
// Replace known AMPscript function calls with Platform.Function.X equivalents;
|
|
702
712
|
// emit a hint comment for native-hint functions; leave others with a MANUAL_REWRITE comment.
|
|
703
713
|
// Note: AMP-only functions used in a SET assignment are handled at the statement level
|
|
704
714
|
// (convertAmpStatement SET handler) which emits _ampScript(...) for the whole expression.
|
|
705
|
-
let result = expr.replaceAll(/\b(\w+)\s*\(/g, (match,
|
|
706
|
-
const key =
|
|
715
|
+
let result = expr.replaceAll(/\b(\w+)\s*\(/g, (match, functionName) => {
|
|
716
|
+
const key = functionName.toLowerCase();
|
|
707
717
|
const ssName = AMP_TO_PLATFORM_FUNCTION[key];
|
|
708
718
|
if (ssName)
|
|
709
719
|
return `Platform.Function.${ssName}(`;
|
|
710
720
|
const hint = AMP_NATIVE_JS_HINTS[key];
|
|
711
721
|
if (hint)
|
|
712
|
-
return `${hint} /* ${
|
|
722
|
+
return `${hint} /* ${functionName}( */`;
|
|
713
723
|
// Unknown function in expression context — annotate
|
|
714
|
-
return `/* MANUAL_REWRITE_REQUIRED: no SSJS equivalent for ${
|
|
724
|
+
return `/* MANUAL_REWRITE_REQUIRED: no SSJS equivalent for ${functionName} */ ${functionName}(`;
|
|
715
725
|
});
|
|
716
726
|
// Strip @ from variable references
|
|
717
727
|
result = stripAmpVars(result);
|
|
@@ -750,59 +760,57 @@ export function stripAmpVars(expr) {
|
|
|
750
760
|
*/
|
|
751
761
|
export const HBS_GAP_NOTE = 'documented as supported in Marketing Cloud Next but currently fails at runtime — no Handlebars helper exists yet';
|
|
752
762
|
/**
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
* @param argsStr - The argument string (contents between the outer parens).
|
|
757
|
-
* @returns {string[]} Trimmed top-level arguments.
|
|
763
|
+
* Fold a single character into the argument-splitting accumulator state.
|
|
764
|
+
* @param state - Mutable accumulator carried across characters.
|
|
765
|
+
* @param ch - The current character.
|
|
758
766
|
*/
|
|
759
|
-
function
|
|
760
|
-
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
767
|
+
function foldArgumentChar(state, ch) {
|
|
768
|
+
if (state.quote) {
|
|
769
|
+
state.current += ch;
|
|
770
|
+
if (ch === state.quote)
|
|
771
|
+
state.quote = null;
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
switch (ch) {
|
|
775
|
+
case '"':
|
|
776
|
+
case "'": {
|
|
777
|
+
state.quote = ch;
|
|
778
|
+
state.current += ch;
|
|
779
|
+
break;
|
|
772
780
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
current += ch;
|
|
790
|
-
break;
|
|
781
|
+
case '(':
|
|
782
|
+
case '[': {
|
|
783
|
+
state.depth++;
|
|
784
|
+
state.current += ch;
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
case ')':
|
|
788
|
+
case ']': {
|
|
789
|
+
state.depth--;
|
|
790
|
+
state.current += ch;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
default: {
|
|
794
|
+
if (ch === ',' && state.depth === 0) {
|
|
795
|
+
state.args.push(state.current.trim());
|
|
796
|
+
state.current = '';
|
|
791
797
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
args.push(current.trim());
|
|
795
|
-
current = '';
|
|
796
|
-
}
|
|
797
|
-
else {
|
|
798
|
-
current += ch;
|
|
799
|
-
}
|
|
798
|
+
else {
|
|
799
|
+
state.current += ch;
|
|
800
800
|
}
|
|
801
801
|
}
|
|
802
802
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
803
|
+
}
|
|
804
|
+
function splitArguments(argsStr) {
|
|
805
|
+
if (!argsStr.trim())
|
|
806
|
+
return [];
|
|
807
|
+
const state = { depth: 0, current: '', quote: null, args: [] };
|
|
808
|
+
for (const ch of argsStr) {
|
|
809
|
+
foldArgumentChar(state, ch);
|
|
810
|
+
}
|
|
811
|
+
if (state.current.trim())
|
|
812
|
+
state.args.push(state.current.trim());
|
|
813
|
+
return state.args;
|
|
806
814
|
}
|
|
807
815
|
/**
|
|
808
816
|
* Convert a single AMPscript call argument to its Handlebars form: string and
|
|
@@ -810,7 +818,7 @@ function splitArgs(argsStr) {
|
|
|
810
818
|
* @param arg - A single AMPscript argument.
|
|
811
819
|
* @returns {string} The Handlebars argument.
|
|
812
820
|
*/
|
|
813
|
-
function
|
|
821
|
+
function ampArgumentToHbs(arg) {
|
|
814
822
|
const t = arg.trim();
|
|
815
823
|
if (/^["']/.test(t))
|
|
816
824
|
return t;
|
|
@@ -823,7 +831,7 @@ function ampArgToHbs(arg) {
|
|
|
823
831
|
* @param arg - A single Handlebars argument.
|
|
824
832
|
* @returns {string} The AMPscript argument.
|
|
825
833
|
*/
|
|
826
|
-
function
|
|
834
|
+
function hbsArgumentToAmp(arg) {
|
|
827
835
|
const t = arg.trim();
|
|
828
836
|
if (/^["']/.test(t))
|
|
829
837
|
return t;
|
|
@@ -854,42 +862,42 @@ function convertInlineAmpToHbs(inner, lineNum, changes, flaggedSections) {
|
|
|
854
862
|
return `{{${vMatch[1]}}}`;
|
|
855
863
|
}
|
|
856
864
|
// Bare @var / var → {{var}}
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
changes.push({ line: lineNum, description: `%%=${inner}=%% → {{${
|
|
860
|
-
return `{{${
|
|
865
|
+
const variableMatch = /^@?([A-Za-z_]\w*)$/.exec(inner);
|
|
866
|
+
if (variableMatch) {
|
|
867
|
+
changes.push({ line: lineNum, description: `%%=${inner}=%% → {{${variableMatch[1]}}}` });
|
|
868
|
+
return `{{${variableMatch[1]}}}`;
|
|
861
869
|
}
|
|
862
870
|
// FunctionName(args)
|
|
863
|
-
const
|
|
864
|
-
if (
|
|
865
|
-
const
|
|
866
|
-
const key =
|
|
867
|
-
const
|
|
871
|
+
const functionMatch = /^([A-Za-z_]\w*)\s*\(([\s\S]*)\)$/.exec(inner);
|
|
872
|
+
if (functionMatch) {
|
|
873
|
+
const functionName = functionMatch[1];
|
|
874
|
+
const key = functionName.toLowerCase();
|
|
875
|
+
const hbsArguments = splitArguments(functionMatch[2]).map((a) => ampArgumentToHbs(a));
|
|
868
876
|
// Category A — mapped helper.
|
|
869
877
|
const helper = AMP_TO_HANDLEBARS[key];
|
|
870
878
|
if (helper) {
|
|
871
879
|
changes.push({
|
|
872
880
|
line: lineNum,
|
|
873
|
-
description: `%%=${
|
|
881
|
+
description: `%%=${functionName}(…)=%% → {{${helper} …}}`,
|
|
874
882
|
});
|
|
875
|
-
return `{{${helper}${
|
|
883
|
+
return `{{${helper}${hbsArguments.length > 0 ? ' ' + hbsArguments.join(' ') : ''}}}`;
|
|
876
884
|
}
|
|
877
885
|
// Category C — mcnHandlebarsGap (distinct note from Category B).
|
|
878
886
|
if (AMP_MCN_HANDLEBARS_GAP.has(key)) {
|
|
879
887
|
flaggedSections.push({
|
|
880
888
|
line: lineNum,
|
|
881
889
|
code: `%%=${inner}=%%`,
|
|
882
|
-
reason: `${
|
|
890
|
+
reason: `${functionName} is ${HBS_GAP_NOTE}`,
|
|
883
891
|
});
|
|
884
|
-
return `{{!-- MANUAL_REWRITE_REQUIRED: ${
|
|
892
|
+
return `{{!-- MANUAL_REWRITE_REQUIRED: ${functionName} is ${HBS_GAP_NOTE} --}}`;
|
|
885
893
|
}
|
|
886
894
|
// Category B — no Handlebars counterpart.
|
|
887
895
|
flaggedSections.push({
|
|
888
896
|
line: lineNum,
|
|
889
897
|
code: `%%=${inner}=%%`,
|
|
890
|
-
reason: `AMPscript function '${
|
|
898
|
+
reason: `AMPscript function '${functionName}' has no Handlebars equivalent`,
|
|
891
899
|
});
|
|
892
|
-
return `{{!-- MANUAL_REWRITE_REQUIRED: AMPscript function '${
|
|
900
|
+
return `{{!-- MANUAL_REWRITE_REQUIRED: AMPscript function '${functionName}' has no Handlebars equivalent --}}`;
|
|
893
901
|
}
|
|
894
902
|
// Anything else (complex expression).
|
|
895
903
|
flaggedSections.push({
|
|
@@ -920,22 +928,22 @@ export function ampscriptToHandlebars(code) {
|
|
|
920
928
|
const flaggedSections = [];
|
|
921
929
|
const lines = code.split('\n');
|
|
922
930
|
const outputLines = [];
|
|
923
|
-
let
|
|
924
|
-
for (const [
|
|
925
|
-
const
|
|
931
|
+
let isInBlock = false;
|
|
932
|
+
for (const [index, line] of lines.entries()) {
|
|
933
|
+
const lineNumber = index + 1;
|
|
926
934
|
const trimmed = line.trim();
|
|
927
935
|
// Track multi-line %%[ … ]%% blocks — flag the whole block once.
|
|
928
|
-
if (
|
|
936
|
+
if (isInBlock) {
|
|
929
937
|
if (/\]%%/.test(line))
|
|
930
|
-
|
|
938
|
+
isInBlock = false;
|
|
931
939
|
continue;
|
|
932
940
|
}
|
|
933
941
|
if (/%%\[/.test(line)) {
|
|
934
942
|
if (!/\]%%/.test(line))
|
|
935
|
-
|
|
943
|
+
isInBlock = true;
|
|
936
944
|
outputLines.push('{{!-- MANUAL_REWRITE_REQUIRED: AMPscript block (SET/VAR/IF/FOR) has no Handlebars equivalent — Handlebars cannot assign variables or run imperative control flow --}}');
|
|
937
945
|
flaggedSections.push({
|
|
938
|
-
line:
|
|
946
|
+
line: lineNumber,
|
|
939
947
|
code: trimmed.slice(0, 80),
|
|
940
948
|
reason: 'AMPscript procedural block has no Handlebars counterpart',
|
|
941
949
|
});
|
|
@@ -943,9 +951,9 @@ export function ampscriptToHandlebars(code) {
|
|
|
943
951
|
}
|
|
944
952
|
// Convert inline expressions on this line.
|
|
945
953
|
const converted = line
|
|
946
|
-
.replaceAll(/%%=\s*([\s\S]*?)\s*=%%/g, (_full, raw) => convertInlineAmpToHbs(raw.trim(),
|
|
954
|
+
.replaceAll(/%%=\s*([\s\S]*?)\s*=%%/g, (_full, raw) => convertInlineAmpToHbs(raw.trim(), lineNumber, changes, flaggedSections))
|
|
947
955
|
.replaceAll(/%%([A-Za-z_]\w*)%%/g, (_full, v) => {
|
|
948
|
-
changes.push({ line:
|
|
956
|
+
changes.push({ line: lineNumber, description: `%%${v}%% → {{${v}}}` });
|
|
949
957
|
return `{{${v}}}`;
|
|
950
958
|
});
|
|
951
959
|
outputLines.push(converted);
|
|
@@ -971,7 +979,7 @@ export function handlebarsToAmpscript(code) {
|
|
|
971
979
|
const convertedCode = code.replaceAll(/\{\{([\s\S]*?)\}\}/g, (full, raw, offset) => {
|
|
972
980
|
lineCursor += countNewlines(code.slice(lastIndex, offset));
|
|
973
981
|
lastIndex = offset;
|
|
974
|
-
const
|
|
982
|
+
const lineNumber = lineCursor;
|
|
975
983
|
const inner = raw.trim();
|
|
976
984
|
// Comments — preserve as an AMPscript comment.
|
|
977
985
|
if (inner.startsWith('!')) {
|
|
@@ -980,7 +988,7 @@ export function handlebarsToAmpscript(code) {
|
|
|
980
988
|
// Block helpers / partials / closing tags — no deterministic AMPscript form.
|
|
981
989
|
if (/^[#/>]/.test(inner)) {
|
|
982
990
|
flaggedSections.push({
|
|
983
|
-
line:
|
|
991
|
+
line: lineNumber,
|
|
984
992
|
code: full,
|
|
985
993
|
reason: 'Handlebars block helper / partial has no direct AMPscript equivalent',
|
|
986
994
|
});
|
|
@@ -989,21 +997,21 @@ export function handlebarsToAmpscript(code) {
|
|
|
989
997
|
// Strip a leading no-escape ampersand: {{& x}}.
|
|
990
998
|
const body = inner.replace(/^&\s*/, '');
|
|
991
999
|
// Helper with arguments: {{helper arg1 arg2 …}}
|
|
992
|
-
const
|
|
993
|
-
if (
|
|
994
|
-
const helperName =
|
|
1000
|
+
const functionMatch = /^([A-Za-z_]\w*)\s+(.+)$/.exec(body);
|
|
1001
|
+
if (functionMatch) {
|
|
1002
|
+
const helperName = functionMatch[1];
|
|
995
1003
|
const key = helperName.toLowerCase();
|
|
996
1004
|
const ampName = HANDLEBARS_TO_AMP[key];
|
|
997
|
-
const
|
|
1005
|
+
const ampArguments = splitArguments(functionMatch[2]).map((a) => hbsArgumentToAmp(a));
|
|
998
1006
|
if (ampName) {
|
|
999
1007
|
changes.push({
|
|
1000
|
-
line:
|
|
1008
|
+
line: lineNumber,
|
|
1001
1009
|
description: `{{${helperName} …}} → %%=${ampName}(…)=%%`,
|
|
1002
1010
|
});
|
|
1003
|
-
return `%%=${ampName}(${
|
|
1011
|
+
return `%%=${ampName}(${ampArguments.join(', ')})=%%`;
|
|
1004
1012
|
}
|
|
1005
1013
|
flaggedSections.push({
|
|
1006
|
-
line:
|
|
1014
|
+
line: lineNumber,
|
|
1007
1015
|
code: full,
|
|
1008
1016
|
reason: `Handlebars helper '${helperName}' has no AMPscript equivalent`,
|
|
1009
1017
|
});
|
|
@@ -1013,14 +1021,14 @@ export function handlebarsToAmpscript(code) {
|
|
|
1013
1021
|
const bareMatch = /^([A-Za-z_]\w*)$/.exec(body);
|
|
1014
1022
|
if (bareMatch) {
|
|
1015
1023
|
changes.push({
|
|
1016
|
-
line:
|
|
1024
|
+
line: lineNumber,
|
|
1017
1025
|
description: `{{${bareMatch[1]}}} → %%=v(@${bareMatch[1]})=%%`,
|
|
1018
1026
|
});
|
|
1019
1027
|
return `%%=v(@${bareMatch[1]})=%%`;
|
|
1020
1028
|
}
|
|
1021
1029
|
// Dotted binding path or other expression — context-specific.
|
|
1022
1030
|
flaggedSections.push({
|
|
1023
|
-
line:
|
|
1031
|
+
line: lineNumber,
|
|
1024
1032
|
code: full,
|
|
1025
1033
|
reason: 'Handlebars binding path requires context-specific AMPscript mapping',
|
|
1026
1034
|
});
|
|
@@ -1090,15 +1098,15 @@ export function rewriteAmpForMcn(code, options) {
|
|
|
1090
1098
|
let rewrittenCode = code;
|
|
1091
1099
|
// 1. Remove StringToDate() wrapper inside FormatDate() first argument
|
|
1092
1100
|
// FormatDate(StringToDate(x), fmt) → FormatDate(x, fmt)
|
|
1093
|
-
const
|
|
1094
|
-
if (
|
|
1101
|
+
const stringToDatePattern = /FormatDate\s*\(\s*StringToDate\s*\(([^)]+)\)\s*,/gi;
|
|
1102
|
+
if (stringToDatePattern.test(rewrittenCode)) {
|
|
1095
1103
|
rewrittenCode = rewrittenCode.replaceAll(/FormatDate\s*\(\s*StringToDate\s*\(([^)]+)\)\s*,/gi, 'FormatDate($1,');
|
|
1096
1104
|
// Find affected lines for change tracking
|
|
1097
1105
|
const lines = code.split('\n');
|
|
1098
|
-
for (const [
|
|
1106
|
+
for (const [index, line] of lines.entries()) {
|
|
1099
1107
|
if (/FormatDate\s*\(\s*StringToDate\s*\(/i.test(line)) {
|
|
1100
1108
|
changes.push({
|
|
1101
|
-
line:
|
|
1109
|
+
line: index + 1,
|
|
1102
1110
|
type: 'rewritten',
|
|
1103
1111
|
description: 'Removed StringToDate() wrapper: FormatDate(StringToDate(x), fmt) → FormatDate(x, fmt)',
|
|
1104
1112
|
});
|
|
@@ -1106,104 +1114,102 @@ export function rewriteAmpForMcn(code, options) {
|
|
|
1106
1114
|
}
|
|
1107
1115
|
}
|
|
1108
1116
|
// 2. Convert .NET format strings to Java SimpleDateFormat in FormatDate() calls
|
|
1109
|
-
rewrittenCode = rewrittenCode.replaceAll(/FormatDate\s*\(\s*([^,]+),\s*"([^"]+)"\s*\)/gi, (match,
|
|
1110
|
-
let newFormat = formatStr;
|
|
1111
|
-
let changed = false;
|
|
1112
|
-
let hasShorthand = false;
|
|
1117
|
+
rewrittenCode = rewrittenCode.replaceAll(/FormatDate\s*\(\s*([^,]+),\s*"([^"]+)"\s*\)/gi, (match, argument1, formatString) => {
|
|
1113
1118
|
// Check for standard shorthands
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
hasShorthand = true;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
+
const trimmedFormat = formatString.trim();
|
|
1120
|
+
const hasShorthand = [...DOTNET_STANDARD_SHORTHANDS].some((shorthand) => new RegExp(`^${shorthand}$`).test(trimmedFormat));
|
|
1119
1121
|
if (hasShorthand) {
|
|
1120
1122
|
// Annotate with comment about needing explicit format
|
|
1121
|
-
return `FormatDate(${
|
|
1123
|
+
return `FormatDate(${argument1}, "/* MANUAL_REWRITE_REQUIRED: Convert .NET standard shorthand '${formatString}' to explicit Java SimpleDateFormat pattern */"${formatString}")`;
|
|
1122
1124
|
}
|
|
1125
|
+
let newFormat = formatString;
|
|
1126
|
+
let isChanged = false;
|
|
1123
1127
|
// Apply .NET → Java replacements
|
|
1124
1128
|
for (const [pattern, replacement] of DOTNET_TO_JAVA_FORMAT_REPLACEMENTS) {
|
|
1125
1129
|
const before = newFormat;
|
|
1126
|
-
newFormat = newFormat.replace(pattern, replacement);
|
|
1130
|
+
newFormat = newFormat.replace(pattern, () => replacement);
|
|
1127
1131
|
if (newFormat !== before) {
|
|
1128
|
-
|
|
1132
|
+
isChanged = true;
|
|
1129
1133
|
}
|
|
1130
1134
|
}
|
|
1131
|
-
if (
|
|
1132
|
-
return `FormatDate(${
|
|
1135
|
+
if (isChanged) {
|
|
1136
|
+
return `FormatDate(${argument1}, "${newFormat}")`;
|
|
1133
1137
|
}
|
|
1134
1138
|
return match;
|
|
1135
1139
|
});
|
|
1136
1140
|
// Track format string changes
|
|
1137
1141
|
const codeLines = code.split('\n');
|
|
1138
1142
|
const rewrittenLines = rewrittenCode.split('\n');
|
|
1139
|
-
for (const [
|
|
1140
|
-
if (codeLine !== rewrittenLines[
|
|
1143
|
+
for (const [index, codeLine] of codeLines.entries()) {
|
|
1144
|
+
if (codeLine !== rewrittenLines[index] && /FormatDate/i.test(codeLine)) {
|
|
1141
1145
|
changes.push({
|
|
1142
|
-
line:
|
|
1146
|
+
line: index + 1,
|
|
1143
1147
|
type: 'rewritten',
|
|
1144
1148
|
description: `Converted .NET format string to Java SimpleDateFormat in FormatDate()`,
|
|
1145
1149
|
});
|
|
1146
1150
|
}
|
|
1147
1151
|
}
|
|
1148
1152
|
// 3. Annotate Lookup() calls with odd argument counts
|
|
1149
|
-
rewrittenCode = rewrittenCode.replaceAll(/\bLookup\s*\(([^)]+)\)/gi, (match,
|
|
1150
|
-
const
|
|
1153
|
+
rewrittenCode = rewrittenCode.replaceAll(/\bLookup\s*\(([^)]+)\)/gi, (match, argumentsString) => {
|
|
1154
|
+
const argumentCount = countArgs(argumentsString);
|
|
1151
1155
|
// Lookup takes: DE, returnCol, [searchCol, searchVal, ...]
|
|
1152
1156
|
// Min 2 args, then pairs after that → should be even number > 2 or exactly 2
|
|
1153
1157
|
// Odd count after first 2 = problem
|
|
1154
|
-
if (
|
|
1155
|
-
return `${match} %%-- MCN NOTE: Lookup() requires search arguments in column/value pairs (even count after DE and return column). Current arg count (${
|
|
1158
|
+
if (argumentCount >= 3 && (argumentCount - 2) % 2 !== 0) {
|
|
1159
|
+
return `${match} %%-- MCN NOTE: Lookup() requires search arguments in column/value pairs (even count after DE and return column). Current arg count (${argumentCount}) may cause an error in MCN. --%% `;
|
|
1156
1160
|
}
|
|
1157
1161
|
return match;
|
|
1158
1162
|
});
|
|
1159
1163
|
// 4. Mark MCE-only function calls with annotation
|
|
1160
1164
|
// Find all function calls and mark unsupported ones
|
|
1161
|
-
const
|
|
1162
|
-
let
|
|
1165
|
+
const functionCallPattern = /\b([A-Z][A-Za-z]+)\s*\(/g;
|
|
1166
|
+
let functionMatch;
|
|
1163
1167
|
const seenUnsupported = new Set();
|
|
1164
|
-
while ((
|
|
1165
|
-
const
|
|
1166
|
-
if (!isMcnSupportedFn(
|
|
1167
|
-
seenUnsupported.add(
|
|
1168
|
+
while ((functionMatch = functionCallPattern.exec(code)) !== null) {
|
|
1169
|
+
const functionName = functionMatch[1];
|
|
1170
|
+
if (!isMcnSupportedFn(functionName) && functionName.length > 1) {
|
|
1171
|
+
seenUnsupported.add(functionName);
|
|
1168
1172
|
}
|
|
1169
1173
|
}
|
|
1170
|
-
for (const
|
|
1171
|
-
const mcnNotes = getMcnNotesFn(
|
|
1172
|
-
const annotationPattern = new RegExp(String.raw `\b${
|
|
1174
|
+
for (const functionName of seenUnsupported) {
|
|
1175
|
+
const mcnNotes = getMcnNotesFn(functionName);
|
|
1176
|
+
const annotationPattern = new RegExp(String.raw `\b${functionName}\s*\(`, 'gi');
|
|
1173
1177
|
rewrittenCode = rewrittenCode.replace(annotationPattern, (m) => {
|
|
1174
|
-
return `%%-- NOT SUPPORTED IN MCN: ${
|
|
1178
|
+
return `%%-- NOT SUPPORTED IN MCN: ${functionName}${mcnNotes ? ` — ${mcnNotes}` : ''} --%%\n${m}`;
|
|
1175
1179
|
});
|
|
1176
1180
|
// Find lines for tracking
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1181
|
+
const linePattern = new RegExp(String.raw `\b${functionName}\s*\(`, 'i');
|
|
1182
|
+
const matchingLines = codeLines
|
|
1183
|
+
.map((codeLine, index) => ({ codeLine, index }))
|
|
1184
|
+
.filter(({ codeLine }) => linePattern.test(codeLine));
|
|
1185
|
+
for (const { codeLine, index } of matchingLines) {
|
|
1186
|
+
changes.push({
|
|
1187
|
+
line: index + 1,
|
|
1188
|
+
type: 'annotated',
|
|
1189
|
+
description: `${functionName}() is not supported in Marketing Cloud Next`,
|
|
1190
|
+
});
|
|
1191
|
+
nonMigratableItems.push({
|
|
1192
|
+
line: index + 1,
|
|
1193
|
+
code: codeLine.trim(),
|
|
1194
|
+
reason: `${functionName}() is not available in Marketing Cloud Next`,
|
|
1195
|
+
});
|
|
1190
1196
|
}
|
|
1191
1197
|
}
|
|
1192
1198
|
// 5. Check for CloudPages functions
|
|
1193
|
-
const
|
|
1199
|
+
const cloudFunctionPattern = /\b(CloudPagesURL|RequestParameter|QueryParameter|Redirect|MicrositeURL)\s*\(/gi;
|
|
1194
1200
|
let cloudMatch;
|
|
1195
|
-
while ((cloudMatch =
|
|
1196
|
-
const
|
|
1197
|
-
const
|
|
1201
|
+
while ((cloudMatch = cloudFunctionPattern.exec(code)) !== null) {
|
|
1202
|
+
const functionName = cloudMatch[1];
|
|
1203
|
+
const lineNumber = code.slice(0, cloudMatch.index).split('\n').length;
|
|
1198
1204
|
nonMigratableItems.push({
|
|
1199
|
-
line:
|
|
1200
|
-
code: codeLines[
|
|
1201
|
-
reason: `${
|
|
1205
|
+
line: lineNumber,
|
|
1206
|
+
code: codeLines[lineNumber - 1]?.trim() ?? functionName,
|
|
1207
|
+
reason: `${functionName}() is a CloudPages-specific function and cannot run in Marketing Cloud Next`,
|
|
1202
1208
|
});
|
|
1203
1209
|
}
|
|
1204
1210
|
// Assess difficulty
|
|
1205
1211
|
const hasNotMigratable = nonMigratableItems.some((item) => item.reason.includes('CloudPages') || item.reason.includes('NOT SUPPORTED'));
|
|
1206
|
-
const hasMcnNotes = Array.from(seenUnsupported).some((
|
|
1212
|
+
const hasMcnNotes = Array.from(seenUnsupported).some((function_) => getMcnNotesFn(function_) !== null) ||
|
|
1207
1213
|
/FormatDate|StringToDate|Lookup/i.test(code);
|
|
1208
1214
|
const hasSsjs = /<script[^>]+runat/i.test(code);
|
|
1209
1215
|
let difficulty;
|