prettier-plugin-sfmc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/docs/options/ampscript-block-line-breaks.md +64 -0
- package/docs/options/ampscript-enforce-variable-casing.md +63 -0
- package/docs/options/ampscript-function-case.md +82 -0
- package/docs/options/ampscript-keyword-case.md +74 -0
- package/docs/options/ampscript-quote-style.md +69 -0
- package/docs/options/ampscript-remove-unnecessary-brackets.md +63 -0
- package/docs/options/ampscript-spacing.md +54 -0
- package/docs/options/ampscript-var-declaration-style.md +86 -0
- package/package.json +56 -0
- package/src/index.js +320 -0
- package/src/printer.js +428 -0
package/src/printer.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AMPscript Printer
|
|
3
|
+
*
|
|
4
|
+
* Converts the AMPscript AST into Prettier's intermediate Doc representation.
|
|
5
|
+
* Uses Prettier's recursive print callback for child traversal so that
|
|
6
|
+
* hasPrettierIgnore, comment attachment, and embed work on all nodes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as prettier from 'prettier';
|
|
10
|
+
import { FUNCTION_CANONICAL_MAP, AMPSCRIPT_KEYWORDS } from 'ampscript-data';
|
|
11
|
+
|
|
12
|
+
const { group, indent, join, line, softline, hardline } = prettier.doc.builders;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Collect all variable names in first-occurrence order for consistent casing.
|
|
16
|
+
*/
|
|
17
|
+
function collectVariableMap(node) {
|
|
18
|
+
const map = new Map(); // lowercase -> first-seen casing
|
|
19
|
+
|
|
20
|
+
function walk(n) {
|
|
21
|
+
if (!n || typeof n !== 'object') return;
|
|
22
|
+
|
|
23
|
+
if (n.type === 'Variable' && n.value) {
|
|
24
|
+
const lower = n.value.toLowerCase();
|
|
25
|
+
if (!map.has(lower)) {
|
|
26
|
+
map.set(lower, n.value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(n.children)) n.children.forEach(walk);
|
|
31
|
+
if (Array.isArray(n.statements)) n.statements.forEach(walk);
|
|
32
|
+
if (n.target) walk(n.target);
|
|
33
|
+
if (n.value && typeof n.value === 'object') walk(n.value);
|
|
34
|
+
if (n.expression) walk(n.expression);
|
|
35
|
+
if (n.condition) walk(n.condition);
|
|
36
|
+
if (n.left) walk(n.left);
|
|
37
|
+
if (n.right) walk(n.right);
|
|
38
|
+
if (n.argument) walk(n.argument);
|
|
39
|
+
if (Array.isArray(n.arguments)) n.arguments.forEach(walk);
|
|
40
|
+
if (Array.isArray(n.variables)) n.variables.forEach(walk);
|
|
41
|
+
if (Array.isArray(n.consequent)) n.consequent.forEach(walk);
|
|
42
|
+
if (Array.isArray(n.alternates)) {
|
|
43
|
+
for (const alt of n.alternates) {
|
|
44
|
+
if (alt.condition) walk(alt.condition);
|
|
45
|
+
if (Array.isArray(alt.body)) alt.body.forEach(walk);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(n.body)) n.body.forEach(walk);
|
|
49
|
+
if (n.counter) walk(n.counter);
|
|
50
|
+
if (n.startExpr) walk(n.startExpr);
|
|
51
|
+
if (n.endExpr) walk(n.endExpr);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
walk(node);
|
|
55
|
+
return map;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Detect needless parentheses around a single variable or literal.
|
|
60
|
+
*/
|
|
61
|
+
function isNeedlessParenExpression(node) {
|
|
62
|
+
if (node.type !== 'ParenExpression') return false;
|
|
63
|
+
const inner = node.expression;
|
|
64
|
+
return (
|
|
65
|
+
inner.type === 'Variable' ||
|
|
66
|
+
inner.type === 'StringLiteral' ||
|
|
67
|
+
inner.type === 'NumberLiteral' ||
|
|
68
|
+
inner.type === 'BooleanLiteral' ||
|
|
69
|
+
inner.type === 'Identifier'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const OPERATOR_PRECEDENCE = {
|
|
74
|
+
or: 1,
|
|
75
|
+
and: 2,
|
|
76
|
+
not: 3,
|
|
77
|
+
'==': 4,
|
|
78
|
+
'!=': 4,
|
|
79
|
+
'<': 4,
|
|
80
|
+
'>': 4,
|
|
81
|
+
'<=': 4,
|
|
82
|
+
'>=': 4,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function getOperatorPrecedence(op) {
|
|
86
|
+
return OPERATOR_PRECEDENCE[op.toLowerCase()] || 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Context-aware check for whether a ParenExpression's parens can be removed.
|
|
91
|
+
* Covers simple atoms, function calls, nested parens, statement-level
|
|
92
|
+
* positions, and precedence-based removal inside binary expressions.
|
|
93
|
+
*/
|
|
94
|
+
function canRemoveParens(path, node) {
|
|
95
|
+
const inner = node.expression;
|
|
96
|
+
|
|
97
|
+
if (isNeedlessParenExpression(node)) return true;
|
|
98
|
+
|
|
99
|
+
if (inner.type === 'FunctionCall' || inner.type === 'ParenExpression') {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parent = path.getParentNode();
|
|
104
|
+
if (!parent) return false;
|
|
105
|
+
|
|
106
|
+
const STATEMENT_PARENTS = [
|
|
107
|
+
'SetStatement',
|
|
108
|
+
'IfStatement',
|
|
109
|
+
'ElseIfClause',
|
|
110
|
+
'ExpressionStatement',
|
|
111
|
+
'ForStatement',
|
|
112
|
+
'InlineExpression',
|
|
113
|
+
];
|
|
114
|
+
if (STATEMENT_PARENTS.includes(parent.type)) return true;
|
|
115
|
+
|
|
116
|
+
if (parent.type === 'BinaryExpression') {
|
|
117
|
+
const parentPrec = getOperatorPrecedence(parent.operator);
|
|
118
|
+
|
|
119
|
+
if (inner.type === 'BinaryExpression') {
|
|
120
|
+
return getOperatorPrecedence(inner.operator) > parentPrec;
|
|
121
|
+
}
|
|
122
|
+
if (inner.type === 'UnaryExpression') {
|
|
123
|
+
return getOperatorPrecedence(inner.operator) > parentPrec;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build the Doc for a single AST node using Prettier's recursive print
|
|
132
|
+
* callback so that hasPrettierIgnore, comment attachment, and embed
|
|
133
|
+
* processing are applied to every child node.
|
|
134
|
+
*
|
|
135
|
+
* @param {import("prettier").AstPath} path
|
|
136
|
+
* @param {object} options Merged Prettier + plugin options
|
|
137
|
+
* @param {function} print Prettier recursive print callback
|
|
138
|
+
*/
|
|
139
|
+
function printAmpscriptNode(path, options, print) {
|
|
140
|
+
const node = path.node;
|
|
141
|
+
if (!node) return '';
|
|
142
|
+
|
|
143
|
+
const variableMap = options.__ampscriptVariableMap || new Map();
|
|
144
|
+
const enforceSpacing = options.ampscriptSpacing;
|
|
145
|
+
const quoteChar = options.ampscriptQuoteStyle === 'single' ? "'" : '"';
|
|
146
|
+
const enforceCasing = options.ampscriptEnforceVariableCasing;
|
|
147
|
+
const removeBrackets = options.ampscriptRemoveUnnecessaryBrackets;
|
|
148
|
+
const variableStyle = options.ampscriptVarDeclarationStyle || 'auto';
|
|
149
|
+
const keywordCase = options.ampscriptKeywordCase || 'lower';
|
|
150
|
+
const functionCase = options.ampscriptFunctionCase || 'upper-camel';
|
|
151
|
+
const blockLineBreaks = options.ampscriptBlockLineBreaks !== false;
|
|
152
|
+
|
|
153
|
+
function resolveVariable(variableName) {
|
|
154
|
+
if (!enforceCasing) return variableName;
|
|
155
|
+
const lower = variableName.toLowerCase();
|
|
156
|
+
return variableMap.get(lower) || variableName;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Apply keyword casing rule. Pass originalText for preserve mode. */
|
|
160
|
+
function kw(keyword, originalText) {
|
|
161
|
+
if (keywordCase === 'upper') return keyword.toUpperCase();
|
|
162
|
+
if (keywordCase === 'lower') return keyword.toLowerCase();
|
|
163
|
+
return originalText || keyword;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Apply function casing rule */
|
|
167
|
+
function function_(name) {
|
|
168
|
+
if (functionCase === 'preserve') return name;
|
|
169
|
+
if (functionCase === 'upper') return name.toUpperCase();
|
|
170
|
+
if (functionCase === 'lower') return name.toLowerCase();
|
|
171
|
+
const canonical = FUNCTION_CANONICAL_MAP.get(name.toLowerCase()) || name;
|
|
172
|
+
if (functionCase === 'lower-camel') {
|
|
173
|
+
return canonical[0].toLowerCase() + canonical.slice(1);
|
|
174
|
+
}
|
|
175
|
+
return canonical;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function printString(content, originalQuote) {
|
|
179
|
+
const altChar = quoteChar === "'" ? '"' : "'";
|
|
180
|
+
if (content.includes(quoteChar)) {
|
|
181
|
+
if (!content.includes(altChar)) {
|
|
182
|
+
return [altChar, content, altChar];
|
|
183
|
+
}
|
|
184
|
+
const safeQuote = originalQuote || quoteChar;
|
|
185
|
+
return [safeQuote, content, safeQuote];
|
|
186
|
+
}
|
|
187
|
+
return [quoteChar, content, quoteChar];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
switch (node.type) {
|
|
191
|
+
case 'Document': {
|
|
192
|
+
return path.map(print, 'children');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case 'Content': {
|
|
196
|
+
return node.value;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'Block': {
|
|
200
|
+
const stmts = path.map(print, 'statements');
|
|
201
|
+
const stmtNodes = node.statements;
|
|
202
|
+
const body = [];
|
|
203
|
+
for (const [index, stmt] of stmts.entries()) {
|
|
204
|
+
if (index > 0) {
|
|
205
|
+
body.push(hardline);
|
|
206
|
+
if (stmtNodes[index] && stmtNodes[index].blankLineBefore) {
|
|
207
|
+
body.push(hardline);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
body.push(stmt);
|
|
211
|
+
}
|
|
212
|
+
const isScriptTag = node.syntax === 'script-tag';
|
|
213
|
+
const blockOpen = isScriptTag ? '<script runat="server" language="ampscript">' : '%%[';
|
|
214
|
+
const blockClose = isScriptTag ? '</script>' : ']%%';
|
|
215
|
+
const blockDocument = group([
|
|
216
|
+
blockOpen,
|
|
217
|
+
indent([hardline, ...body]),
|
|
218
|
+
hardline,
|
|
219
|
+
blockClose,
|
|
220
|
+
]);
|
|
221
|
+
if (blockLineBreaks) {
|
|
222
|
+
return [hardline, blockDocument, hardline];
|
|
223
|
+
}
|
|
224
|
+
return blockDocument;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
case 'InlineExpression': {
|
|
228
|
+
const expr = print('expression');
|
|
229
|
+
if (enforceSpacing) {
|
|
230
|
+
return ['%%= ', expr, ' =%%'];
|
|
231
|
+
}
|
|
232
|
+
return ['%%=', expr, '=%%'];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
case 'Comment': {
|
|
236
|
+
return node.value;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'VarDeclaration': {
|
|
240
|
+
const variables = path.map(print, 'variables');
|
|
241
|
+
if (variableStyle === 'multi-line') {
|
|
242
|
+
return [
|
|
243
|
+
kw('var', node.originalKeyword),
|
|
244
|
+
' ',
|
|
245
|
+
indent(join([',', hardline], variables)),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
if (variableStyle === 'single-line') {
|
|
249
|
+
return [kw('var', node.originalKeyword), ' ', join(', ', variables)];
|
|
250
|
+
}
|
|
251
|
+
return group([
|
|
252
|
+
kw('var', node.originalKeyword),
|
|
253
|
+
' ',
|
|
254
|
+
indent(join([',', line], variables)),
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'SetStatement': {
|
|
259
|
+
const target = node.target ? resolveVariable(node.target.value) : '';
|
|
260
|
+
const value = print('value');
|
|
261
|
+
return group([
|
|
262
|
+
kw('set', node.originalKeyword),
|
|
263
|
+
' ',
|
|
264
|
+
target,
|
|
265
|
+
' =',
|
|
266
|
+
indent([line, value]),
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
case 'IfStatement': {
|
|
271
|
+
const okw = node.originalKeywords || {};
|
|
272
|
+
const parts = [];
|
|
273
|
+
parts.push(
|
|
274
|
+
group([kw('if', okw.if), ' ', print('condition'), ' ', kw('then', okw.then)]),
|
|
275
|
+
);
|
|
276
|
+
if (node.consequent.length > 0) {
|
|
277
|
+
parts.push(indent([hardline, join(hardline, path.map(print, 'consequent'))]));
|
|
278
|
+
}
|
|
279
|
+
const altDocs = path.map(print, 'alternates');
|
|
280
|
+
for (const altDocument of altDocs) {
|
|
281
|
+
parts.push(altDocument);
|
|
282
|
+
}
|
|
283
|
+
parts.push([hardline, kw('endif', okw.endif)]);
|
|
284
|
+
return parts;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case 'ElseIfClause': {
|
|
288
|
+
const akw = node.originalKeywords || {};
|
|
289
|
+
const parts = [];
|
|
290
|
+
parts.push(
|
|
291
|
+
group([
|
|
292
|
+
hardline,
|
|
293
|
+
kw('elseif', akw.elseif),
|
|
294
|
+
' ',
|
|
295
|
+
print('condition'),
|
|
296
|
+
' ',
|
|
297
|
+
kw('then', akw.then),
|
|
298
|
+
]),
|
|
299
|
+
);
|
|
300
|
+
if (node.body.length > 0) {
|
|
301
|
+
parts.push(indent([hardline, join(hardline, path.map(print, 'body'))]));
|
|
302
|
+
}
|
|
303
|
+
return parts;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'ElseClause': {
|
|
307
|
+
const akw = node.originalKeywords || {};
|
|
308
|
+
const parts = [];
|
|
309
|
+
parts.push([hardline, kw('else', akw.else)]);
|
|
310
|
+
if (node.body.length > 0) {
|
|
311
|
+
parts.push(indent([hardline, join(hardline, path.map(print, 'body'))]));
|
|
312
|
+
}
|
|
313
|
+
return parts;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'ForStatement': {
|
|
317
|
+
const okw = node.originalKeywords || {};
|
|
318
|
+
const counterDocument = node.counter ? print('counter') : '';
|
|
319
|
+
const startDocument = print('startExpr');
|
|
320
|
+
const endDocument = print('endExpr');
|
|
321
|
+
const dir = kw(node.direction, okw.direction);
|
|
322
|
+
const parts = [];
|
|
323
|
+
parts.push([
|
|
324
|
+
kw('for', okw.for),
|
|
325
|
+
' ',
|
|
326
|
+
counterDocument,
|
|
327
|
+
' = ',
|
|
328
|
+
startDocument,
|
|
329
|
+
' ',
|
|
330
|
+
dir,
|
|
331
|
+
' ',
|
|
332
|
+
endDocument,
|
|
333
|
+
' ',
|
|
334
|
+
kw('do', okw.do),
|
|
335
|
+
]);
|
|
336
|
+
if (node.body.length > 0) {
|
|
337
|
+
parts.push(indent([hardline, join(hardline, path.map(print, 'body'))]));
|
|
338
|
+
}
|
|
339
|
+
parts.push([hardline, kw('next', okw.next), ' ', counterDocument]);
|
|
340
|
+
return parts;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case 'ExpressionStatement': {
|
|
344
|
+
return print('expression');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
case 'FunctionCall': {
|
|
348
|
+
const arguments_ = path.map(print, 'arguments');
|
|
349
|
+
const functionName = function_(node.name);
|
|
350
|
+
return group([
|
|
351
|
+
functionName,
|
|
352
|
+
'(',
|
|
353
|
+
indent([softline, join([',', line], arguments_)]),
|
|
354
|
+
softline,
|
|
355
|
+
')',
|
|
356
|
+
]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case 'Variable': {
|
|
360
|
+
return resolveVariable(node.value);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
case 'Identifier': {
|
|
364
|
+
return node.value;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
case 'StringLiteral': {
|
|
368
|
+
return printString(node.value, node.quote);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case 'NumberLiteral': {
|
|
372
|
+
return node.value;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case 'BooleanLiteral': {
|
|
376
|
+
return kw(node.value, node.originalValue);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
case 'BinaryExpression': {
|
|
380
|
+
const left = print('left');
|
|
381
|
+
const right = print('right');
|
|
382
|
+
const opLower = node.operator.toLowerCase();
|
|
383
|
+
const op = AMPSCRIPT_KEYWORDS.includes(opLower)
|
|
384
|
+
? kw(node.operator, node.originalOperator)
|
|
385
|
+
: node.operator;
|
|
386
|
+
if (opLower === 'and' || opLower === 'or') {
|
|
387
|
+
return group([left, indent([line, op, ' ', right])]);
|
|
388
|
+
}
|
|
389
|
+
return group([left, ' ', op, ' ', right]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
case 'UnaryExpression': {
|
|
393
|
+
const argument = print('argument');
|
|
394
|
+
return [kw(node.operator, node.originalOperator), ' ', argument];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case 'ParenExpression': {
|
|
398
|
+
if (removeBrackets && canRemoveParens(path, node)) {
|
|
399
|
+
return print('expression');
|
|
400
|
+
}
|
|
401
|
+
return ['(', print('expression'), ')'];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
case 'RawStatement': {
|
|
405
|
+
return kw(node.value.toLowerCase(), node.keyword);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
case 'Raw': {
|
|
409
|
+
return node.value;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
case 'Empty': {
|
|
413
|
+
return '';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
default: {
|
|
417
|
+
return '';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export {
|
|
423
|
+
printAmpscriptNode,
|
|
424
|
+
collectVariableMap,
|
|
425
|
+
isNeedlessParenExpression,
|
|
426
|
+
canRemoveParens,
|
|
427
|
+
getOperatorPrecedence,
|
|
428
|
+
};
|