@aforemendude/prettier-plugin-wrap-comments 1.1.0 → 1.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.
Files changed (35) hide show
  1. package/README.md +80 -6
  2. package/dist/comments/comment-body.js +2 -2
  3. package/dist/comments/comment-directives.js +4 -0
  4. package/dist/comments/comment-eligibility.d.ts +1 -0
  5. package/dist/comments/comment-eligibility.js +4 -1
  6. package/dist/comments/comment-location.js +24 -6
  7. package/dist/comments/embedded-expression-ranges.d.ts +2 -3
  8. package/dist/comments/embedded-expression-ranges.js +39 -27
  9. package/dist/comments/jsx-expression-layout.d.ts +2 -2
  10. package/dist/comments/jsx-expression-layout.js +16 -19
  11. package/dist/comments/prettier-ignore.d.ts +2 -1
  12. package/dist/comments/prettier-ignore.js +110 -16
  13. package/dist/comments/printer-layout.d.ts +1 -0
  14. package/dist/comments/printer-layout.js +1 -0
  15. package/dist/comments/wrap-block-comment.d.ts +1 -0
  16. package/dist/comments/wrap-block-comment.js +3 -1
  17. package/dist/comments/wrap-comments.js +75 -7
  18. package/dist/comments/wrap-trailing-line-comment.d.ts +1 -1
  19. package/dist/index.d.ts +6 -2
  20. package/dist/index.js +4 -1
  21. package/dist/plugin/create-parsers.js +92 -5
  22. package/dist/plugin/create-printers.js +23 -2
  23. package/dist/plugin/get-printer-layout-source.js +10 -2
  24. package/dist/plugin/plugin-name.d.ts +1 -0
  25. package/dist/plugin/plugin-name.js +1 -0
  26. package/dist/utils/ast.d.ts +6 -0
  27. package/dist/utils/ast.js +43 -0
  28. package/dist/utils/display-width.d.ts +1 -1
  29. package/dist/utils/display-width.js +3 -3
  30. package/dist/utils/format-markdown.js +50 -11
  31. package/dist/utils/source-lines.d.ts +1 -0
  32. package/dist/utils/source-lines.js +3 -0
  33. package/dist/utils/whitespace.d.ts +1 -0
  34. package/dist/utils/whitespace.js +4 -0
  35. package/package.json +15 -6
package/README.md CHANGED
@@ -5,7 +5,8 @@ real column to calculate the available content width, so nested comments wrap mo
5
5
 
6
6
  ## Requirements
7
7
 
8
- - Node.js 20 or newer
8
+ - Node.js 20 or newer to use the plugin
9
+ - Node.js 22.12 or newer to develop the plugin
9
10
  - Prettier 3 (`>=3.0.0 <4.0.0`)
10
11
 
11
12
  ## Install
@@ -30,12 +31,52 @@ Then run Prettier normally:
30
31
  npx prettier --write .
31
32
  ```
32
33
 
34
+ ### Use With `prettier-plugin-jsdoc`
35
+
36
+ When using both plugins, `@aforemendude/prettier-plugin-wrap-comments` must be loaded after `prettier-plugin-jsdoc`:
37
+
38
+ ```json
39
+ {
40
+ "plugins": ["prettier-plugin-jsdoc", "@aforemendude/prettier-plugin-wrap-comments"]
41
+ }
42
+ ```
43
+
44
+ This order is required regardless of `prettier-plugin-jsdoc`'s documentation saying that it should be the last plugin.
45
+ With wrap-comments last, ordinary comments are handled here and parsing is delegated to the preceding JSDoc plugin, so
46
+ JSDoc comments are formatted as well.
47
+
48
+ ### Cache Repeated CLI Runs
49
+
50
+ Prettier's CLI cache skips files that have not changed since a successful formatting pass. Enable it in package scripts
51
+ so contributors use it consistently. This repository uses the faster `metadata` strategy for local formatting:
52
+
53
+ ```json
54
+ {
55
+ "scripts": {
56
+ "format": "prettier --write --cache --cache-strategy metadata .",
57
+ "format:check": "prettier --check --cache --cache-strategy metadata .",
58
+ "format:nocache": "prettier --write ."
59
+ }
60
+ }
61
+ ```
62
+
63
+ The first cached run still processes every file; later runs skip files whose relevant metadata and other cache keys have
64
+ not changed. Omit `--cache-strategy metadata` to use the default `content` strategy when workflows such as Git
65
+ operations frequently change timestamps without changing file contents.
66
+
67
+ By default, Prettier stores the cache under `node_modules/.cache/prettier/`, which is normally excluded from version
68
+ control with `node_modules`. Prettier does not include plugin versions or implementations in its cache keys, so run the
69
+ uncached command once after updating this plugin or another Prettier plugin. Running Prettier without `--cache`, as the
70
+ `format:nocache` script does, also removes the default cache. See [Prettier's CLI cache documentation][prettier-cache]
71
+ for cache keys, strategies, and custom cache locations.
72
+
33
73
  ## Behavior
34
74
 
35
75
  The plugin wraps comments for Prettier's `babel`, `babel-ts`, and `typescript` parsers. It runs during parser
36
- preprocessing: the underlying Prettier parser preprocesses and parses the source first, the plugin rewrites eligible
37
- comments from that parsed comment list, and Prettier then formats the rewritten source with its built-in JavaScript and
38
- TypeScript printers. If the parser cannot parse the preprocessed source, the plugin leaves the source unchanged.
76
+ preprocessing: the nearest preceding plugin for the selected parser, or Prettier's built-in parser when there is none,
77
+ preprocesses and parses the source first. This plugin rewrites eligible comments from that parsed comment list, and
78
+ Prettier then formats the rewritten source with its built-in JavaScript and TypeScript printers. If the parser cannot
79
+ parse the preprocessed source, the plugin leaves the source unchanged.
39
80
 
40
81
  Offset-sensitive formatting is conservative. Full-file `formatWithCursor` calls skip comment rewriting so Prettier can
41
82
  map the cursor from the original source. During range formatting, preprocessing does not rewrite text outside Prettier's
@@ -148,10 +189,13 @@ The plugin leaves these comments unchanged:
148
189
  - JSDoc comments that start with `/**`
149
190
  - bang-preserved comments that start with `/*!` or `//!`
150
191
  - TypeScript-style triple-slash line comments that start with `///`
192
+ - Flow type annotations and includes that start with `/*:`, `/*::`, or `/*flow-include`, including Flow's supported
193
+ spaces or tabs before the marker
151
194
  - empty comment bodies
152
195
  - `prettier-ignore` markers themselves
153
- - other directive comments such as `@license`, `@preserve`, JSX and TypeScript pragmas, source map directives,
154
- `#__PURE__`, `@__PURE__`, lint/coverage/formatter directives, `vite-ignore`, and webpack magic comments
196
+ - other directive comments such as `@license`, `@preserve`, JSX, TypeScript, and Flow pragmas, Flow error suppressions,
197
+ `flowlint` comments, source map directives, `#__PURE__`, `@__PURE__`, exact Node test coverage controls, other
198
+ lint/coverage/formatter directives, `vite-ignore`, and webpack magic comments
155
199
 
156
200
  ## Supported Parsers
157
201
 
@@ -159,6 +203,28 @@ The plugin leaves these comments unchanged:
159
203
  - `babel-ts`
160
204
  - `typescript`
161
205
 
206
+ ## Performance
207
+
208
+ `npm run benchmark` compares uncached, in-memory `prettier.format()` calls with and without the plugin across five
209
+ generated files. Lower times are better. The plugin/plain column divides the plugin mean by the plain Prettier mean.
210
+
211
+ These results are from a representative run on August 8, 2026, using Linux 6.8, an Intel Core i7-10750H, Node.js
212
+ 24.18.0, Prettier 3.9.6, and Vitest 4.1.10. The suite was configured with `time: 500`, `iterations: 10`,
213
+ `warmupTime: 100`, and `warmupIterations: 2` for each case.
214
+
215
+ | Generated workload | Characters | Plain Prettier mean | Plugin mean | Plugin/plain |
216
+ | ------------------------------------------ | ---------: | ------------------: | -----------------: | -----------: |
217
+ | Comment-free JavaScript | 29,995 | 54.78 ms (±10.09%) | 45.59 ms (±5.82%) | 0.83× |
218
+ | Code-heavy JavaScript with sparse comments | 30,494 | 44.05 ms (±3.22%) | 135.66 ms (±3.81%) | 3.08× |
219
+ | Comment-heavy TypeScript | 64,432 | 56.75 ms (±11.82%) | 327.61 ms (±2.67%) | 5.77× |
220
+ | Preformatted comment-heavy TypeScript | 54,467 | 54.54 ms (±2.47%) | 279.65 ms (±2.50%) | 5.13× |
221
+ | JSX-comment-heavy TSX | 40,575 | 57.52 ms (±6.87%) | 263.67 ms (±3.83%) | 4.58× |
222
+
223
+ The comment-free case takes the plugin's early exit after scanning for comment delimiters. Its apparent speedup should
224
+ be treated as benchmark variation, not as an expected optimization over plain Prettier. In this run, sparse comments
225
+ took about 3.08× the plain formatting time, while comment-heavy inputs took 4.58–5.77×. These measurements isolate
226
+ formatter work on changed files; enabling the CLI cache above skips that work for unchanged files.
227
+
162
228
  ## Development
163
229
 
164
230
  Source files are organized by responsibility. `src/plugin/` contains parser and printer integration,
@@ -168,6 +234,7 @@ concerns and source file names wherever practical.
168
234
 
169
235
  ```sh
170
236
  npm install
237
+ npm run benchmark
171
238
  npm run format:check
172
239
  npm run typecheck
173
240
  npm run test
@@ -177,3 +244,10 @@ npm run build
177
244
  `npm run test` runs the TypeScript unit and fixture-based integration suites with Vitest. Use `npm run test:unit` or
178
245
  `npm run test:integration` to run one suite. `npm run build` removes and recreates `dist` using a cross-platform Node
179
246
  cleanup script, and `npm run verify` runs formatting, type checking, the build, and both test suites.
247
+
248
+ `npm run benchmark` compares plain Prettier with Prettier using the plugin. Its JavaScript, TypeScript, and TSX inputs
249
+ are generated in memory by the files under `test/benchmark`, so large benchmark fixtures are not stored in the
250
+ repository. Benchmarks use Vitest's separate benchmark mode and do not run as part of `npm run test` or
251
+ `npm run verify`.
252
+
253
+ [prettier-cache]: https://prettier.io/docs/cli#--cache
@@ -1,4 +1,4 @@
1
- import { isBlankLine } from '../utils/source-lines.js';
1
+ import { isBlankLine, normalizeLineTerminators } from '../utils/source-lines.js';
2
2
  export function getCommentBody(text, comment) {
3
3
  const raw = text.slice(comment.start, comment.end);
4
4
  return comment.kind === 'line' ? normalizeLineCommentBody(raw.slice(2)) : normalizeBlockCommentBody(raw);
@@ -10,7 +10,7 @@ export function normalizeLineCommentBody(rawBody) {
10
10
  return rawBody.replace(/^[ \t]?/, '').replace(/[ \t]+$/u, '');
11
11
  }
12
12
  export function normalizeBlockCommentBody(rawComment) {
13
- const body = rawComment.slice(2, -2).replace(/\r\n?/g, '\n');
13
+ const body = normalizeLineTerminators(rawComment.slice(2, -2));
14
14
  const lines = body.split('\n');
15
15
  if (lines.length === 1) {
16
16
  return lines[0]?.trim() ?? '';
@@ -1,4 +1,5 @@
1
1
  const PRAGMA_DIRECTIVE_COMMENT_PATTERNS = [
2
+ /^@(?:flow|noflow)\b/u,
2
3
  /^@(?:license|preserve)\b/u,
3
4
  /^@(?:jsxFrag|jsxImportSource|jsxRuntime|jsx)\b/u,
4
5
  /^@(?:ts-check|ts-expect-error|ts-ignore|ts-nocheck)\b/u,
@@ -17,9 +18,11 @@ const TOOL_DIRECTIVE_COMMENT_PATTERNS = [
17
18
  /^eslint\b/u,
18
19
  /^eslint-/u,
19
20
  /^exported\b/u,
21
+ /^flowlint(?:-next-line|-line)?\b/u,
20
22
  /^globals?\b/u,
21
23
  /^istanbul\b/u,
22
24
  /^jshint\b/u,
25
+ /^node:coverage (?:disable|enable|ignore next(?: [1-9]\d*)?)$/u,
23
26
  /^nyc\b/u,
24
27
  /^oxlint\b/u,
25
28
  /^prettier-ignore\b/u,
@@ -29,6 +32,7 @@ const TOOL_DIRECTIVE_COMMENT_PATTERNS = [
29
32
  /^tslint\b/u,
30
33
  /^v8\b/u,
31
34
  /^vite-ignore\b/u,
35
+ /^\$(?:FlowExpectedError|FlowFixMe)\b/u,
32
36
  ];
33
37
  const BUNDLER_DIRECTIVE_COMMENT_PATTERNS = [
34
38
  /^webpack(?:ChunkName|Exclude|Ignore|Include|Mode|Prefetch|Preload)\b/u,
@@ -2,3 +2,4 @@ import type { CommentRange } from './comment-ranges.js';
2
2
  export declare function shouldSkipLineComment(text: string, comment: CommentRange): boolean;
3
3
  export declare function shouldSkipBlockComment(text: string, comment: CommentRange): boolean;
4
4
  export declare function hasPreserveCommentMarker(rawComment: string): boolean;
5
+ export declare function hasFlowCommentTypeMarker(rawComment: string): boolean;
@@ -9,7 +9,7 @@ export function shouldSkipLineComment(text, comment) {
9
9
  }
10
10
  export function shouldSkipBlockComment(text, comment) {
11
11
  const raw = text.slice(comment.start, comment.end);
12
- if (raw.startsWith('/**') || hasPreserveCommentMarker(raw)) {
12
+ if (raw.startsWith('/**') || hasPreserveCommentMarker(raw) || hasFlowCommentTypeMarker(raw)) {
13
13
  return true;
14
14
  }
15
15
  const body = normalizeBlockCommentBody(raw);
@@ -18,3 +18,6 @@ export function shouldSkipBlockComment(text, comment) {
18
18
  export function hasPreserveCommentMarker(rawComment) {
19
19
  return rawComment.startsWith('/*!') || rawComment.startsWith('//!');
20
20
  }
21
+ export function hasFlowCommentTypeMarker(rawComment) {
22
+ return /^\/\*[ \t]*(?::|flow-include)/u.test(rawComment);
23
+ }
@@ -1,21 +1,39 @@
1
1
  import { getLineEnd, getLinePrefix, getLineStart } from '../utils/source-lines.js';
2
+ import { isEcmaScriptHorizontalWhitespace } from '../utils/whitespace.js';
2
3
  export function isStandaloneComment(text, comment) {
3
4
  return comment.kind === 'line' ? isStandaloneLineComment(text, comment) : isStandaloneBlockComment(text, comment);
4
5
  }
5
6
  export function isStandaloneLineComment(text, comment) {
6
- return /^[ \t]*$/u.test(getLinePrefix(text, comment.start));
7
+ return isOnlyHorizontalWhitespace(getLinePrefix(text, comment.start));
7
8
  }
8
9
  export function isStandaloneBlockComment(text, comment) {
9
10
  const before = text.slice(getLineStart(text, comment.start), comment.start);
10
11
  const after = text.slice(comment.end, getLineEnd(text, comment.end));
11
- return /^[ \t]*$/u.test(before) && /^[ \t]*$/u.test(after);
12
+ return isOnlyHorizontalWhitespace(before) && isOnlyHorizontalWhitespace(after);
12
13
  }
13
14
  export function areCommentsOnAdjacentLines(text, previousComment, comment) {
14
- return isOnlyNewlineAndIndent(text.slice(previousComment.end, comment.start));
15
+ return isOnlyHorizontalWhitespaceAroundNewline(text.slice(previousComment.end, comment.start));
15
16
  }
16
17
  export function isCommentAdjacentBeforeIndex(text, comment, index) {
17
- return isOnlyNewlineAndIndent(text.slice(comment.end, index));
18
+ return isOnlyHorizontalWhitespaceAroundNewline(text.slice(comment.end, index));
18
19
  }
19
- function isOnlyNewlineAndIndent(text) {
20
- return /^(?:\r\n|[\n\r\u2028\u2029])[ \t]*$/u.test(text);
20
+ function isOnlyHorizontalWhitespaceAroundNewline(text) {
21
+ let newlineStart = 0;
22
+ while (newlineStart < text.length) {
23
+ const character = text[newlineStart];
24
+ if (character === undefined || !isEcmaScriptHorizontalWhitespace(character)) {
25
+ break;
26
+ }
27
+ newlineStart += 1;
28
+ }
29
+ const newline = /^(?:\r\n|[\n\r\u2028\u2029])/u.exec(text.slice(newlineStart))?.[0];
30
+ return newline !== undefined && isOnlyHorizontalWhitespace(text.slice(newlineStart + newline.length));
31
+ }
32
+ function isOnlyHorizontalWhitespace(text) {
33
+ for (const character of text) {
34
+ if (!isEcmaScriptHorizontalWhitespace(character)) {
35
+ return false;
36
+ }
37
+ }
38
+ return true;
21
39
  }
@@ -8,6 +8,5 @@ export type EmbeddedTrailingLineCommentMove = {
8
8
  removeStart: number;
9
9
  };
10
10
  export declare function collectEmbeddedExpressionRanges(ast: unknown): EmbeddedExpressionRange[];
11
- export declare function isCommentInEmbeddedExpression(comment: CommentRange, ranges: SourceRange[]): boolean;
12
- export declare function getEmbeddedTrailingLineCommentMove(text: string, comment: CommentRange, ranges: EmbeddedExpressionRange[]): EmbeddedTrailingLineCommentMove | undefined;
13
- export declare function doesBlockCommentSeparateEmbeddedTrailingLineComment(text: string, blockComment: CommentRange, nextComment: CommentRange | undefined, ranges: EmbeddedExpressionRange[]): boolean;
11
+ export declare function getEmbeddedTrailingLineCommentMove(text: string, comment: CommentRange, range: EmbeddedExpressionRange | undefined): EmbeddedTrailingLineCommentMove | undefined;
12
+ export declare function doesBlockCommentSeparateEmbeddedTrailingLineComment(text: string, blockComment: CommentRange, nextComment: CommentRange | undefined, range: EmbeddedExpressionRange | undefined, nextRange: EmbeddedExpressionRange | undefined): boolean;
@@ -1,6 +1,7 @@
1
1
  import { getAstNodeRange, visitAstNodes } from '../utils/ast.js';
2
2
  import { getLineEnd } from '../utils/source-lines.js';
3
3
  import { isRecord, numberOrUndefined } from '../utils/type-guards.js';
4
+ import { skipWhitespace, trimWhitespaceEnd } from '../utils/whitespace.js';
4
5
  const JSX_EMBEDDED_EXPRESSION_TYPES = new Set(['JSXExpressionContainer', 'JSXSpreadAttribute', 'JSXSpreadChild']);
5
6
  export function collectEmbeddedExpressionRanges(ast) {
6
7
  const ranges = [];
@@ -27,36 +28,59 @@ export function collectEmbeddedExpressionRanges(ast) {
27
28
  });
28
29
  return ranges.sort((left, right) => left.start - right.start || right.end - left.end);
29
30
  }
30
- export function isCommentInEmbeddedExpression(comment, ranges) {
31
- return getSmallestContainingRange(comment, ranges) !== undefined;
32
- }
33
- export function getEmbeddedTrailingLineCommentMove(text, comment, ranges) {
34
- const range = getSmallestContainingRange(comment, ranges);
31
+ export function getEmbeddedTrailingLineCommentMove(text, comment, range) {
35
32
  const expression = range?.expression;
36
- if (expression === undefined ||
37
- expression.end > comment.start ||
38
- !/^[\t ]*$/u.test(text.slice(expression.end, comment.start))) {
33
+ if (range === undefined || expression === undefined) {
39
34
  return undefined;
40
35
  }
41
- return { insertAt: expression.start, removeStart: expression.end };
36
+ const rootExpression = getRootExpressionRangeBefore(text, expression, comment.start, range.start);
37
+ return rootExpression === undefined ? undefined : { insertAt: rootExpression.start, removeStart: rootExpression.end };
42
38
  }
43
- export function doesBlockCommentSeparateEmbeddedTrailingLineComment(text, blockComment, nextComment, ranges) {
39
+ export function doesBlockCommentSeparateEmbeddedTrailingLineComment(text, blockComment, nextComment, range, nextRange) {
44
40
  if (blockComment.kind !== 'block' ||
45
41
  nextComment?.kind !== 'line' ||
46
42
  blockComment.end >= nextComment.start ||
47
43
  nextComment.end > getLineEnd(text, blockComment.start)) {
48
44
  return false;
49
45
  }
50
- const range = getSmallestContainingRange(blockComment, ranges);
51
- const nextRange = getSmallestContainingRange(nextComment, ranges);
52
46
  const expression = range?.expression;
47
+ const rootExpression = range === undefined || expression === undefined
48
+ ? undefined
49
+ : getRootExpressionRangeBefore(text, expression, blockComment.start, range.start);
53
50
  return (range !== undefined &&
54
51
  range === nextRange &&
55
- expression !== undefined &&
56
- expression.end <= blockComment.start &&
57
- /^[\t ]*$/u.test(text.slice(expression.end, blockComment.start)) &&
52
+ rootExpression !== undefined &&
58
53
  /^[\t ]*$/u.test(text.slice(blockComment.end, nextComment.start)));
59
54
  }
55
+ function getRootExpressionRangeBefore(text, expression, before, containerStart) {
56
+ if (expression.end > before) {
57
+ return undefined;
58
+ }
59
+ let cursor = expression.end;
60
+ let expandedEnd = expression.end;
61
+ let parenthesisCount = 0;
62
+ while (cursor < before) {
63
+ cursor = skipWhitespace(text, cursor);
64
+ if (cursor >= before) {
65
+ break;
66
+ }
67
+ if (text[cursor] !== ')') {
68
+ return undefined;
69
+ }
70
+ cursor += 1;
71
+ expandedEnd = cursor;
72
+ parenthesisCount += 1;
73
+ }
74
+ let expandedStart = expression.start;
75
+ for (let index = 0; index < parenthesisCount; index += 1) {
76
+ expandedStart = trimWhitespaceEnd(text, containerStart, expandedStart);
77
+ if (expandedStart <= containerStart || text[expandedStart - 1] !== '(') {
78
+ return undefined;
79
+ }
80
+ expandedStart -= 1;
81
+ }
82
+ return { end: expandedEnd, start: expandedStart };
83
+ }
60
84
  function collectTemplateInterpolationRanges(node, expressionsKey, ranges) {
61
85
  const quasis = node['quasis'];
62
86
  const expressions = node[expressionsKey];
@@ -91,15 +115,3 @@ function getNodeBoundary(value) {
91
115
  }
92
116
  return { end, start };
93
117
  }
94
- function getSmallestContainingRange(comment, ranges) {
95
- let containingRange;
96
- for (const range of ranges) {
97
- if (comment.start <= range.start || comment.end >= range.end) {
98
- continue;
99
- }
100
- if (containingRange === undefined || range.end - range.start < containingRange.end - containingRange.start) {
101
- containingRange = range;
102
- }
103
- }
104
- return containingRange;
105
- }
@@ -1,10 +1,10 @@
1
1
  import type { CommentRange } from './comment-ranges.js';
2
2
  import type { PrinterCommentLayout } from './printer-layout.js';
3
3
  import type { BlockCommentLayout } from './wrap-block-comment.js';
4
- import type { SourceRange } from '../utils/ast.js';
4
+ import type { ContainingRangeMatch, SourceRange } from '../utils/ast.js';
5
5
  export type JsxExpressionContainerRange = SourceRange & {
6
6
  expression: SourceRange | undefined;
7
7
  };
8
8
  export declare function collectJsxExpressionContainerRanges(ast: unknown): JsxExpressionContainerRange[];
9
9
  export declare function getPrintedJsxCommentMarkerColumn(text: string, container: SourceRange, tabWidth: number): number;
10
- export declare function getJsxExpressionBlockCommentLayout(text: string, comment: CommentRange, previousComment: CommentRange | undefined, containers: JsxExpressionContainerRange[], tabWidth: number, outputCommentLayout: PrinterCommentLayout | undefined, outputCommentMarkerColumns: Array<number | undefined>): BlockCommentLayout | undefined;
10
+ export declare function getJsxExpressionBlockCommentLayout(text: string, comment: CommentRange, previousComment: CommentRange | undefined, containerMatch: ContainingRangeMatch<JsxExpressionContainerRange> | undefined, tabWidth: number, outputCommentLayout: PrinterCommentLayout | undefined, outputCommentMarkerColumns: Array<number | undefined>): BlockCommentLayout | undefined;
@@ -2,7 +2,7 @@ import { isStandaloneBlockComment } from './comment-location.js';
2
2
  import { getAstNodeRange, visitAstNodes } from '../utils/ast.js';
3
3
  import { getColumns } from '../utils/display-width.js';
4
4
  import { getLeadingIndent } from '../utils/indentation.js';
5
- import { getLinePrefix } from '../utils/source-lines.js';
5
+ import { getLinePrefix, getLineStart } from '../utils/source-lines.js';
6
6
  import { isRecord } from '../utils/type-guards.js';
7
7
  import { skipWhitespace, trimWhitespaceEnd } from '../utils/whitespace.js';
8
8
  export function collectJsxExpressionContainerRanges(ast) {
@@ -28,15 +28,17 @@ export function getPrintedJsxCommentMarkerColumn(text, container, tabWidth) {
28
28
  const lineIndent = getLeadingIndent(linePrefix);
29
29
  return getColumns(lineIndent, tabWidth) + tabWidth;
30
30
  }
31
- export function getJsxExpressionBlockCommentLayout(text, comment, previousComment, containers, tabWidth, outputCommentLayout, outputCommentMarkerColumns) {
32
- const container = getSmallestContainingRange(comment, containers);
33
- if (container === undefined || text[container.start] !== '{' || text[container.end - 1] !== '}') {
31
+ export function getJsxExpressionBlockCommentLayout(text, comment, previousComment, containerMatch, tabWidth, outputCommentLayout, outputCommentMarkerColumns) {
32
+ if (containerMatch === undefined) {
33
+ return undefined;
34
+ }
35
+ const container = containerMatch.range;
36
+ if (text[container.start] !== '{' || text[container.end - 1] !== '}') {
34
37
  return undefined;
35
38
  }
36
39
  const hasExpressionBeforeComment = container.expression !== undefined && container.expression.start < comment.start;
37
40
  const hasExpressionAfterComment = container.expression !== undefined && container.expression.end > comment.end;
38
- const containerIndex = containers.indexOf(container);
39
- const multilineMarkerColumn = outputCommentMarkerColumns[containerIndex] ??
41
+ const multilineMarkerColumn = outputCommentMarkerColumns[containerMatch.index] ??
40
42
  getJsxExpressionContainerOutputColumn(text, container, tabWidth) + tabWidth;
41
43
  const markerColumn = outputCommentLayout?.markerColumn ?? multilineMarkerColumn;
42
44
  const contentColumn = multilineMarkerColumn + 3;
@@ -78,7 +80,14 @@ export function getJsxExpressionBlockCommentLayout(text, comment, previousCommen
78
80
  }
79
81
  if (!hasExpressionBeforeComment) {
80
82
  if (isStandaloneBlockComment(text, comment)) {
81
- return { placement: 'inline' };
83
+ return {
84
+ contentColumn,
85
+ markerColumn,
86
+ multilineIndent: '',
87
+ placement: 'standalone',
88
+ preserveMultiline: getLineStart(text, comment.start) !== getLineStart(text, comment.end),
89
+ singleLineSuffixWidth: outputCommentLayout?.suffixWidth ?? 0,
90
+ };
82
91
  }
83
92
  const expressionStart = skipWhitespace(text, comment.end);
84
93
  return {
@@ -103,15 +112,3 @@ function getJsxExpressionContainerOutputColumn(text, container, tabWidth) {
103
112
  const lineIndent = getLeadingIndent(linePrefix);
104
113
  return getColumns(lineIndent, tabWidth) + tabWidth;
105
114
  }
106
- function getSmallestContainingRange(comment, ranges) {
107
- let containingRange;
108
- for (const range of ranges) {
109
- if (comment.start <= range.start || comment.end >= range.end) {
110
- continue;
111
- }
112
- if (containingRange === undefined || range.end - range.start < containingRange.end - containingRange.start) {
113
- containingRange = range;
114
- }
115
- }
116
- return containingRange;
117
- }
@@ -1,8 +1,9 @@
1
1
  import type { CommentEntry, CommentRange } from './comment-ranges.js';
2
2
  import type { SourceRange } from '../utils/ast.js';
3
3
  export declare function neutralizePrettierIgnoreForIgnoredComments<T>(text: string, ast: T): T;
4
+ export declare function getNeutralizedPrettierIgnoreOriginalText(comment: unknown): string | undefined;
4
5
  export declare function collectPrettierIgnoredLineRanges(text: string, ast: unknown, comments: CommentEntry[]): SourceRange[];
5
- export declare function isCommentInIgnoredLineRange(comment: CommentRange, ignoredLineRanges: SourceRange[]): boolean;
6
+ export declare function isCommentInIgnoredLineRange(comment: CommentRange, ignoredLineRange: SourceRange | undefined): boolean;
6
7
  export declare function isPrettierIgnoredBlockComment(text: string, comments: CommentEntry[], index: number): boolean;
7
8
  export declare function isPrettierIgnoredStandaloneLineComment(text: string, comments: CommentEntry[], index: number): boolean;
8
9
  export declare function isPrettierIgnoredTrailingLineComment(text: string, comments: CommentEntry[], index: number): boolean;
@@ -3,10 +3,12 @@ import { isPrettierIgnoreComment } from './comment-directives.js';
3
3
  import { shouldSkipBlockComment, shouldSkipLineComment } from './comment-eligibility.js';
4
4
  import { areCommentsOnAdjacentLines, isCommentAdjacentBeforeIndex, isStandaloneBlockComment, isStandaloneComment, isStandaloneLineComment, } from './comment-location.js';
5
5
  import { collectCommentEntries } from './comment-ranges.js';
6
- import { getAstNodeRange, visitAstNodes } from '../utils/ast.js';
6
+ import { collectAstNodeRangesByStart, getAstNodeRange, visitAstNodes } from '../utils/ast.js';
7
7
  import { getLineEnd, getLineStart } from '../utils/source-lines.js';
8
+ import { isRecord } from '../utils/type-guards.js';
8
9
  import { skipWhitespace } from '../utils/whitespace.js';
9
10
  const NEUTRALIZED_PRETTIER_IGNORE_COMMENT = 'prettier-ignore wrap-comments';
11
+ const neutralizedPrettierIgnoreOriginalTextKey = Symbol('neutralizedPrettierIgnoreOriginalText');
10
12
  export function neutralizePrettierIgnoreForIgnoredComments(text, ast) {
11
13
  const comments = collectCommentEntries(ast, text);
12
14
  for (let index = 0; index < comments.length; index += 1) {
@@ -17,38 +19,55 @@ export function neutralizePrettierIgnoreForIgnoredComments(text, ast) {
17
19
  ((isPrettierIgnoredBlockComment(text, comments, index) && !shouldSkipBlockComment(text, entry.range)) ||
18
20
  (isPrettierIgnoredStandaloneLineComment(text, comments, index) && !shouldSkipLineComment(text, entry.range)));
19
21
  if (shouldNeutralize) {
22
+ if (previousEntry.range.kind === 'block') {
23
+ const originalText = text.slice(previousEntry.range.start, previousEntry.range.end);
24
+ previousEntry.raw[neutralizedPrettierIgnoreOriginalTextKey] =
25
+ originalText;
26
+ }
20
27
  previousEntry.raw.value = NEUTRALIZED_PRETTIER_IGNORE_COMMENT;
21
28
  }
22
29
  }
23
30
  return ast;
24
31
  }
32
+ export function getNeutralizedPrettierIgnoreOriginalText(comment) {
33
+ if (!isRecord(comment)) {
34
+ return undefined;
35
+ }
36
+ const originalText = comment[neutralizedPrettierIgnoreOriginalTextKey];
37
+ return typeof originalText === 'string' ? originalText : undefined;
38
+ }
25
39
  export function collectPrettierIgnoredLineRanges(text, ast, comments) {
26
- const nodeRanges = collectAstNodeRanges(ast);
40
+ const nodeRangesByStart = collectAstNodeRangesByStart(ast);
41
+ const jsxTargetRangesByIgnoreStart = collectJsxPrettierIgnoreTargetRanges(text, ast, comments);
27
42
  const ignoredLineRanges = [];
28
43
  for (let index = 0; index < comments.length; index += 1) {
29
44
  const comment = comments[index]?.range;
45
+ const jsxTargetRange = comment === undefined ? undefined : jsxTargetRangesByIgnoreStart.get(comment.start);
30
46
  if (comment === undefined ||
31
- !isStandaloneComment(text, comment) ||
47
+ (!isStandaloneComment(text, comment) && jsxTargetRange === undefined) ||
32
48
  !isPrettierIgnoreComment(getCommentBody(text, comment))) {
33
49
  continue;
34
50
  }
35
- const targetStart = getPrettierIgnoreTargetStart(text, comments, index);
36
- if (targetStart === undefined) {
37
- continue;
51
+ let targetRange = jsxTargetRange;
52
+ if (targetRange === undefined) {
53
+ const targetStart = getPrettierIgnoreTargetStart(text, comments, index);
54
+ if (targetStart === undefined) {
55
+ continue;
56
+ }
57
+ targetRange = nodeRangesByStart.get(targetStart);
38
58
  }
39
- const targetRange = nodeRanges.find((range) => range.start === targetStart);
40
59
  if (targetRange === undefined) {
41
60
  continue;
42
61
  }
43
- ignoredLineRanges.push({
62
+ appendMergedRange(ignoredLineRanges, {
44
63
  end: getLineEnd(text, targetRange.end),
45
64
  start: getLineStart(text, targetRange.start),
46
65
  });
47
66
  }
48
67
  return ignoredLineRanges;
49
68
  }
50
- export function isCommentInIgnoredLineRange(comment, ignoredLineRanges) {
51
- return ignoredLineRanges.some((range) => comment.start >= range.start && comment.start < range.end);
69
+ export function isCommentInIgnoredLineRange(comment, ignoredLineRange) {
70
+ return (ignoredLineRange !== undefined && comment.start >= ignoredLineRange.start && comment.start < ignoredLineRange.end);
52
71
  }
53
72
  export function isPrettierIgnoredBlockComment(text, comments, index) {
54
73
  const comment = comments[index]?.range;
@@ -107,15 +126,82 @@ export function isPrettierIgnoredTrailingLineComment(text, comments, index) {
107
126
  }
108
127
  return false;
109
128
  }
110
- function collectAstNodeRanges(ast) {
111
- const ranges = [];
129
+ function collectJsxPrettierIgnoreTargetRanges(text, ast, comments) {
130
+ const commentsByStart = new Map(comments.map((entry) => [entry.range.start, entry.range]));
131
+ const targetRangesByIgnoreStart = new Map();
112
132
  visitAstNodes(ast, (node) => {
113
- const range = getAstNodeRange(node);
114
- if (range !== undefined) {
115
- ranges.push(range);
133
+ if (!isJsxElementOrFragment(node)) {
134
+ return;
135
+ }
136
+ const children = node['children'];
137
+ if (!Array.isArray(children)) {
138
+ return;
139
+ }
140
+ for (let ignoreIndex = 0; ignoreIndex < children.length; ignoreIndex += 1) {
141
+ const ignoreContainer = children[ignoreIndex];
142
+ if (!isRecord(ignoreContainer)) {
143
+ continue;
144
+ }
145
+ const ignoreComment = getCommentOnlyJsxExpressionContainerComment(text, ignoreContainer, commentsByStart);
146
+ if (ignoreComment === undefined || !isPrettierIgnoreComment(getCommentBody(text, ignoreComment))) {
147
+ continue;
148
+ }
149
+ const target = getNextSignificantJsxChild(text, children, ignoreIndex);
150
+ if (target === undefined || !isJsxElementOrFragment(target)) {
151
+ continue;
152
+ }
153
+ const targetRange = getAstNodeRange(target);
154
+ if (targetRange !== undefined) {
155
+ targetRangesByIgnoreStart.set(ignoreComment.start, targetRange);
156
+ }
116
157
  }
117
158
  });
118
- return ranges.sort((left, right) => left.start - right.start || right.end - left.end);
159
+ return targetRangesByIgnoreStart;
160
+ }
161
+ function getNextSignificantJsxChild(text, children, ignoreIndex) {
162
+ for (let index = ignoreIndex + 1; index < children.length; index += 1) {
163
+ const child = children[index];
164
+ if (!isRecord(child)) {
165
+ return undefined;
166
+ }
167
+ if (isMultilineWhitespaceJsxText(text, child)) {
168
+ continue;
169
+ }
170
+ return child;
171
+ }
172
+ return undefined;
173
+ }
174
+ function isMultilineWhitespaceJsxText(text, node) {
175
+ if (node['type'] !== 'JSXText') {
176
+ return false;
177
+ }
178
+ const range = getAstNodeRange(node);
179
+ if (range === undefined) {
180
+ return false;
181
+ }
182
+ const raw = text.slice(range.start, range.end);
183
+ return raw.includes('\n') && /^[ \t\r\n]*$/u.test(raw);
184
+ }
185
+ function getCommentOnlyJsxExpressionContainerComment(text, container, commentsByStart) {
186
+ const expression = container['expression'];
187
+ if (container['type'] !== 'JSXExpressionContainer' ||
188
+ !isRecord(expression) ||
189
+ expression['type'] !== 'JSXEmptyExpression') {
190
+ return undefined;
191
+ }
192
+ const range = getAstNodeRange(container);
193
+ if (range === undefined || text[range.start] !== '{' || text[range.end - 1] !== '}') {
194
+ return undefined;
195
+ }
196
+ const commentStart = skipWhitespace(text, range.start + 1);
197
+ const comment = commentsByStart.get(commentStart);
198
+ if (comment === undefined || comment.kind !== 'block' || skipWhitespace(text, comment.end) !== range.end - 1) {
199
+ return undefined;
200
+ }
201
+ return comment;
202
+ }
203
+ function isJsxElementOrFragment(node) {
204
+ return node['type'] === 'JSXElement' || node['type'] === 'JSXFragment';
119
205
  }
120
206
  function getPrettierIgnoreTargetStart(text, comments, ignoreCommentIndex) {
121
207
  const ignoreComment = comments[ignoreCommentIndex]?.range;
@@ -143,3 +229,11 @@ function isSkippableCommentBetweenIgnoreAndTarget(text, comment) {
143
229
  }
144
230
  return shouldSkipLineComment(text, comment) && !isPrettierIgnoreComment(getCommentBody(text, comment));
145
231
  }
232
+ function appendMergedRange(ranges, range) {
233
+ const previousRange = ranges[ranges.length - 1];
234
+ if (previousRange === undefined || range.start > previousRange.end) {
235
+ ranges.push(range);
236
+ return;
237
+ }
238
+ previousRange.end = Math.max(previousRange.end, range.end);
239
+ }
@@ -6,6 +6,7 @@ export type PrinterLayoutSource = {
6
6
  };
7
7
  export type PrinterCommentLayout = {
8
8
  lineIndentColumn: number;
9
+ lineStart: number;
9
10
  lineWidth: number;
10
11
  markerColumn: number;
11
12
  suffixWidth: number;
@@ -57,6 +57,7 @@ function getPrinterCommentLayout(text, comment, tabWidth) {
57
57
  const suffix = text.slice(comment.end, lineEnd).replace(/[ \t]+$/u, '');
58
58
  return {
59
59
  lineIndentColumn: getColumns(lineIndent, tabWidth),
60
+ lineStart,
60
61
  lineWidth: getColumns(lineText, tabWidth),
61
62
  markerColumn: getColumns(linePrefix, tabWidth),
62
63
  suffixWidth: getColumns(suffix, tabWidth),
@@ -10,6 +10,7 @@ export type BlockCommentLayout = {
10
10
  markerColumn?: number;
11
11
  multilineIndent?: string;
12
12
  placement: 'inline' | 'standalone' | 'trailing';
13
+ preserveMultiline?: boolean;
13
14
  singleLineSuffixWidth?: number;
14
15
  trailingMove?: {
15
16
  insertAt: number;