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.
@@ -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 [i, original] of rawLines.entries()) {
241
- const lineNum = i + 1;
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: lineNum,
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
- let isFlagged = false;
258
- for (const { pattern, reason } of NON_MIGRATABLE_SSJS_PATTERNS) {
259
- // Reset lastIndex for global regexes
260
- pattern.lastIndex = 0;
261
- if (pattern.test(trimmed)) {
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, (_, varName, value) => {
279
+ line = line.replaceAll(/Platform\.Variable\.SetValue\s*\(\s*["']([^"']+)["']\s*,\s*([^)]+)\)\s*;?/gi, (_, variableName, value) => {
276
280
  changes.push({
277
- line: lineNum,
278
- description: `Platform.Variable.SetValue → SET @${varName}`,
281
+ line: lineNumber,
282
+ description: `Platform.Variable.SetValue → SET @${variableName}`,
279
283
  });
280
- return `SET @${varName} = ${value.trim()}`;
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: lineNum, description: 'Platform.Response.Write → OutputLine' });
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, (_, fnName) => {
289
- const key = fnName.toLowerCase();
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: lineNum,
293
- code: `Platform.Function.${fnName}(...)`,
294
- reason: `Platform.Function.${fnName} has no AMPscript equivalent`,
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.${fnName} has no AMPscript equivalent */ ${fnName}(`;
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: lineNum,
302
- code: `Platform.Function.${fnName}(...)`,
303
- reason: `Platform.Function.${fnName} not found in ssjs-data catalog`,
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.${fnName} */ ${fnName}(`;
309
+ return `/* MANUAL_REWRITE_REQUIRED: unknown Platform.Function.${functionName} */ ${functionName}(`;
306
310
  }
307
311
  changes.push({
308
- line: lineNum,
309
- description: `Platform.Function.${fnName} → ${ampName}`,
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*$/, (_, varName, value) => {
318
+ line = line.replace(/\bvar\s+([A-Za-z_]\w*)\s*=\s*(.+?)\s*;?\s*$/, (_, variableName, value) => {
315
319
  changes.push({
316
- line: lineNum,
317
- description: `var ${varName} = ... → SET @${varName}`,
320
+ line: lineNumber,
321
+ description: `var ${variableName} = ... → SET @${variableName}`,
318
322
  });
319
- return `SET @${varName} = ${value.trim()}`;
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*$/, (_, varName) => {
323
- changes.push({ line: lineNum, description: `var ${varName} → VAR @${varName}` });
324
- return `VAR @${varName}`;
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: lineNum, description: 'if (...) { → IF ... THEN' });
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: lineNum, description: '} else if (...) { → ELSEIF ... THEN' });
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: lineNum, description: '} else { → ELSE' });
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 [, iterVar, start, end] = forMatch;
353
+ const [, iterVariable, start, end] = forMatch;
347
354
  changes.push({
348
- line: lineNum,
349
- description: `for (var ${iterVar}...) → FOR @${iterVar} = ${start} TO ${end} DO`,
355
+ line: lineNumber,
356
+ description: `for (var ${iterVariable}...) → FOR @${iterVariable} = ${start} TO ${end} DO`,
350
357
  });
351
- line = `FOR @${iterVar} = ${start} TO ${end} DO`;
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: lineNum, description: '} → ENDIF' });
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 [i, line] of lines.entries()) {
412
- const lineNum = i + 1 + lineOffset;
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 expr = stripAmpVars(inlineOutputMatch[1].trim());
428
+ const expression = stripAmpVars(inlineOutputMatch[1].trim());
422
429
  changes.push({
423
- line: lineNum,
430
+ line: lineNumber,
424
431
  description: '%%=Output(...)=%% → Platform.Response.Write(...)',
425
432
  });
426
- outputLines.push(`Platform.Response.Write(${expr});`);
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 inlineFnMatch = /^%%=\s*(\w+)\s*\((.*)?\)\s*=%%$/i.exec(trimmed);
431
- if (inlineFnMatch) {
432
- const fnName = inlineFnMatch[1];
433
- const args = inlineFnMatch[2]?.trim() ?? '';
434
- const key = fnName.toLowerCase();
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 argsConverted = stripAmpVars(args);
443
+ const nativeHint = AMP_NATIVE_JS_HINTS[key];
444
+ const argumentsConverted = stripAmpVars(arguments_);
437
445
  if (ssName) {
438
446
  changes.push({
439
- line: lineNum,
440
- description: `%%=${fnName}(...)=%% → Platform.Response.Write(Platform.Function.${ssName}(...))`,
447
+ line: lineNumber,
448
+ description: `%%=${functionName}(...)=%% → Platform.Response.Write(Platform.Function.${ssName}(...))`,
441
449
  });
442
- outputLines.push(`Platform.Response.Write(Platform.Function.${ssName}(${argsConverted}));`);
450
+ outputLines.push(`Platform.Response.Write(Platform.Function.${ssName}(${argumentsConverted}));`);
443
451
  }
444
- else if (AMP_NATIVE_JS_HINTS[key]) {
452
+ else if (nativeHint) {
445
453
  changes.push({
446
- line: lineNum,
447
- description: `%%=${fnName}(...)=%% → native JS hint`,
454
+ line: lineNumber,
455
+ description: `%%=${functionName}(...)=%% → native JS hint`,
448
456
  });
449
- outputLines.push(`Platform.Response.Write(${AMP_NATIVE_JS_HINTS[key]} /* AMPscript: ${fnName}(${args}) */);`);
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: %%=${fnName}(${args})=%% */`);
461
+ outputLines.push(`/* MANUAL_REWRITE_REQUIRED: %%=${functionName}(${arguments_})=%% */`);
454
462
  flaggedSections.push({
455
- line: lineNum,
463
+ line: lineNumber,
456
464
  code: trimmed,
457
- reason: `AMPscript function '${fnName}' is CloudPages-only — no SSJS equivalent`,
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: lineNum,
465
- description: `%%=${fnName}(...)=%% → _ampScript polyfill`,
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 stmts = blockContent.split(/\n+/);
476
- for (const stmt of stmts) {
477
- const converted = convertAmpStatement(stmt.trim(), lineNum, changes, flaggedSections, polyfillUsed);
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, lineNum, changes, flaggedSections, polyfillUsed);
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, lineNum, changes, flaggedSections, polyfillUsed);
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 [, varName, expr] = setMatch;
551
- const exprTrimmed = expr.trim();
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 singleFnMatch = /^(\w+)\s*\(/.exec(exprTrimmed);
555
- if (singleFnMatch) {
556
- const key = singleFnMatch[1].toLowerCase();
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 @${varName} = ${singleFnMatch[1]}(...) → _ampScript polyfill`,
571
+ description: `SET @${variableName} = ${singleFunctionMatch[1]}(...) → _ampScript polyfill`,
564
572
  });
565
- return `var ${varName} = _ampScript('${exprTrimmed.replaceAll("'", String.raw `\'`)}');`;
573
+ return `var ${variableName} = _ampScript('${expressionTrimmed.replaceAll("'", String.raw `\'`)}');`;
566
574
  }
567
575
  }
568
- const ssExpr = convertAmpExpr(exprTrimmed);
576
+ const ssExpression = convertAmpExpression(expressionTrimmed);
569
577
  changes.push({
570
578
  line: lineNum,
571
- description: `SET @${varName} = ... → var ${varName} = ...`,
579
+ description: `SET @${variableName} = ... → var ${variableName} = ...`,
572
580
  });
573
- return `var ${varName} = ${ssExpr};`;
581
+ return `var ${variableName} = ${ssExpression};`;
574
582
  }
575
583
  // VAR @x, @y → var x, y;
576
- const varMatch = /^VAR\s+(.+)$/i.exec(stmt);
577
- if (varMatch) {
578
- const vars = varMatch[1].split(',').map((v) => v.trim().replace(/^@/, ''));
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 @${vars.join(', @')} → var ${vars.join(', ')}`,
591
+ description: `VAR @${variables.join(', @')} → var ${variables.join(', ')}`,
582
592
  });
583
- return `var ${vars.join(', ')};`;
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 [, iterVar, start, end] = forMatch;
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 @${iterVar} = ${start} TO ${end} DO → for loop`,
627
+ description: `FOR @${iterVariable} = ${start} TO ${end} DO → for loop`,
618
628
  });
619
- return `for (var ${iterVar} = ${ssStart}; ${iterVar} <= ${ssEnd}; ${iterVar}++) {`;
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 expr = convertAmpExpr(outputMatch[1].trim());
639
+ const expression = convertAmpExpression(outputMatch[1].trim());
630
640
  changes.push({ line: lineNum, description: 'Output/OutputLine → Platform.Response.Write' });
631
- return `Platform.Response.Write(${expr});`;
641
+ return `Platform.Response.Write(${expression});`;
632
642
  }
633
643
  // Known AMPscript function call → Platform.Function.X(args)
634
- const fnCallMatch = /^(\w+)\s*\((.*)?\)$/i.exec(stmt);
635
- if (fnCallMatch) {
636
- const [, fnName, args] = fnCallMatch;
637
- const key = fnName.toLowerCase();
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 argsConverted = args ? convertAmpExpr(args.trim()) : '';
650
+ const argumentsConverted = arguments_ ? convertAmpExpression(arguments_.trim()) : '';
641
651
  changes.push({
642
652
  line: lineNum,
643
- description: `${fnName}(…) → Platform.Function.${ssName}(…)`,
653
+ description: `${functionName}(…) → Platform.Function.${ssName}(…)`,
644
654
  });
645
- return `Platform.Function.${ssName}(${argsConverted});`;
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: `${fnName}(…) → native JS equivalent`,
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: `${fnName}(…) → _ampScript polyfill (no direct SSJS equivalent)`,
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 convertAmpExpr(expr) {
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, fnName) => {
706
- const key = fnName.toLowerCase();
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} /* ${fnName}( */`;
722
+ return `${hint} /* ${functionName}( */`;
713
723
  // Unknown function in expression context — annotate
714
- return `/* MANUAL_REWRITE_REQUIRED: no SSJS equivalent for ${fnName} */ ${fnName}(`;
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
- * Split a function-argument string into top-level comma-separated arguments,
754
- * respecting nested parens/brackets and quoted strings. Returns an empty array
755
- * for an empty/whitespace-only string.
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 splitArgs(argsStr) {
760
- if (!argsStr.trim())
761
- return [];
762
- const args = [];
763
- let depth = 0;
764
- let current = '';
765
- let quote = null;
766
- for (const ch of argsStr) {
767
- if (quote) {
768
- current += ch;
769
- if (ch === quote)
770
- quote = null;
771
- continue;
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
- switch (ch) {
774
- case '"':
775
- case "'": {
776
- quote = ch;
777
- current += ch;
778
- break;
779
- }
780
- case '(':
781
- case '[': {
782
- depth++;
783
- current += ch;
784
- break;
785
- }
786
- case ')':
787
- case ']': {
788
- depth--;
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
- default: {
793
- if (ch === ',' && depth === 0) {
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
- if (current.trim())
804
- args.push(current.trim());
805
- return args;
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 ampArgToHbs(arg) {
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 hbsArgToAmp(arg) {
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 varMatch = /^@?([A-Za-z_]\w*)$/.exec(inner);
858
- if (varMatch) {
859
- changes.push({ line: lineNum, description: `%%=${inner}=%% → {{${varMatch[1]}}}` });
860
- return `{{${varMatch[1]}}}`;
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 fnMatch = /^([A-Za-z_]\w*)\s*\(([\s\S]*)\)$/.exec(inner);
864
- if (fnMatch) {
865
- const fnName = fnMatch[1];
866
- const key = fnName.toLowerCase();
867
- const hbsArgs = splitArgs(fnMatch[2]).map((a) => ampArgToHbs(a));
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: `%%=${fnName}(…)=%% → {{${helper} …}}`,
881
+ description: `%%=${functionName}(…)=%% → {{${helper} …}}`,
874
882
  });
875
- return `{{${helper}${hbsArgs.length > 0 ? ' ' + hbsArgs.join(' ') : ''}}}`;
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: `${fnName} is ${HBS_GAP_NOTE}`,
890
+ reason: `${functionName} is ${HBS_GAP_NOTE}`,
883
891
  });
884
- return `{{!-- MANUAL_REWRITE_REQUIRED: ${fnName} is ${HBS_GAP_NOTE} --}}`;
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 '${fnName}' has no Handlebars equivalent`,
898
+ reason: `AMPscript function '${functionName}' has no Handlebars equivalent`,
891
899
  });
892
- return `{{!-- MANUAL_REWRITE_REQUIRED: AMPscript function '${fnName}' has no Handlebars equivalent --}}`;
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 inBlock = false;
924
- for (const [i, line] of lines.entries()) {
925
- const lineNum = i + 1;
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 (inBlock) {
936
+ if (isInBlock) {
929
937
  if (/\]%%/.test(line))
930
- inBlock = false;
938
+ isInBlock = false;
931
939
  continue;
932
940
  }
933
941
  if (/%%\[/.test(line)) {
934
942
  if (!/\]%%/.test(line))
935
- inBlock = true;
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: lineNum,
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(), lineNum, changes, flaggedSections))
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: lineNum, description: `%%${v}%% → {{${v}}}` });
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 lineNum = lineCursor;
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: lineNum,
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 fnMatch = /^([A-Za-z_]\w*)\s+(.+)$/.exec(body);
993
- if (fnMatch) {
994
- const helperName = fnMatch[1];
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 ampArgs = splitArgs(fnMatch[2]).map((a) => hbsArgToAmp(a));
1005
+ const ampArguments = splitArguments(functionMatch[2]).map((a) => hbsArgumentToAmp(a));
998
1006
  if (ampName) {
999
1007
  changes.push({
1000
- line: lineNum,
1008
+ line: lineNumber,
1001
1009
  description: `{{${helperName} …}} → %%=${ampName}(…)=%%`,
1002
1010
  });
1003
- return `%%=${ampName}(${ampArgs.join(', ')})=%%`;
1011
+ return `%%=${ampName}(${ampArguments.join(', ')})=%%`;
1004
1012
  }
1005
1013
  flaggedSections.push({
1006
- line: lineNum,
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: lineNum,
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: lineNum,
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 strToDatePattern = /FormatDate\s*\(\s*StringToDate\s*\(([^)]+)\)\s*,/gi;
1094
- if (strToDatePattern.test(rewrittenCode)) {
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 [i, line] of lines.entries()) {
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: i + 1,
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, arg1, formatStr) => {
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
- for (const shorthand of DOTNET_STANDARD_SHORTHANDS) {
1115
- if (new RegExp(`^${shorthand}$`).test(formatStr.trim())) {
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(${arg1}, "/* MANUAL_REWRITE_REQUIRED: Convert .NET standard shorthand '${formatStr}' to explicit Java SimpleDateFormat pattern */"${formatStr}")`;
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
- changed = true;
1132
+ isChanged = true;
1129
1133
  }
1130
1134
  }
1131
- if (changed) {
1132
- return `FormatDate(${arg1}, "${newFormat}")`;
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 [i, codeLine] of codeLines.entries()) {
1140
- if (codeLine !== rewrittenLines[i] && /FormatDate/i.test(codeLine)) {
1143
+ for (const [index, codeLine] of codeLines.entries()) {
1144
+ if (codeLine !== rewrittenLines[index] && /FormatDate/i.test(codeLine)) {
1141
1145
  changes.push({
1142
- line: i + 1,
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, argsStr) => {
1150
- const argCount = countArgs(argsStr);
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 (argCount >= 3 && (argCount - 2) % 2 !== 0) {
1155
- return `${match} %%-- MCN NOTE: Lookup() requires search arguments in column/value pairs (even count after DE and return column). Current arg count (${argCount}) may cause an error in MCN. --%% `;
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 funcCallPattern = /\b([A-Z][A-Za-z]+)\s*\(/g;
1162
- let funcMatch;
1165
+ const functionCallPattern = /\b([A-Z][A-Za-z]+)\s*\(/g;
1166
+ let functionMatch;
1163
1167
  const seenUnsupported = new Set();
1164
- while ((funcMatch = funcCallPattern.exec(code)) !== null) {
1165
- const fnName = funcMatch[1];
1166
- if (!isMcnSupportedFn(fnName) && fnName.length > 1) {
1167
- seenUnsupported.add(fnName);
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 fnName of seenUnsupported) {
1171
- const mcnNotes = getMcnNotesFn(fnName);
1172
- const annotationPattern = new RegExp(String.raw `\b${fnName}\s*\(`, 'gi');
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: ${fnName}${mcnNotes ? ` — ${mcnNotes}` : ''} --%%\n${m}`;
1178
+ return `%%-- NOT SUPPORTED IN MCN: ${functionName}${mcnNotes ? ` — ${mcnNotes}` : ''} --%%\n${m}`;
1175
1179
  });
1176
1180
  // Find lines for tracking
1177
- for (const [i, codeLine] of codeLines.entries()) {
1178
- if (new RegExp(String.raw `\b${fnName}\s*\(`, 'i').test(codeLine)) {
1179
- changes.push({
1180
- line: i + 1,
1181
- type: 'annotated',
1182
- description: `${fnName}() is not supported in Marketing Cloud Next`,
1183
- });
1184
- nonMigratableItems.push({
1185
- line: i + 1,
1186
- code: codeLine.trim(),
1187
- reason: `${fnName}() is not available in Marketing Cloud Next`,
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 cloudFnPattern = /\b(CloudPagesURL|RequestParameter|QueryParameter|Redirect|MicrositeURL)\s*\(/gi;
1199
+ const cloudFunctionPattern = /\b(CloudPagesURL|RequestParameter|QueryParameter|Redirect|MicrositeURL)\s*\(/gi;
1194
1200
  let cloudMatch;
1195
- while ((cloudMatch = cloudFnPattern.exec(code)) !== null) {
1196
- const fnName = cloudMatch[1];
1197
- const lineNum = code.slice(0, cloudMatch.index).split('\n').length;
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: lineNum,
1200
- code: codeLines[lineNum - 1]?.trim() ?? fnName,
1201
- reason: `${fnName}() is a CloudPages-specific function and cannot run in Marketing Cloud Next`,
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((fn) => getMcnNotesFn(fn) !== null) ||
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;