prettier-plugin-sfmc 0.3.0 → 0.4.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 CHANGED
@@ -36,7 +36,7 @@ All options use the `ampscript` prefix — they control AMPscript formatting beh
36
36
  | [`ampscriptQuoteStyle`](docs/options/ampscript-quote-style.md) | `"single"` \| `"double"` | `"single"` | String quote style |
37
37
  | [`ampscriptKeywordCase`](docs/options/ampscript-keyword-case.md) | `"lower"` \| `"upper"` \| `"preserve"` | `"lower"` | Keyword casing |
38
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 |
39
+ | [`ampscriptBlockLineBreaks`](docs/options/ampscript-block-line-breaks.md) | boolean | `false` | Optional line breaks around `%%[ ]%%` when not already at a line boundary |
40
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`
@@ -1,25 +1,31 @@
1
1
  # `ampscriptBlockLineBreaks`
2
2
 
3
- > Enforce blank lines before and after `%%[ … ]%%` block expressions.
3
+ > Optionally insert line breaks before and after `%%[ … ]%%` block expressions when they are **not** already at a line boundary in the source.
4
4
 
5
5
  | | |
6
6
  |---|---|
7
7
  | **Type** | `boolean` |
8
- | **Default** | `true` |
8
+ | **Default** | `false` |
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
- Adds an empty line before and after every AMPscript block (`%%[ ]%%`) to visually separate logic from surrounding HTML or text content. Inline expressions (`%%= =%%`) are never affected. Set to `false` when blocks are embedded in contexts such as attribute values or tightly formatted output sections — where extra blank lines would break the template layout.
13
+ When set to `true`, Prettier may add a **single** mandatory line break before the opening delimiter and/or after the closing delimiter so the block is not glued to other text on the same line (for example `</p>%%[` or `]%%<p>`).
14
+
15
+ Breaks are **not** added when the opening is already at the start of a logical line (beginning of file after optional UTF-8 BOM, optional spaces/tabs only before `%%[`, or immediately after `\n` / `\r\n` / `\r`), or when the closing is already at the end of a logical line (only horizontal whitespace until the next line break or end of file). That keeps formatting **idempotent** on repeated save instead of growing extra blank lines.
16
+
17
+ Inline expressions (`%%= … =%%`) are never affected.
18
+
19
+ The default is `false` so SMS and other line-break–sensitive templates are not altered unless you opt in.
14
20
 
15
21
  ## Settings
16
22
 
17
23
  | Value | Effect |
18
24
  |-------|--------|
19
- | `true` (default) | Blank line inserted before and after every `%%[ ]%%` block |
20
- | `false` | No blank lines added; blocks formatted flush with surrounding content |
25
+ | `false` (default) | No extra outer line breaks from this option; inner block layout still normalizes |
26
+ | `true` | Add outer line breaks only where the block is not already isolated on its own line |
21
27
 
22
- ### `true` (default)
28
+ ### `true` — inline-adjacent block
23
29
 
24
30
  **Input:**
25
31
 
@@ -30,35 +36,26 @@ set @name = "Alice"
30
36
  ]%%<p>%%=v(@name)=%%</p>
31
37
  ```
32
38
 
33
- **Output:**
34
-
35
- ```html
36
- <p>Hello</p>
39
+ **Output** (illustrative; exact HTML wrapping may include normal Prettier line breaks):
37
40
 
38
- %%[
39
- var @name
40
- set @name = "Alice"
41
- ]%%
41
+ - A line break is inserted so `%%[` is not on the same line as `</p>`.
42
+ - A line break is inserted so `]%%` is not on the same line as `<p>`.
42
43
 
43
- <p>%%= v(@name) =%%</p>
44
- ```
44
+ ### `true` — already line-isolated (no extra outer breaks)
45
45
 
46
- ### `false`
46
+ If the source already has `%%[` at the beginning of a line (after optional BOM / spaces / tabs) and `]%%` at the end of a line (before optional spaces and `\n` or EOF), enabling this option does **not** add further outer breaks, so the document does not grow on every format.
47
47
 
48
- **Output:**
48
+ ### `false` (default)
49
49
 
50
- ```html
51
- <p>Hello</p>%%[
52
- var @name
53
- set @name = "Alice"
54
- ]%%<p>%%= v(@name) =%%</p>
55
- ```
50
+ **Output:** blocks stay flush with surrounding content except for whatever the HTML / AMPscript printers do for normal indentation and line wrapping.
56
51
 
57
52
  ## Configuration Example
58
53
 
54
+ Opt in when you want blocks separated from inline HTML only where needed:
55
+
59
56
  ```json
60
57
  {
61
58
  "plugins": ["prettier-plugin-sfmc"],
62
- "ampscriptBlockLineBreaks": false
59
+ "ampscriptBlockLineBreaks": true
63
60
  }
64
61
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-sfmc",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Prettier plugin for Salesforce Marketing Cloud — AMPscript, SSJS, and SQL formatting",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,6 +27,7 @@
27
27
  "test": "node ./scripts/run-jest.mjs",
28
28
  "lint": "eslint .",
29
29
  "lint:fix": "eslint --fix .",
30
+ "prepare": "husky || true",
30
31
  "format": "prettier --write .",
31
32
  "format:check": "prettier --check ."
32
33
  },
@@ -59,6 +60,7 @@
59
60
  "eslint-plugin-prettier": "^5.5.0",
60
61
  "eslint-plugin-unicorn": "^64.0.0",
61
62
  "globals": "^17.4.0",
63
+ "husky": "^9.1.7",
62
64
  "jest": "^29.0.0",
63
65
  "prettier": "^3.8.1"
64
66
  },
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Detect whether AMPscript block delimiters already sit on their own logical
3
+ * lines in the original source (LF, CRLF, or CR). Used to avoid Prettier
4
+ * hardlines that would accumulate on repeated format.
5
+ *
6
+ * @param {string} ch
7
+ */
8
+ function isHorizontalSpaceOrBom(ch) {
9
+ return ch === ' ' || ch === '\t' || ch === '\uFEFF';
10
+ }
11
+
12
+ /**
13
+ * True if `index` is the first non–BOM/horizontal-ws column of a line
14
+ * (BOF, or only BOM/ws before index, or newline before index after skipping ws).
15
+ *
16
+ * @param {string} text
17
+ * @param {number} index
18
+ */
19
+ function isIndexAtLineStart(text, index) {
20
+ if (index <= 0) {
21
+ return true;
22
+ }
23
+
24
+ let i = index - 1;
25
+ while (i >= 0 && isHorizontalSpaceOrBom(text[i])) {
26
+ i--;
27
+ }
28
+
29
+ if (i < 0) {
30
+ return true;
31
+ }
32
+
33
+ const ch = text[i];
34
+ return ch === '\n' || ch === '\r';
35
+ }
36
+
37
+ /**
38
+ * True if everything from `index` to EOF is horizontal ws/BOM, or the first
39
+ * non-ws character begins a new line.
40
+ *
41
+ * @param {string} text
42
+ * @param {number} index
43
+ */
44
+ function isIndexAtLineEnd(text, index) {
45
+ const len = text.length;
46
+ let j = index;
47
+
48
+ while (j < len && isHorizontalSpaceOrBom(text[j])) {
49
+ j++;
50
+ }
51
+
52
+ if (j >= len) {
53
+ return true;
54
+ }
55
+
56
+ const ch = text[j];
57
+ return ch === '\n' || ch === '\r';
58
+ }
59
+
60
+ /**
61
+ * When ampscriptBlockLineBreaks is enabled, add a leading hardline only if
62
+ * the block opening is not already at the start of a line in the source.
63
+ *
64
+ * @param {string | undefined} originalText
65
+ * @param {number | undefined} blockStart
66
+ */
67
+ export function needsLeadingBlockBreak(originalText, blockStart) {
68
+ if (typeof originalText !== 'string' || blockStart == null || blockStart < 0) {
69
+ return false;
70
+ }
71
+
72
+ return !isIndexAtLineStart(originalText, blockStart);
73
+ }
74
+
75
+ /**
76
+ * When ampscriptBlockLineBreaks is enabled, add a trailing hardline only if
77
+ * the block closing is not already at the end of a line in the source.
78
+ *
79
+ * @param {string | undefined} originalText
80
+ * @param {number | undefined} blockEnd
81
+ */
82
+ export function needsTrailingBlockBreak(originalText, blockEnd) {
83
+ if (typeof originalText !== 'string' || blockEnd == null || blockEnd < 0) {
84
+ return false;
85
+ }
86
+
87
+ return !isIndexAtLineEnd(originalText, blockEnd);
88
+ }
package/src/index.js CHANGED
@@ -333,11 +333,12 @@ const sfmcOptions = {
333
333
  ampscriptBlockLineBreaks: {
334
334
  type: 'boolean',
335
335
  category: 'AMPscript',
336
- default: true,
336
+ default: false,
337
337
  description:
338
- 'Enforce line breaks before and after %%[ ... ]%% blocks. ' +
338
+ 'When true, insert line breaks before and after %%[ ... ]%% blocks only when the ' +
339
+ 'opening/closing delimiters are not already at a line boundary in the source. ' +
339
340
  'Inline expressions (%%= ... =%%) are never affected. ' +
340
- 'Set to false when blocks are embedded in contexts where extra line breaks are unwanted.',
341
+ 'Default is false to avoid surprising line breaks in SMS and similar contexts.',
341
342
  },
342
343
  ampscriptVarDeclarationStyle: {
343
344
  type: 'choice',
package/src/printer.js CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import * as prettier from 'prettier';
10
10
  import { FUNCTION_CANONICAL_MAP, AMPSCRIPT_KEYWORDS } from 'ampscript-data';
11
+ import { needsLeadingBlockBreak, needsTrailingBlockBreak } from './ampscript-block-boundaries.js';
11
12
 
12
13
  const { group, indent, join, line, softline, hardline } = prettier.doc.builders;
13
14
 
@@ -203,7 +204,7 @@ function printAmpscriptNode(path, options, print) {
203
204
  const variableStyle = options.ampscriptVarDeclarationStyle || 'multi-line';
204
205
  const keywordCase = options.ampscriptKeywordCase || 'lower';
205
206
  const functionCase = options.ampscriptFunctionCase || 'upper-camel';
206
- const blockLineBreaks = options.ampscriptBlockLineBreaks !== false;
207
+ const blockLineBreaks = options.ampscriptBlockLineBreaks === true;
207
208
 
208
209
  function resolveVariable(variableName) {
209
210
  if (!enforceCasing) {
@@ -295,8 +296,21 @@ function printAmpscriptNode(path, options, print) {
295
296
  blockClose,
296
297
  ]);
297
298
  if (blockLineBreaks) {
298
- return [hardline, blockDocument, hardline];
299
+ const originalText = options.originalText;
300
+ const parts = [];
301
+ if (needsLeadingBlockBreak(originalText, node.start)) {
302
+ parts.push(hardline);
303
+ }
304
+
305
+ parts.push(blockDocument);
306
+
307
+ if (needsTrailingBlockBreak(originalText, node.end)) {
308
+ parts.push(hardline);
309
+ }
310
+
311
+ return parts.length === 1 ? parts[0] : parts;
299
312
  }
313
+
300
314
  return blockDocument;
301
315
  }
302
316