prettier-plugin-sfmc 0.1.4 → 0.3.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 +54 -16
- package/docs/options/ampscript-function-case.md +24 -0
- package/docs/options/ampscript-var-declaration-style.md +25 -26
- package/package.json +19 -4
- package/src/index.js +74 -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,44 @@ 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, JavaScript / SSJS)
|
|
64
|
+
|
|
65
|
+
This plugin exports [Prettier `defaultOptions`](https://prettier.io/docs/plugins#defaultoptions). Prettier merges them from whichever plugin **owns the active printer** for the file being formatted. This package supplies printers for **AMPscript**, **SQL** (via composed `prettier-plugin-sql`), and the shared **`estree`** printer (the same implementation Prettier ships for JavaScript). User plugins are loaded **after** built-ins, so this plugin becomes the effective `estree` printer—meaning **`.ssjs`** files (typically `parser: "babel"`) pick up the table below **without** copying these keys into `.prettierrc`.
|
|
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` | Common JS style; aligns with `ampscriptQuoteStyle: 'single'` where the core quote option applies. |
|
|
73
|
+
| `trailingComma` | `'none'` | Avoids trailing commas that can break SSJS in some SFMC contexts. |
|
|
74
|
+
|
|
75
|
+
String delimiters inside AMPscript blocks still follow `ampscriptQuoteStyle`. See [Prettier options](https://prettier.io/docs/options) for every standard flag.
|
|
76
|
+
|
|
77
|
+
**Overrides:** Add options to `.prettierrc` or [overrides](https://prettier.io/docs/configuration#configuration-overrides) only when you want to **diverge** (for example `tabWidth: 2` for the whole project, or different rules per file glob).
|
|
78
|
+
|
|
79
|
+
**Scope:** With this plugin enabled, any file Prettier formats using the **`estree`** printer uses these defaults—not only `.ssjs`. If another plugin in your config also registers `printers.estree`, plugin **order** matters (the last one wins).
|
|
80
|
+
|
|
81
|
+
## SQL (Transact-SQL / SFMC)
|
|
82
|
+
|
|
83
|
+
`.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.
|
|
84
|
+
|
|
85
|
+
**Defaults for SFMC-style T-SQL** (from composed [prettier-plugin-sql](https://www.npmjs.com/package/prettier-plugin-sql)):
|
|
86
|
+
|
|
87
|
+
| Option | Default | Other values | Rationale |
|
|
88
|
+
| ------ | ------- | ------------ | --------- |
|
|
89
|
+
| `language` | `tsql` | n/a | Must stay `tsql` for SFMC T-SQL. Other dialects are not supported for SFMC SQL; changing this can break formatting or behaviour. |
|
|
90
|
+
| `formatter` | `sql-formatter` | n/a | Must stay `sql-formatter` for SFMC SQL. Other formatters are not supported in this context; changing this can break. |
|
|
91
|
+
| `keywordCase` | `upper` | `preserve`, `lower` | Casing for reserved keywords. |
|
|
92
|
+
| `functionCase` | `upper` | `preserve`, `lower` | Casing for function names. |
|
|
93
|
+
| `identifierCase` | `preserve` | `upper`, `lower` | Unquoted identifiers only (upstream treats this as experimental). |
|
|
94
|
+
| `dataTypeCase` | `preserve` | `upper`, `lower` | Casing for data type names. |
|
|
95
|
+
|
|
96
|
+
Do **not** override `language` or `formatter` for SFMC. You may override the **casing** options in `.prettierrc` or under `overrides` with `files: "*.sql"` if you want different keyword/function/identifier/data-type casing.
|
|
97
|
+
|
|
98
|
+
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
99
|
|
|
62
100
|
## HTML Embedding
|
|
63
101
|
|
|
@@ -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.3.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 ./scripts/run-jest.mjs"
|
|
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",
|
|
@@ -40,12 +47,20 @@
|
|
|
40
47
|
"dependencies": {
|
|
41
48
|
"ampscript-data": "^0.1.3",
|
|
42
49
|
"ampscript-parser": "^0.1.1",
|
|
50
|
+
"prettier-plugin-sql": "^0.20.0",
|
|
43
51
|
"ssjs-data": "^0.2.2"
|
|
44
52
|
},
|
|
45
53
|
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^10.0.1",
|
|
46
55
|
"@jest/globals": "^29.0.0",
|
|
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",
|
|
47
62
|
"jest": "^29.0.0",
|
|
48
|
-
"prettier": "^3.
|
|
63
|
+
"prettier": "^3.8.1"
|
|
49
64
|
},
|
|
50
65
|
"peerDependencies": {
|
|
51
66
|
"prettier": ">=3.7.0"
|
package/src/index.js
CHANGED
|
@@ -2,17 +2,47 @@
|
|
|
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
|
+
* Re-exports Prettier’s `estree` printer so `defaultOptions` apply to JS/SSJS.
|
|
7
8
|
*
|
|
8
9
|
* Exports: languages, parsers, printers, options, defaultOptions
|
|
9
10
|
* Following the Prettier v3 plugin API (ESM).
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { parse } from 'ampscript-parser';
|
|
14
|
+
import sqlPlugin from 'prettier-plugin-sql';
|
|
15
|
+
import { printers as prettierEstreePrinters } from 'prettier/plugins/estree';
|
|
13
16
|
import { printAmpscriptNode, collectVariableMap } from './printer.js';
|
|
14
17
|
import * as prettier from 'prettier';
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Clone sql plugin option descriptors and set SFMC-friendly defaults.
|
|
21
|
+
*
|
|
22
|
+
* @param {Record<string, object>} sqlOptions
|
|
23
|
+
* @returns {Record<string, object>} Option map with adjusted defaults.
|
|
24
|
+
*/
|
|
25
|
+
function mergeSqlOptionsWithSfmcDefaults(sqlOptions) {
|
|
26
|
+
/** @type {Record<string, object>} */
|
|
27
|
+
const merged = {};
|
|
28
|
+
for (const [key, descriptor] of Object.entries(sqlOptions)) {
|
|
29
|
+
merged[key] = { ...descriptor };
|
|
30
|
+
}
|
|
31
|
+
const defaults = {
|
|
32
|
+
language: 'tsql',
|
|
33
|
+
keywordCase: 'upper',
|
|
34
|
+
functionCase: 'upper',
|
|
35
|
+
identifierCase: 'preserve',
|
|
36
|
+
dataTypeCase: 'preserve',
|
|
37
|
+
};
|
|
38
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
39
|
+
if (merged[key]) {
|
|
40
|
+
merged[key].default = defaultValue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
// ── Languages ────────────────────────────────────────────────────────────────
|
|
17
47
|
|
|
18
48
|
export const languages = [
|
|
@@ -28,6 +58,7 @@ export const languages = [
|
|
|
28
58
|
extensions: ['.ssjs'],
|
|
29
59
|
vscodeLanguageIds: ['ssjs'],
|
|
30
60
|
},
|
|
61
|
+
...sqlPlugin.languages,
|
|
31
62
|
];
|
|
32
63
|
|
|
33
64
|
// ── Parsers ──────────────────────────────────────────────────────────────────
|
|
@@ -35,6 +66,7 @@ export const languages = [
|
|
|
35
66
|
const PRAGMA_RE = /^\s*\/\*\*?\s*@(?:format|prettier)\s*\*\//;
|
|
36
67
|
|
|
37
68
|
export const parsers = {
|
|
69
|
+
...sqlPlugin.parsers,
|
|
38
70
|
'ampscript-parse': {
|
|
39
71
|
parse(text) {
|
|
40
72
|
return parse(text);
|
|
@@ -52,7 +84,9 @@ export const parsers = {
|
|
|
52
84
|
const blockClose = text.indexOf(']%%', blockOpen);
|
|
53
85
|
if (blockClose !== -1) {
|
|
54
86
|
const blockContent = text.slice(blockOpen + 3, blockClose);
|
|
55
|
-
if (PRAGMA_RE.test(blockContent))
|
|
87
|
+
if (PRAGMA_RE.test(blockContent)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
56
90
|
}
|
|
57
91
|
}
|
|
58
92
|
return PRAGMA_RE.test(text);
|
|
@@ -63,6 +97,8 @@ export const parsers = {
|
|
|
63
97
|
// ── Printers ─────────────────────────────────────────────────────────────────
|
|
64
98
|
|
|
65
99
|
export const printers = {
|
|
100
|
+
...sqlPlugin.printers,
|
|
101
|
+
estree: prettierEstreePrinters.estree,
|
|
66
102
|
'ampscript-ast': {
|
|
67
103
|
print(path, options, print) {
|
|
68
104
|
const node = path.node;
|
|
@@ -76,12 +112,16 @@ export const printers = {
|
|
|
76
112
|
|
|
77
113
|
embed(path, options) {
|
|
78
114
|
const node = path.node;
|
|
79
|
-
if (node.type !== 'Document')
|
|
115
|
+
if (node.type !== 'Document') {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
80
118
|
|
|
81
119
|
const hasHtml = node.children.some(
|
|
82
120
|
(c) => c.type === 'Content' && /<[a-zA-Z!/]/.test(c.value),
|
|
83
121
|
);
|
|
84
|
-
if (!hasHtml)
|
|
122
|
+
if (!hasHtml) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
85
125
|
|
|
86
126
|
return async (textToDocument, print) => {
|
|
87
127
|
if (!options.__ampscriptVariableMap) {
|
|
@@ -111,19 +151,27 @@ export const printers = {
|
|
|
111
151
|
|
|
112
152
|
const re = new RegExp(String.raw`${PH}(\d+)END`, 'g');
|
|
113
153
|
return prettier.doc.utils.mapDoc(htmlDocument, (d) => {
|
|
114
|
-
if (typeof d !== 'string')
|
|
115
|
-
|
|
154
|
+
if (typeof d !== 'string') {
|
|
155
|
+
return d;
|
|
156
|
+
}
|
|
157
|
+
if (!d.includes(PH)) {
|
|
158
|
+
return d;
|
|
159
|
+
}
|
|
116
160
|
|
|
117
161
|
const parts = [];
|
|
118
162
|
let last = 0;
|
|
119
163
|
let m;
|
|
120
164
|
re.lastIndex = 0;
|
|
121
165
|
while ((m = re.exec(d)) !== null) {
|
|
122
|
-
if (m.index > last)
|
|
166
|
+
if (m.index > last) {
|
|
167
|
+
parts.push(d.slice(last, m.index));
|
|
168
|
+
}
|
|
123
169
|
parts.push(print(['children', Number.parseInt(m[1], 10)]));
|
|
124
170
|
last = m.index + m[0].length;
|
|
125
171
|
}
|
|
126
|
-
if (last < d.length)
|
|
172
|
+
if (last < d.length) {
|
|
173
|
+
parts.push(d.slice(last));
|
|
174
|
+
}
|
|
127
175
|
return parts.length === 1 ? parts[0] : parts;
|
|
128
176
|
});
|
|
129
177
|
};
|
|
@@ -143,7 +191,9 @@ export const printers = {
|
|
|
143
191
|
|
|
144
192
|
printComment(path) {
|
|
145
193
|
const comment = path.node;
|
|
146
|
-
if (comment.type === 'Comment')
|
|
194
|
+
if (comment.type === 'Comment') {
|
|
195
|
+
return comment.value;
|
|
196
|
+
}
|
|
147
197
|
return '';
|
|
148
198
|
},
|
|
149
199
|
|
|
@@ -205,7 +255,7 @@ export const printers = {
|
|
|
205
255
|
|
|
206
256
|
// ── Options ──────────────────────────────────────────────────────────────────
|
|
207
257
|
|
|
208
|
-
|
|
258
|
+
const sfmcOptions = {
|
|
209
259
|
ampscriptSpacing: {
|
|
210
260
|
type: 'boolean',
|
|
211
261
|
category: 'AMPscript',
|
|
@@ -292,12 +342,12 @@ export const options = {
|
|
|
292
342
|
ampscriptVarDeclarationStyle: {
|
|
293
343
|
type: 'choice',
|
|
294
344
|
category: 'AMPscript',
|
|
295
|
-
default: '
|
|
345
|
+
default: 'multi-line',
|
|
296
346
|
choices: [
|
|
297
347
|
{
|
|
298
348
|
value: 'auto',
|
|
299
349
|
description:
|
|
300
|
-
|
|
350
|
+
'Let Prettier decide based on printWidth: single line when it fits, otherwise break after each comma.',
|
|
301
351
|
},
|
|
302
352
|
{
|
|
303
353
|
value: 'single-line',
|
|
@@ -305,16 +355,25 @@ export const options = {
|
|
|
305
355
|
},
|
|
306
356
|
{
|
|
307
357
|
value: 'multi-line',
|
|
308
|
-
description:
|
|
358
|
+
description:
|
|
359
|
+
'Always break after each comma: one `var` keyword, variables listed on separate lines.',
|
|
309
360
|
},
|
|
310
361
|
],
|
|
311
362
|
description: 'Control how var declarations with multiple variables are formatted.',
|
|
312
363
|
},
|
|
313
364
|
};
|
|
314
365
|
|
|
366
|
+
export const options = {
|
|
367
|
+
...mergeSqlOptionsWithSfmcDefaults(sqlPlugin.options),
|
|
368
|
+
...sfmcOptions,
|
|
369
|
+
};
|
|
370
|
+
|
|
315
371
|
// ── Default Options ──────────────────────────────────────────────────────────
|
|
316
372
|
|
|
317
373
|
export const defaultOptions = {
|
|
318
|
-
tabWidth: 2,
|
|
319
374
|
useTabs: false,
|
|
375
|
+
tabWidth: 4,
|
|
376
|
+
printWidth: 100,
|
|
377
|
+
singleQuote: true,
|
|
378
|
+
trailingComma: 'none',
|
|
320
379
|
};
|
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);
|