prettier-plugin-sfmc 0.1.0 → 0.2.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/README.md +114 -21
- package/docs/options/ampscript-function-case.md +24 -0
- package/docs/options/ampscript-var-declaration-style.md +25 -26
- package/package.json +23 -8
- package/src/index.js +71 -15
- package/src/printer.js +111 -35
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# prettier-plugin-sfmc
|
|
2
2
|
|
|
3
|
-
Unified Prettier plugin for **Salesforce Marketing Cloud** — formats **AMPscript** (`.ampscript`, `.amp`, `.html`)
|
|
3
|
+
Unified Prettier plugin for **Salesforce Marketing Cloud** — formats **AMPscript** (`.ampscript`, `.amp`, `.html`), registers **SSJS** (`.ssjs`) with Prettier’s JavaScript formatter, and formats **SQL** (`.sql`) via embedded [prettier-plugin-sql](https://www.npmjs.com/package/prettier-plugin-sql).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ Requires Prettier 3.7+.
|
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
|
-
Prettier auto-discovers plugins installed in `node_modules`. No config needed for `.ampscript`, `.amp`, and `.
|
|
15
|
+
Prettier auto-discovers plugins installed in `node_modules`. No config needed for `.ampscript`, `.amp`, `.ssjs`, and `.sql` files.
|
|
16
16
|
|
|
17
17
|
For `.html` files containing AMPscript, the plugin's parser handles AMPscript regions while delegating HTML to Prettier's built-in HTML formatter. SSJS `<script runat="server">` blocks are formatted as JavaScript by Prettier's HTML pipeline automatically.
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ To use explicitly in `.prettierrc`:
|
|
|
20
20
|
|
|
21
21
|
```json
|
|
22
22
|
{
|
|
23
|
-
|
|
23
|
+
"plugins": ["prettier-plugin-sfmc"]
|
|
24
24
|
}
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -28,25 +28,25 @@ To use explicitly in `.prettierrc`:
|
|
|
28
28
|
|
|
29
29
|
All options use the `ampscript` prefix — they control AMPscript formatting behavior.
|
|
30
30
|
|
|
31
|
-
| Option
|
|
32
|
-
|
|
|
33
|
-
| [`ampscriptSpacing`](docs/options/ampscript-spacing.md)
|
|
34
|
-
| [`ampscriptEnforceVariableCasing`](docs/options/ampscript-enforce-variable-casing.md)
|
|
31
|
+
| Option | Type | Default | Description |
|
|
32
|
+
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------- | ---------------------------------------------- |
|
|
33
|
+
| [`ampscriptSpacing`](docs/options/ampscript-spacing.md) | boolean | `true` | Spacing in inline expressions: `%%= V(@x) =%%` |
|
|
34
|
+
| [`ampscriptEnforceVariableCasing`](docs/options/ampscript-enforce-variable-casing.md) | boolean | `true` | Normalize variable casing to first occurrence |
|
|
35
35
|
| [`ampscriptRemoveUnnecessaryBrackets`](docs/options/ampscript-remove-unnecessary-brackets.md) | boolean | `true` | Remove needless parentheses |
|
|
36
|
-
| [`ampscriptQuoteStyle`](docs/options/ampscript-quote-style.md)
|
|
37
|
-
| [`ampscriptKeywordCase`](docs/options/ampscript-keyword-case.md)
|
|
38
|
-
| [`ampscriptFunctionCase`](docs/options/ampscript-function-case.md)
|
|
39
|
-
| [`ampscriptBlockLineBreaks`](docs/options/ampscript-block-line-breaks.md)
|
|
40
|
-
| [`ampscriptVarDeclarationStyle`](docs/options/ampscript-var-declaration-style.md)
|
|
36
|
+
| [`ampscriptQuoteStyle`](docs/options/ampscript-quote-style.md) | `"single"` \| `"double"` | `"single"` | String quote style |
|
|
37
|
+
| [`ampscriptKeywordCase`](docs/options/ampscript-keyword-case.md) | `"lower"` \| `"upper"` \| `"preserve"` | `"lower"` | Keyword casing |
|
|
38
|
+
| [`ampscriptFunctionCase`](docs/options/ampscript-function-case.md) | `"upper-camel"` \| `"lower-camel"` \| `"upper"` \| `"lower"` \| `"preserve"` | `"upper-camel"` | Function name casing |
|
|
39
|
+
| [`ampscriptBlockLineBreaks`](docs/options/ampscript-block-line-breaks.md) | boolean | `true` | Line breaks before/after `%%[ ]%%` blocks |
|
|
40
|
+
| [`ampscriptVarDeclarationStyle`](docs/options/ampscript-var-declaration-style.md) | `"auto"` \| `"single-line"` \| `"multi-line"` | `"multi-line"` | Var declaration formatting |
|
|
41
41
|
|
|
42
42
|
### Example `.prettierrc`
|
|
43
43
|
|
|
44
44
|
```json
|
|
45
45
|
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
"plugins": ["prettier-plugin-sfmc"],
|
|
47
|
+
"ampscriptKeywordCase": "upper",
|
|
48
|
+
"ampscriptFunctionCase": "upper-camel",
|
|
49
|
+
"ampscriptQuoteStyle": "single"
|
|
50
50
|
}
|
|
51
51
|
```
|
|
52
52
|
|
|
@@ -58,6 +58,45 @@ All options use the `ampscript` prefix — they control AMPscript formatting beh
|
|
|
58
58
|
| `.amp` | `ampscript-parse` | Full AMPscript formatting |
|
|
59
59
|
| `.html` | `ampscript-parse` | AMPscript formatted; HTML delegated to Prettier |
|
|
60
60
|
| `.ssjs` | `babel` (built-in) | Standard JavaScript formatting |
|
|
61
|
+
| `.sql` | `sql` | SQL via composed `prettier-plugin-sql` |
|
|
62
|
+
|
|
63
|
+
## Core Prettier defaults (AMPscript, HTML, SQL)
|
|
64
|
+
|
|
65
|
+
This plugin exports [Prettier `defaultOptions`](https://prettier.io/docs/plugins#defaultoptions). Prettier merges them when **this plugin’s printer** is active (AMPscript / embedded AMPscript in HTML, and SQL via the composed `sql` printer). They affect layout choices such as `tabWidth` and `printWidth` for those formats.
|
|
66
|
+
|
|
67
|
+
| Option | Default | Rationale |
|
|
68
|
+
| ------ | ------- | --------- |
|
|
69
|
+
| `useTabs` | `false` | SFMC often normalizes tabs away on save; spaces keep layout stable. |
|
|
70
|
+
| `tabWidth` | `4` | Readable indentation (override in config if you prefer 2). |
|
|
71
|
+
| `printWidth` | `100` | Fits typical editor panes better than Prettier’s 80. |
|
|
72
|
+
| `singleQuote` | `true` | Matches common JS style and `ampscriptQuoteStyle: 'single'` for AMPscript-related output where the core quote option applies. |
|
|
73
|
+
| `trailingComma` | `'none'` | Avoids trailing commas in stacks where they can be problematic (notably SSJS — see below). |
|
|
74
|
+
|
|
75
|
+
String delimiters inside AMPscript blocks still follow `ampscriptQuoteStyle`. See [Prettier options](https://prettier.io/docs/options) for every standard flag.
|
|
76
|
+
|
|
77
|
+
### SSJS (`.ssjs`)
|
|
78
|
+
|
|
79
|
+
`.ssjs` files use Prettier’s built-in **JavaScript** printer, not this plugin’s printer, so the `defaultOptions` above are **not** applied automatically to SSJS. Use `.prettierrc` (or [overrides](https://prettier.io/docs/configuration#configuration-overrides) on `*.ssjs`) if you want the same style, for example:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"plugins": ["prettier-plugin-sfmc"],
|
|
84
|
+
"singleQuote": true,
|
|
85
|
+
"trailingComma": "none",
|
|
86
|
+
"tabWidth": 4,
|
|
87
|
+
"printWidth": 100
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
To keep **AMPscript-only** indentation at 2 spaces while using different values for SSJS, use `overrides` keyed by file pattern.
|
|
92
|
+
|
|
93
|
+
## SQL (Transact-SQL / SFMC)
|
|
94
|
+
|
|
95
|
+
`.sql` formatting is provided by [prettier-plugin-sql](https://github.com/un-ts/prettier/tree/master/packages/sql) (npm: [prettier-plugin-sql](https://www.npmjs.com/package/prettier-plugin-sql)). You only need `prettier` and `prettier-plugin-sfmc`; do not add a second entry in `plugins` for SQL.
|
|
96
|
+
|
|
97
|
+
**Defaults chosen for SFMC-style T-SQL:** `language` `tsql`, `formatter` `sql-formatter`, `keywordCase` / `functionCase` `upper`, `identifierCase` / `dataTypeCase` `preserve`. Override any of these in `.prettierrc` or under `overrides` with `files: "*.sql"` if needed.
|
|
98
|
+
|
|
99
|
+
Core layout still follows [Prettier options](https://prettier.io/docs/options). SQL-specific knobs (`expressionWidth`, `linesBetweenQueries`, etc.) are documented in the upstream package README.
|
|
61
100
|
|
|
62
101
|
## HTML Embedding
|
|
63
102
|
|
|
@@ -69,13 +108,67 @@ When formatting `.html` files, the plugin:
|
|
|
69
108
|
|
|
70
109
|
This means a single plugin handles all SFMC formatting in HTML email templates.
|
|
71
110
|
|
|
72
|
-
##
|
|
111
|
+
## Ignoring Code
|
|
112
|
+
|
|
113
|
+
Prettier provides two ways to exclude code from formatting: ignore comments for specific code sections, and `.prettierignore` for entire files.
|
|
114
|
+
|
|
115
|
+
### SSJS
|
|
73
116
|
|
|
74
|
-
|
|
117
|
+
SSJS uses Prettier's built-in JavaScript formatter. Use `// prettier-ignore` to exclude the next statement from formatting:
|
|
75
118
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
```js
|
|
120
|
+
// prettier-ignore
|
|
121
|
+
var config = {
|
|
122
|
+
clientId: "abc123",
|
|
123
|
+
clientSecret: "xyz789",
|
|
124
|
+
endpoint: "https://example.com"
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Without the comment, Prettier would collapse the aligned spacing.
|
|
129
|
+
|
|
130
|
+
### AMPscript
|
|
131
|
+
|
|
132
|
+
AMPscript uses block comment syntax. Use `/* prettier-ignore */` to exclude the next statement from formatting:
|
|
133
|
+
|
|
134
|
+
```ampscript
|
|
135
|
+
%%[
|
|
136
|
+
/* prettier-ignore */
|
|
137
|
+
set @matrix = Concat(
|
|
138
|
+
'1, 0, 0,',
|
|
139
|
+
'0, 1, 0,',
|
|
140
|
+
'0, 0, 1'
|
|
141
|
+
)
|
|
142
|
+
]%%
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Ignoring Files: .prettierignore
|
|
146
|
+
|
|
147
|
+
To exclude entire files from formatting, create a `.prettierignore` file in the root of your project. It uses [gitignore syntax](https://git-scm.com/docs/gitignore#_pattern_format).
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
# Ignore artifacts:
|
|
153
|
+
build
|
|
154
|
+
coverage
|
|
155
|
+
|
|
156
|
+
# Ignore all HTML files:
|
|
157
|
+
**/*.html
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
By default Prettier ignores files in version control directories (`.git`, `.svn`, `.hg`) and `node_modules`. Prettier also follows rules in `.gitignore` if present.
|
|
161
|
+
|
|
162
|
+
#### SMS and Mobile Messages
|
|
163
|
+
|
|
164
|
+
For SMS or MobilePush messages, line breaks and exact character placement often matter for delivery and display. Excluding these files from formatting prevents Prettier from altering whitespace that affects message rendering.
|
|
165
|
+
|
|
166
|
+
Example pattern for SFMC DevTools mobile message assets:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
# Preserve exact formatting in mobile messages
|
|
170
|
+
**/*.asset-mobile-meta.amp
|
|
171
|
+
```
|
|
79
172
|
|
|
80
173
|
## License
|
|
81
174
|
|
|
@@ -44,6 +44,30 @@ Normalises the casing of every known AMPscript function name. AMPscript function
|
|
|
44
44
|
]%%
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
### `"lower-camel"`
|
|
48
|
+
|
|
49
|
+
Uses the same canonical name as `"upper-camel"`, but with the first character lowercased (camelCase).
|
|
50
|
+
|
|
51
|
+
**Input:**
|
|
52
|
+
|
|
53
|
+
```ampscript
|
|
54
|
+
%%[
|
|
55
|
+
var @result
|
|
56
|
+
set @result = LOOKUP("MyDE", "Value", "Key", @key)
|
|
57
|
+
set @html = contentblockbykey("my-block")
|
|
58
|
+
]%%
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Output:**
|
|
62
|
+
|
|
63
|
+
```ampscript
|
|
64
|
+
%%[
|
|
65
|
+
var @result
|
|
66
|
+
set @result = lookup("MyDE", "Value", "Key", @key)
|
|
67
|
+
set @html = contentBlockByKey("my-block")
|
|
68
|
+
]%%
|
|
69
|
+
```
|
|
70
|
+
|
|
47
71
|
### `"upper"`
|
|
48
72
|
|
|
49
73
|
**Output:**
|
|
@@ -5,22 +5,22 @@
|
|
|
5
5
|
| | |
|
|
6
6
|
|---|---|
|
|
7
7
|
| **Type** | `"auto"` \| `"single-line"` \| `"multi-line"` |
|
|
8
|
-
| **Default** | `"
|
|
8
|
+
| **Default** | `"multi-line"` |
|
|
9
9
|
| **Applied by** | `prettier --write` · VS Code format-on-save (requires the [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)) |
|
|
10
10
|
|
|
11
11
|
## What It Controls
|
|
12
12
|
|
|
13
|
-
Determines whether multiple variables in a single `var` statement
|
|
13
|
+
Determines whether multiple variables in a single `var` statement stay on one line, break after each comma (still a single `var` keyword), or follow Prettier’s `printWidth` heuristic. AMPscript allows `var @a, @b, @c` on one line; this option does not split into separate `var` statements.
|
|
14
14
|
|
|
15
15
|
## Settings
|
|
16
16
|
|
|
17
17
|
| Value | Effect |
|
|
18
18
|
|-------|--------|
|
|
19
|
-
| `"
|
|
19
|
+
| `"multi-line"` (default) | One `var` keyword; variables separated by comma and line breaks (indented under `var`) |
|
|
20
|
+
| `"auto"` | Single line when it fits within `printWidth`; otherwise same comma-and-line-break layout as `"multi-line"` |
|
|
20
21
|
| `"single-line"` | Always `var @a, @b, @c` on one line regardless of width |
|
|
21
|
-
| `"multi-line"` | Always one variable per `var` statement |
|
|
22
22
|
|
|
23
|
-
### `"
|
|
23
|
+
### `"multi-line"` (default)
|
|
24
24
|
|
|
25
25
|
**Input:**
|
|
26
26
|
|
|
@@ -30,49 +30,48 @@ Determines whether multiple variables in a single `var` statement are kept on on
|
|
|
30
30
|
]%%
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
**Output (
|
|
33
|
+
**Output** (indentation follows your Prettier `tabWidth`; shown with the plugin default of 4):
|
|
34
34
|
|
|
35
35
|
```ampscript
|
|
36
36
|
%%[
|
|
37
|
-
|
|
37
|
+
var @firstName,
|
|
38
|
+
@lastName,
|
|
39
|
+
@email,
|
|
40
|
+
@address,
|
|
41
|
+
@city,
|
|
42
|
+
@postalCode
|
|
38
43
|
]%%
|
|
39
44
|
```
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
### `"auto"`
|
|
47
|
+
|
|
48
|
+
**Input:**
|
|
42
49
|
|
|
43
50
|
```ampscript
|
|
44
51
|
%%[
|
|
45
|
-
var @firstName
|
|
46
|
-
var @lastName
|
|
47
|
-
var @email
|
|
48
|
-
var @address
|
|
49
|
-
var @city
|
|
50
|
-
var @postalCode
|
|
52
|
+
var @firstName, @lastName, @email
|
|
51
53
|
]%%
|
|
52
54
|
```
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
**Output:**
|
|
56
|
+
**Output (when the line fits within `printWidth`):**
|
|
57
57
|
|
|
58
58
|
```ampscript
|
|
59
59
|
%%[
|
|
60
|
-
|
|
60
|
+
var @firstName, @lastName, @email
|
|
61
61
|
]%%
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
**Output (when the line exceeds `printWidth`):**
|
|
65
|
+
|
|
66
|
+
Same wrapped shape as `"multi-line"`: one `var`, commas at end of lines, continuation lines indented.
|
|
67
|
+
|
|
68
|
+
### `"single-line"`
|
|
65
69
|
|
|
66
70
|
**Output:**
|
|
67
71
|
|
|
68
72
|
```ampscript
|
|
69
73
|
%%[
|
|
70
|
-
|
|
71
|
-
var @lastName
|
|
72
|
-
var @email
|
|
73
|
-
var @address
|
|
74
|
-
var @city
|
|
75
|
-
var @postalCode
|
|
74
|
+
var @firstName, @lastName, @email, @address, @city, @postalCode
|
|
76
75
|
]%%
|
|
77
76
|
```
|
|
78
77
|
|
|
@@ -81,6 +80,6 @@ Determines whether multiple variables in a single `var` statement are kept on on
|
|
|
81
80
|
```json
|
|
82
81
|
{
|
|
83
82
|
"plugins": ["prettier-plugin-sfmc"],
|
|
84
|
-
"ampscriptVarDeclarationStyle": "
|
|
83
|
+
"ampscriptVarDeclarationStyle": "auto"
|
|
85
84
|
}
|
|
86
85
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prettier-plugin-sfmc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Prettier plugin for Salesforce Marketing Cloud — AMPscript
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Prettier plugin for Salesforce Marketing Cloud — AMPscript, SSJS, and SQL formatting",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -24,12 +24,19 @@
|
|
|
24
24
|
"author": "Joern Berkefeld",
|
|
25
25
|
"scripts": {
|
|
26
26
|
"example": "prettier --plugin . example.ampscript",
|
|
27
|
-
"test": "node
|
|
27
|
+
"test": "node ./scripts/run-jest.mjs",
|
|
28
|
+
"lint": "eslint .",
|
|
29
|
+
"lint:fix": "eslint --fix .",
|
|
30
|
+
"format": "prettier --write .",
|
|
31
|
+
"format:check": "prettier --check ."
|
|
28
32
|
},
|
|
29
33
|
"keywords": [
|
|
30
34
|
"prettier",
|
|
31
35
|
"ampscript",
|
|
32
36
|
"ssjs",
|
|
37
|
+
"sql",
|
|
38
|
+
"tsql",
|
|
39
|
+
"transact-sql",
|
|
33
40
|
"sfmc",
|
|
34
41
|
"salesforce",
|
|
35
42
|
"marketing-cloud",
|
|
@@ -38,14 +45,22 @@
|
|
|
38
45
|
],
|
|
39
46
|
"license": "MIT",
|
|
40
47
|
"dependencies": {
|
|
41
|
-
"ampscript-data": "^0.1.
|
|
42
|
-
"ampscript-parser": "^0.1.
|
|
43
|
-
"
|
|
48
|
+
"ampscript-data": "^0.1.3",
|
|
49
|
+
"ampscript-parser": "^0.1.1",
|
|
50
|
+
"prettier-plugin-sql": "^0.20.0",
|
|
51
|
+
"ssjs-data": "^0.2.2"
|
|
44
52
|
},
|
|
45
53
|
"devDependencies": {
|
|
46
|
-
"
|
|
54
|
+
"@eslint/js": "^10.0.1",
|
|
47
55
|
"@jest/globals": "^29.0.0",
|
|
48
|
-
"
|
|
56
|
+
"eslint": "^10.1.0",
|
|
57
|
+
"eslint-config-prettier": "^10.1.8",
|
|
58
|
+
"eslint-plugin-jsdoc": "^62.0.0",
|
|
59
|
+
"eslint-plugin-prettier": "^5.5.0",
|
|
60
|
+
"eslint-plugin-unicorn": "^64.0.0",
|
|
61
|
+
"globals": "^17.4.0",
|
|
62
|
+
"jest": "^29.0.0",
|
|
63
|
+
"prettier": "^3.8.1"
|
|
49
64
|
},
|
|
50
65
|
"peerDependencies": {
|
|
51
66
|
"prettier": ">=3.7.0"
|
package/src/index.js
CHANGED
|
@@ -2,17 +2,45 @@
|
|
|
2
2
|
* prettier-plugin-sfmc
|
|
3
3
|
*
|
|
4
4
|
* A unified Prettier plugin for Salesforce Marketing Cloud.
|
|
5
|
-
* Handles AMPscript formatting (.ampscript, .amp, .html)
|
|
6
|
-
*
|
|
5
|
+
* Handles AMPscript formatting (.ampscript, .amp, .html), SSJS file
|
|
6
|
+
* registration (.ssjs), and SQL (.sql) via composed prettier-plugin-sql.
|
|
7
7
|
*
|
|
8
8
|
* Exports: languages, parsers, printers, options, defaultOptions
|
|
9
9
|
* Following the Prettier v3 plugin API (ESM).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { parse } from 'ampscript-parser';
|
|
13
|
+
import sqlPlugin from 'prettier-plugin-sql';
|
|
13
14
|
import { printAmpscriptNode, collectVariableMap } from './printer.js';
|
|
14
15
|
import * as prettier from 'prettier';
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Clone sql plugin option descriptors and set SFMC-friendly defaults.
|
|
19
|
+
*
|
|
20
|
+
* @param {Record<string, object>} sqlOptions
|
|
21
|
+
* @returns {Record<string, object>} Option map with adjusted defaults.
|
|
22
|
+
*/
|
|
23
|
+
function mergeSqlOptionsWithSfmcDefaults(sqlOptions) {
|
|
24
|
+
/** @type {Record<string, object>} */
|
|
25
|
+
const merged = {};
|
|
26
|
+
for (const [key, descriptor] of Object.entries(sqlOptions)) {
|
|
27
|
+
merged[key] = { ...descriptor };
|
|
28
|
+
}
|
|
29
|
+
const defaults = {
|
|
30
|
+
language: 'tsql',
|
|
31
|
+
keywordCase: 'upper',
|
|
32
|
+
functionCase: 'upper',
|
|
33
|
+
identifierCase: 'preserve',
|
|
34
|
+
dataTypeCase: 'preserve',
|
|
35
|
+
};
|
|
36
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
37
|
+
if (merged[key]) {
|
|
38
|
+
merged[key].default = defaultValue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return merged;
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
// ── Languages ────────────────────────────────────────────────────────────────
|
|
17
45
|
|
|
18
46
|
export const languages = [
|
|
@@ -28,6 +56,7 @@ export const languages = [
|
|
|
28
56
|
extensions: ['.ssjs'],
|
|
29
57
|
vscodeLanguageIds: ['ssjs'],
|
|
30
58
|
},
|
|
59
|
+
...sqlPlugin.languages,
|
|
31
60
|
];
|
|
32
61
|
|
|
33
62
|
// ── Parsers ──────────────────────────────────────────────────────────────────
|
|
@@ -35,6 +64,7 @@ export const languages = [
|
|
|
35
64
|
const PRAGMA_RE = /^\s*\/\*\*?\s*@(?:format|prettier)\s*\*\//;
|
|
36
65
|
|
|
37
66
|
export const parsers = {
|
|
67
|
+
...sqlPlugin.parsers,
|
|
38
68
|
'ampscript-parse': {
|
|
39
69
|
parse(text) {
|
|
40
70
|
return parse(text);
|
|
@@ -52,7 +82,9 @@ export const parsers = {
|
|
|
52
82
|
const blockClose = text.indexOf(']%%', blockOpen);
|
|
53
83
|
if (blockClose !== -1) {
|
|
54
84
|
const blockContent = text.slice(blockOpen + 3, blockClose);
|
|
55
|
-
if (PRAGMA_RE.test(blockContent))
|
|
85
|
+
if (PRAGMA_RE.test(blockContent)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
56
88
|
}
|
|
57
89
|
}
|
|
58
90
|
return PRAGMA_RE.test(text);
|
|
@@ -63,6 +95,7 @@ export const parsers = {
|
|
|
63
95
|
// ── Printers ─────────────────────────────────────────────────────────────────
|
|
64
96
|
|
|
65
97
|
export const printers = {
|
|
98
|
+
...sqlPlugin.printers,
|
|
66
99
|
'ampscript-ast': {
|
|
67
100
|
print(path, options, print) {
|
|
68
101
|
const node = path.node;
|
|
@@ -76,12 +109,16 @@ export const printers = {
|
|
|
76
109
|
|
|
77
110
|
embed(path, options) {
|
|
78
111
|
const node = path.node;
|
|
79
|
-
if (node.type !== 'Document')
|
|
112
|
+
if (node.type !== 'Document') {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
80
115
|
|
|
81
116
|
const hasHtml = node.children.some(
|
|
82
117
|
(c) => c.type === 'Content' && /<[a-zA-Z!/]/.test(c.value),
|
|
83
118
|
);
|
|
84
|
-
if (!hasHtml)
|
|
119
|
+
if (!hasHtml) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
85
122
|
|
|
86
123
|
return async (textToDocument, print) => {
|
|
87
124
|
if (!options.__ampscriptVariableMap) {
|
|
@@ -111,19 +148,27 @@ export const printers = {
|
|
|
111
148
|
|
|
112
149
|
const re = new RegExp(String.raw`${PH}(\d+)END`, 'g');
|
|
113
150
|
return prettier.doc.utils.mapDoc(htmlDocument, (d) => {
|
|
114
|
-
if (typeof d !== 'string')
|
|
115
|
-
|
|
151
|
+
if (typeof d !== 'string') {
|
|
152
|
+
return d;
|
|
153
|
+
}
|
|
154
|
+
if (!d.includes(PH)) {
|
|
155
|
+
return d;
|
|
156
|
+
}
|
|
116
157
|
|
|
117
158
|
const parts = [];
|
|
118
159
|
let last = 0;
|
|
119
160
|
let m;
|
|
120
161
|
re.lastIndex = 0;
|
|
121
162
|
while ((m = re.exec(d)) !== null) {
|
|
122
|
-
if (m.index > last)
|
|
163
|
+
if (m.index > last) {
|
|
164
|
+
parts.push(d.slice(last, m.index));
|
|
165
|
+
}
|
|
123
166
|
parts.push(print(['children', Number.parseInt(m[1], 10)]));
|
|
124
167
|
last = m.index + m[0].length;
|
|
125
168
|
}
|
|
126
|
-
if (last < d.length)
|
|
169
|
+
if (last < d.length) {
|
|
170
|
+
parts.push(d.slice(last));
|
|
171
|
+
}
|
|
127
172
|
return parts.length === 1 ? parts[0] : parts;
|
|
128
173
|
});
|
|
129
174
|
};
|
|
@@ -143,7 +188,9 @@ export const printers = {
|
|
|
143
188
|
|
|
144
189
|
printComment(path) {
|
|
145
190
|
const comment = path.node;
|
|
146
|
-
if (comment.type === 'Comment')
|
|
191
|
+
if (comment.type === 'Comment') {
|
|
192
|
+
return comment.value;
|
|
193
|
+
}
|
|
147
194
|
return '';
|
|
148
195
|
},
|
|
149
196
|
|
|
@@ -205,7 +252,7 @@ export const printers = {
|
|
|
205
252
|
|
|
206
253
|
// ── Options ──────────────────────────────────────────────────────────────────
|
|
207
254
|
|
|
208
|
-
|
|
255
|
+
const sfmcOptions = {
|
|
209
256
|
ampscriptSpacing: {
|
|
210
257
|
type: 'boolean',
|
|
211
258
|
category: 'AMPscript',
|
|
@@ -292,12 +339,12 @@ export const options = {
|
|
|
292
339
|
ampscriptVarDeclarationStyle: {
|
|
293
340
|
type: 'choice',
|
|
294
341
|
category: 'AMPscript',
|
|
295
|
-
default: '
|
|
342
|
+
default: 'multi-line',
|
|
296
343
|
choices: [
|
|
297
344
|
{
|
|
298
345
|
value: 'auto',
|
|
299
346
|
description:
|
|
300
|
-
|
|
347
|
+
'Let Prettier decide based on printWidth: single line when it fits, otherwise break after each comma.',
|
|
301
348
|
},
|
|
302
349
|
{
|
|
303
350
|
value: 'single-line',
|
|
@@ -305,16 +352,25 @@ export const options = {
|
|
|
305
352
|
},
|
|
306
353
|
{
|
|
307
354
|
value: 'multi-line',
|
|
308
|
-
description:
|
|
355
|
+
description:
|
|
356
|
+
'Always break after each comma: one `var` keyword, variables listed on separate lines.',
|
|
309
357
|
},
|
|
310
358
|
],
|
|
311
359
|
description: 'Control how var declarations with multiple variables are formatted.',
|
|
312
360
|
},
|
|
313
361
|
};
|
|
314
362
|
|
|
363
|
+
export const options = {
|
|
364
|
+
...mergeSqlOptionsWithSfmcDefaults(sqlPlugin.options),
|
|
365
|
+
...sfmcOptions,
|
|
366
|
+
};
|
|
367
|
+
|
|
315
368
|
// ── Default Options ──────────────────────────────────────────────────────────
|
|
316
369
|
|
|
317
370
|
export const defaultOptions = {
|
|
318
|
-
tabWidth: 2,
|
|
319
371
|
useTabs: false,
|
|
372
|
+
tabWidth: 4,
|
|
373
|
+
printWidth: 100,
|
|
374
|
+
singleQuote: true,
|
|
375
|
+
trailingComma: 'none',
|
|
320
376
|
};
|
package/src/printer.js
CHANGED
|
@@ -13,12 +13,16 @@ const { group, indent, join, line, softline, hardline } = prettier.doc.builders;
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Collect all variable names in first-occurrence order for consistent casing.
|
|
16
|
+
*
|
|
17
|
+
* @param node
|
|
16
18
|
*/
|
|
17
19
|
function collectVariableMap(node) {
|
|
18
20
|
const map = new Map(); // lowercase -> first-seen casing
|
|
19
21
|
|
|
20
22
|
function walk(n) {
|
|
21
|
-
if (!n || typeof n !== 'object')
|
|
23
|
+
if (!n || typeof n !== 'object') {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
if (n.type === 'Variable' && n.value) {
|
|
24
28
|
const lower = n.value.toLowerCase();
|
|
@@ -27,28 +31,64 @@ function collectVariableMap(node) {
|
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
if (Array.isArray(n.children))
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (n.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
if (Array.isArray(n.children)) {
|
|
35
|
+
n.children.forEach(walk);
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(n.statements)) {
|
|
38
|
+
n.statements.forEach(walk);
|
|
39
|
+
}
|
|
40
|
+
if (n.target) {
|
|
41
|
+
walk(n.target);
|
|
42
|
+
}
|
|
43
|
+
if (n.value && typeof n.value === 'object') {
|
|
44
|
+
walk(n.value);
|
|
45
|
+
}
|
|
46
|
+
if (n.expression) {
|
|
47
|
+
walk(n.expression);
|
|
48
|
+
}
|
|
49
|
+
if (n.condition) {
|
|
50
|
+
walk(n.condition);
|
|
51
|
+
}
|
|
52
|
+
if (n.left) {
|
|
53
|
+
walk(n.left);
|
|
54
|
+
}
|
|
55
|
+
if (n.right) {
|
|
56
|
+
walk(n.right);
|
|
57
|
+
}
|
|
58
|
+
if (n.argument) {
|
|
59
|
+
walk(n.argument);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(n.arguments)) {
|
|
62
|
+
n.arguments.forEach(walk);
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(n.variables)) {
|
|
65
|
+
n.variables.forEach(walk);
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(n.consequent)) {
|
|
68
|
+
n.consequent.forEach(walk);
|
|
69
|
+
}
|
|
42
70
|
if (Array.isArray(n.alternates)) {
|
|
43
71
|
for (const alt of n.alternates) {
|
|
44
|
-
if (alt.condition)
|
|
45
|
-
|
|
72
|
+
if (alt.condition) {
|
|
73
|
+
walk(alt.condition);
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(alt.body)) {
|
|
76
|
+
alt.body.forEach(walk);
|
|
77
|
+
}
|
|
46
78
|
}
|
|
47
79
|
}
|
|
48
|
-
if (Array.isArray(n.body))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (n.
|
|
80
|
+
if (Array.isArray(n.body)) {
|
|
81
|
+
n.body.forEach(walk);
|
|
82
|
+
}
|
|
83
|
+
if (n.counter) {
|
|
84
|
+
walk(n.counter);
|
|
85
|
+
}
|
|
86
|
+
if (n.startExpr) {
|
|
87
|
+
walk(n.startExpr);
|
|
88
|
+
}
|
|
89
|
+
if (n.endExpr) {
|
|
90
|
+
walk(n.endExpr);
|
|
91
|
+
}
|
|
52
92
|
}
|
|
53
93
|
|
|
54
94
|
walk(node);
|
|
@@ -57,9 +97,13 @@ function collectVariableMap(node) {
|
|
|
57
97
|
|
|
58
98
|
/**
|
|
59
99
|
* Detect needless parentheses around a single variable or literal.
|
|
100
|
+
*
|
|
101
|
+
* @param node
|
|
60
102
|
*/
|
|
61
103
|
function isNeedlessParenExpression(node) {
|
|
62
|
-
if (node.type !== 'ParenExpression')
|
|
104
|
+
if (node.type !== 'ParenExpression') {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
63
107
|
const inner = node.expression;
|
|
64
108
|
return (
|
|
65
109
|
inner.type === 'Variable' ||
|
|
@@ -90,18 +134,25 @@ function getOperatorPrecedence(op) {
|
|
|
90
134
|
* Context-aware check for whether a ParenExpression's parens can be removed.
|
|
91
135
|
* Covers simple atoms, function calls, nested parens, statement-level
|
|
92
136
|
* positions, and precedence-based removal inside binary expressions.
|
|
137
|
+
*
|
|
138
|
+
* @param path
|
|
139
|
+
* @param node
|
|
93
140
|
*/
|
|
94
141
|
function canRemoveParens(path, node) {
|
|
95
142
|
const inner = node.expression;
|
|
96
143
|
|
|
97
|
-
if (isNeedlessParenExpression(node))
|
|
144
|
+
if (isNeedlessParenExpression(node)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
98
147
|
|
|
99
148
|
if (inner.type === 'FunctionCall' || inner.type === 'ParenExpression') {
|
|
100
149
|
return true;
|
|
101
150
|
}
|
|
102
151
|
|
|
103
152
|
const parent = path.getParentNode();
|
|
104
|
-
if (!parent)
|
|
153
|
+
if (!parent) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
105
156
|
|
|
106
157
|
const STATEMENT_PARENTS = [
|
|
107
158
|
'SetStatement',
|
|
@@ -111,7 +162,9 @@ function canRemoveParens(path, node) {
|
|
|
111
162
|
'ForStatement',
|
|
112
163
|
'InlineExpression',
|
|
113
164
|
];
|
|
114
|
-
if (STATEMENT_PARENTS.includes(parent.type))
|
|
165
|
+
if (STATEMENT_PARENTS.includes(parent.type)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
115
168
|
|
|
116
169
|
if (parent.type === 'BinaryExpression') {
|
|
117
170
|
const parentPrec = getOperatorPrecedence(parent.operator);
|
|
@@ -133,41 +186,64 @@ function canRemoveParens(path, node) {
|
|
|
133
186
|
* processing are applied to every child node.
|
|
134
187
|
*
|
|
135
188
|
* @param {import("prettier").AstPath} path
|
|
136
|
-
* @param {object} options
|
|
137
|
-
* @param {function} print
|
|
189
|
+
* @param {object} options Merged Prettier + plugin options
|
|
190
|
+
* @param {function} print Prettier recursive print callback
|
|
138
191
|
*/
|
|
139
192
|
function printAmpscriptNode(path, options, print) {
|
|
140
193
|
const node = path.node;
|
|
141
|
-
if (!node)
|
|
194
|
+
if (!node) {
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
142
197
|
|
|
143
198
|
const variableMap = options.__ampscriptVariableMap || new Map();
|
|
144
199
|
const enforceSpacing = options.ampscriptSpacing;
|
|
145
200
|
const quoteChar = options.ampscriptQuoteStyle === 'single' ? "'" : '"';
|
|
146
201
|
const enforceCasing = options.ampscriptEnforceVariableCasing;
|
|
147
202
|
const removeBrackets = options.ampscriptRemoveUnnecessaryBrackets;
|
|
148
|
-
const variableStyle = options.ampscriptVarDeclarationStyle || '
|
|
203
|
+
const variableStyle = options.ampscriptVarDeclarationStyle || 'multi-line';
|
|
149
204
|
const keywordCase = options.ampscriptKeywordCase || 'lower';
|
|
150
205
|
const functionCase = options.ampscriptFunctionCase || 'upper-camel';
|
|
151
206
|
const blockLineBreaks = options.ampscriptBlockLineBreaks !== false;
|
|
152
207
|
|
|
153
208
|
function resolveVariable(variableName) {
|
|
154
|
-
if (!enforceCasing)
|
|
209
|
+
if (!enforceCasing) {
|
|
210
|
+
return variableName;
|
|
211
|
+
}
|
|
155
212
|
const lower = variableName.toLowerCase();
|
|
156
213
|
return variableMap.get(lower) || variableName;
|
|
157
214
|
}
|
|
158
215
|
|
|
159
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Apply keyword casing rule. Pass originalText for preserve mode.
|
|
218
|
+
*
|
|
219
|
+
* @param keyword
|
|
220
|
+
* @param originalText
|
|
221
|
+
*/
|
|
160
222
|
function kw(keyword, originalText) {
|
|
161
|
-
if (keywordCase === 'upper')
|
|
162
|
-
|
|
223
|
+
if (keywordCase === 'upper') {
|
|
224
|
+
return keyword.toUpperCase();
|
|
225
|
+
}
|
|
226
|
+
if (keywordCase === 'lower') {
|
|
227
|
+
return keyword.toLowerCase();
|
|
228
|
+
}
|
|
163
229
|
return originalText || keyword;
|
|
164
230
|
}
|
|
165
231
|
|
|
166
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Apply function casing rule
|
|
234
|
+
*
|
|
235
|
+
* @param name
|
|
236
|
+
*/
|
|
167
237
|
function function_(name) {
|
|
168
|
-
if (functionCase === 'preserve')
|
|
169
|
-
|
|
170
|
-
|
|
238
|
+
if (functionCase === 'preserve') {
|
|
239
|
+
return name;
|
|
240
|
+
}
|
|
241
|
+
if (functionCase === 'upper') {
|
|
242
|
+
return name.toUpperCase();
|
|
243
|
+
}
|
|
244
|
+
if (functionCase === 'lower') {
|
|
245
|
+
return name.toLowerCase();
|
|
246
|
+
}
|
|
171
247
|
const canonical = FUNCTION_CANONICAL_MAP.get(name.toLowerCase()) || name;
|
|
172
248
|
if (functionCase === 'lower-camel') {
|
|
173
249
|
return canonical[0].toLowerCase() + canonical.slice(1);
|