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/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prettier-plugin-sfmc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Prettier plugin for Salesforce Marketing Cloud — AMPscript formatting and SSJS file registration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"docs",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/JoernBerkefeld/prettier-plugin-sfmc.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/JoernBerkefeld/prettier-plugin-sfmc/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/JoernBerkefeld/prettier-plugin-sfmc#readme",
|
|
24
|
+
"author": "Joern Berkefeld",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"example": "prettier --plugin . example.ampscript",
|
|
27
|
+
"test": "node --experimental-vm-modules ../node_modules/jest/bin/jest.js"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"prettier",
|
|
31
|
+
"ampscript",
|
|
32
|
+
"ssjs",
|
|
33
|
+
"sfmc",
|
|
34
|
+
"salesforce",
|
|
35
|
+
"marketing-cloud",
|
|
36
|
+
"plugin",
|
|
37
|
+
"formatter"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"ampscript-data": "^0.1.1",
|
|
42
|
+
"ampscript-parser": "^0.1.0",
|
|
43
|
+
"ssjs-data": "^0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"jest": "^29.0.0",
|
|
47
|
+
"@jest/globals": "^29.0.0",
|
|
48
|
+
"prettier": "^3.7.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"prettier": ">=3.7.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prettier-plugin-sfmc
|
|
3
|
+
*
|
|
4
|
+
* A unified Prettier plugin for Salesforce Marketing Cloud.
|
|
5
|
+
* Handles AMPscript formatting (.ampscript, .amp, .html) and
|
|
6
|
+
* SSJS file registration (.ssjs).
|
|
7
|
+
*
|
|
8
|
+
* Exports: languages, parsers, printers, options, defaultOptions
|
|
9
|
+
* Following the Prettier v3 plugin API (ESM).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parse } from 'ampscript-parser';
|
|
13
|
+
import { printAmpscriptNode, collectVariableMap } from './printer.js';
|
|
14
|
+
import * as prettier from 'prettier';
|
|
15
|
+
|
|
16
|
+
// ── Languages ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export const languages = [
|
|
19
|
+
{
|
|
20
|
+
name: 'AMPscript',
|
|
21
|
+
parsers: ['ampscript-parse'],
|
|
22
|
+
extensions: ['.ampscript', '.amp', '.html'],
|
|
23
|
+
vscodeLanguageIds: ['ampscript'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'SSJS',
|
|
27
|
+
parsers: ['babel'],
|
|
28
|
+
extensions: ['.ssjs'],
|
|
29
|
+
vscodeLanguageIds: ['ssjs'],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ── Parsers ──────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const PRAGMA_RE = /^\s*\/\*\*?\s*@(?:format|prettier)\s*\*\//;
|
|
36
|
+
|
|
37
|
+
export const parsers = {
|
|
38
|
+
'ampscript-parse': {
|
|
39
|
+
parse(text) {
|
|
40
|
+
return parse(text);
|
|
41
|
+
},
|
|
42
|
+
astFormat: 'ampscript-ast',
|
|
43
|
+
locStart(node) {
|
|
44
|
+
return node.start;
|
|
45
|
+
},
|
|
46
|
+
locEnd(node) {
|
|
47
|
+
return node.end;
|
|
48
|
+
},
|
|
49
|
+
hasPragma(text) {
|
|
50
|
+
const blockOpen = text.indexOf('%%[');
|
|
51
|
+
if (blockOpen !== -1) {
|
|
52
|
+
const blockClose = text.indexOf(']%%', blockOpen);
|
|
53
|
+
if (blockClose !== -1) {
|
|
54
|
+
const blockContent = text.slice(blockOpen + 3, blockClose);
|
|
55
|
+
if (PRAGMA_RE.test(blockContent)) return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return PRAGMA_RE.test(text);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ── Printers ─────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export const printers = {
|
|
66
|
+
'ampscript-ast': {
|
|
67
|
+
print(path, options, print) {
|
|
68
|
+
const node = path.node;
|
|
69
|
+
|
|
70
|
+
if (node.type === 'Document' && !options.__ampscriptVariableMap) {
|
|
71
|
+
options.__ampscriptVariableMap = collectVariableMap(node);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return printAmpscriptNode(path, options, print);
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
embed(path, options) {
|
|
78
|
+
const node = path.node;
|
|
79
|
+
if (node.type !== 'Document') return;
|
|
80
|
+
|
|
81
|
+
const hasHtml = node.children.some(
|
|
82
|
+
(c) => c.type === 'Content' && /<[a-zA-Z!/]/.test(c.value),
|
|
83
|
+
);
|
|
84
|
+
if (!hasHtml) return;
|
|
85
|
+
|
|
86
|
+
return async (textToDocument, print) => {
|
|
87
|
+
if (!options.__ampscriptVariableMap) {
|
|
88
|
+
options.__ampscriptVariableMap = collectVariableMap(node);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const PH = 'AMPSCRIPTPH';
|
|
92
|
+
const htmlParts = [];
|
|
93
|
+
|
|
94
|
+
for (let index = 0; index < node.children.length; index++) {
|
|
95
|
+
const child = node.children[index];
|
|
96
|
+
if (child.type === 'Content') {
|
|
97
|
+
htmlParts.push(child.value);
|
|
98
|
+
} else {
|
|
99
|
+
htmlParts.push(`${PH}${index}END`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const html = htmlParts.join('');
|
|
104
|
+
|
|
105
|
+
let htmlDocument;
|
|
106
|
+
try {
|
|
107
|
+
htmlDocument = await textToDocument(html, { parser: 'html' });
|
|
108
|
+
} catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const re = new RegExp(String.raw`${PH}(\d+)END`, 'g');
|
|
113
|
+
return prettier.doc.utils.mapDoc(htmlDocument, (d) => {
|
|
114
|
+
if (typeof d !== 'string') return d;
|
|
115
|
+
if (!d.includes(PH)) return d;
|
|
116
|
+
|
|
117
|
+
const parts = [];
|
|
118
|
+
let last = 0;
|
|
119
|
+
let m;
|
|
120
|
+
re.lastIndex = 0;
|
|
121
|
+
while ((m = re.exec(d)) !== null) {
|
|
122
|
+
if (m.index > last) parts.push(d.slice(last, m.index));
|
|
123
|
+
parts.push(print(['children', Number.parseInt(m[1], 10)]));
|
|
124
|
+
last = m.index + m[0].length;
|
|
125
|
+
}
|
|
126
|
+
if (last < d.length) parts.push(d.slice(last));
|
|
127
|
+
return parts.length === 1 ? parts[0] : parts;
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
insertPragma(text) {
|
|
133
|
+
return '%%[ /** @format */ ]%%\n' + text;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
canAttachComment(node) {
|
|
137
|
+
return node.type && node.type !== 'Comment' && node.type !== 'Content';
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
isBlockComment(comment) {
|
|
141
|
+
return comment.type === 'Comment';
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
printComment(path) {
|
|
145
|
+
const comment = path.node;
|
|
146
|
+
if (comment.type === 'Comment') return comment.value;
|
|
147
|
+
return '';
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
hasPrettierIgnore(path) {
|
|
151
|
+
return path.node && path.node.prettierIgnore === true;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
getVisitorKeys(node) {
|
|
155
|
+
switch (node.type) {
|
|
156
|
+
case 'Document': {
|
|
157
|
+
return ['children'];
|
|
158
|
+
}
|
|
159
|
+
case 'Block': {
|
|
160
|
+
return ['statements'];
|
|
161
|
+
}
|
|
162
|
+
case 'InlineExpression': {
|
|
163
|
+
return ['expression'];
|
|
164
|
+
}
|
|
165
|
+
case 'IfStatement': {
|
|
166
|
+
return ['condition', 'consequent', 'alternates'];
|
|
167
|
+
}
|
|
168
|
+
case 'ElseIfClause': {
|
|
169
|
+
return ['condition', 'body'];
|
|
170
|
+
}
|
|
171
|
+
case 'ElseClause': {
|
|
172
|
+
return ['body'];
|
|
173
|
+
}
|
|
174
|
+
case 'ForStatement': {
|
|
175
|
+
return ['counter', 'startExpr', 'endExpr', 'body'];
|
|
176
|
+
}
|
|
177
|
+
case 'SetStatement': {
|
|
178
|
+
return ['target', 'value'];
|
|
179
|
+
}
|
|
180
|
+
case 'VarDeclaration': {
|
|
181
|
+
return ['variables'];
|
|
182
|
+
}
|
|
183
|
+
case 'ExpressionStatement': {
|
|
184
|
+
return ['expression'];
|
|
185
|
+
}
|
|
186
|
+
case 'FunctionCall': {
|
|
187
|
+
return ['arguments'];
|
|
188
|
+
}
|
|
189
|
+
case 'BinaryExpression': {
|
|
190
|
+
return ['left', 'right'];
|
|
191
|
+
}
|
|
192
|
+
case 'UnaryExpression': {
|
|
193
|
+
return ['argument'];
|
|
194
|
+
}
|
|
195
|
+
case 'ParenExpression': {
|
|
196
|
+
return ['expression'];
|
|
197
|
+
}
|
|
198
|
+
default: {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ── Options ──────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
export const options = {
|
|
209
|
+
ampscriptSpacing: {
|
|
210
|
+
type: 'boolean',
|
|
211
|
+
category: 'AMPscript',
|
|
212
|
+
default: true,
|
|
213
|
+
description:
|
|
214
|
+
'Enforce consistent spacing around operators and inside inline expressions. ' +
|
|
215
|
+
'When true, prints `%%= v(@name) =%%` instead of `%%=v(@name)=%%`.',
|
|
216
|
+
},
|
|
217
|
+
ampscriptEnforceVariableCasing: {
|
|
218
|
+
type: 'boolean',
|
|
219
|
+
category: 'AMPscript',
|
|
220
|
+
default: true,
|
|
221
|
+
description:
|
|
222
|
+
'Enforce consistent casing for AMPscript variables. ' +
|
|
223
|
+
'When true, all occurrences of a variable will use the casing from ' +
|
|
224
|
+
'its first appearance in the file.',
|
|
225
|
+
},
|
|
226
|
+
ampscriptRemoveUnnecessaryBrackets: {
|
|
227
|
+
type: 'boolean',
|
|
228
|
+
category: 'AMPscript',
|
|
229
|
+
default: true,
|
|
230
|
+
description:
|
|
231
|
+
'Automatically remove parentheses where they are not needed. ' +
|
|
232
|
+
'For example, `(@name)` becomes `@name` when the parens add no grouping value. ' +
|
|
233
|
+
'When false, unnecessary brackets are preserved as-is.',
|
|
234
|
+
},
|
|
235
|
+
ampscriptQuoteStyle: {
|
|
236
|
+
type: 'choice',
|
|
237
|
+
category: 'AMPscript',
|
|
238
|
+
default: 'single',
|
|
239
|
+
choices: [
|
|
240
|
+
{
|
|
241
|
+
value: 'double',
|
|
242
|
+
description: 'Use double quotes for strings: "value"',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
value: 'single',
|
|
246
|
+
description: "Use single quotes for strings: 'value'",
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
description: 'Select whether strings should be wrapped in single or double quotes.',
|
|
250
|
+
},
|
|
251
|
+
ampscriptKeywordCase: {
|
|
252
|
+
type: 'choice',
|
|
253
|
+
category: 'AMPscript',
|
|
254
|
+
default: 'lower',
|
|
255
|
+
choices: [
|
|
256
|
+
{ value: 'lower', description: 'Lowercase keywords: if, set, var, for' },
|
|
257
|
+
{ value: 'upper', description: 'Uppercase keywords: IF, SET, VAR, FOR' },
|
|
258
|
+
{ value: 'preserve', description: 'Keep original keyword casing' },
|
|
259
|
+
],
|
|
260
|
+
description: 'Control the casing of AMPscript keywords (if, else, set, var, for, etc.).',
|
|
261
|
+
},
|
|
262
|
+
ampscriptFunctionCase: {
|
|
263
|
+
type: 'choice',
|
|
264
|
+
category: 'AMPscript',
|
|
265
|
+
default: 'upper-camel',
|
|
266
|
+
choices: [
|
|
267
|
+
{
|
|
268
|
+
value: 'upper-camel',
|
|
269
|
+
description: 'Canonical PascalCase: Lookup, ContentBlockByKey',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
value: 'lower-camel',
|
|
273
|
+
description: 'camelCase: lookup, contentBlockByKey',
|
|
274
|
+
},
|
|
275
|
+
{ value: 'upper', description: 'UPPERCASE: LOOKUP, CONTENTBLOCKBYKEY' },
|
|
276
|
+
{ value: 'lower', description: 'lowercase: lookup, contentblockbykey' },
|
|
277
|
+
{ value: 'preserve', description: 'Keep original function casing' },
|
|
278
|
+
],
|
|
279
|
+
description:
|
|
280
|
+
'Control the casing of AMPscript function names. ' +
|
|
281
|
+
'"upper-camel" uses the canonical PascalCase form of each function name.',
|
|
282
|
+
},
|
|
283
|
+
ampscriptBlockLineBreaks: {
|
|
284
|
+
type: 'boolean',
|
|
285
|
+
category: 'AMPscript',
|
|
286
|
+
default: true,
|
|
287
|
+
description:
|
|
288
|
+
'Enforce line breaks before and after %%[ ... ]%% blocks. ' +
|
|
289
|
+
'Inline expressions (%%= ... =%%) are never affected. ' +
|
|
290
|
+
'Set to false when blocks are embedded in contexts where extra line breaks are unwanted.',
|
|
291
|
+
},
|
|
292
|
+
ampscriptVarDeclarationStyle: {
|
|
293
|
+
type: 'choice',
|
|
294
|
+
category: 'AMPscript',
|
|
295
|
+
default: 'auto',
|
|
296
|
+
choices: [
|
|
297
|
+
{
|
|
298
|
+
value: 'auto',
|
|
299
|
+
description:
|
|
300
|
+
"Let Prettier decide based on printWidth: single line when it fits, one-per-line when it doesn't.",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
value: 'single-line',
|
|
304
|
+
description: 'Always place all variables on one line: var @a, @b, @c',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
value: 'multi-line',
|
|
308
|
+
description: 'Always place each variable on its own line.',
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
description: 'Control how var declarations with multiple variables are formatted.',
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// ── Default Options ──────────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
export const defaultOptions = {
|
|
318
|
+
tabWidth: 2,
|
|
319
|
+
useTabs: false,
|
|
320
|
+
};
|