@aforemendude/prettier-plugin-wrap-comments 1.1.0 → 1.1.1

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
@@ -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,6 +31,31 @@ Then run Prettier normally:
30
31
  npx prettier --write .
31
32
  ```
32
33
 
34
+ ### Cache Repeated CLI Runs
35
+
36
+ Prettier's CLI cache skips files that have not changed since a successful formatting pass. Enable it in package scripts
37
+ so contributors use it consistently. This repository uses the faster `metadata` strategy for local formatting:
38
+
39
+ ```json
40
+ {
41
+ "scripts": {
42
+ "format": "prettier --write --cache --cache-strategy metadata .",
43
+ "format:check": "prettier --check --cache --cache-strategy metadata .",
44
+ "format:nocache": "prettier --write ."
45
+ }
46
+ }
47
+ ```
48
+
49
+ The first cached run still processes every file; later runs skip files whose relevant metadata and other cache keys have
50
+ not changed. Omit `--cache-strategy metadata` to use the default `content` strategy when workflows such as Git
51
+ operations frequently change timestamps without changing file contents.
52
+
53
+ By default, Prettier stores the cache under `node_modules/.cache/prettier/`, which is normally excluded from version
54
+ control with `node_modules`. Prettier does not include plugin versions or implementations in its cache keys, so run the
55
+ uncached command once after updating this plugin or another Prettier plugin. Running Prettier without `--cache`, as the
56
+ `format:nocache` script does, also removes the default cache. See [Prettier's CLI cache documentation][prettier-cache]
57
+ for cache keys, strategies, and custom cache locations.
58
+
33
59
  ## Behavior
34
60
 
35
61
  The plugin wraps comments for Prettier's `babel`, `babel-ts`, and `typescript` parsers. It runs during parser
@@ -148,10 +174,13 @@ The plugin leaves these comments unchanged:
148
174
  - JSDoc comments that start with `/**`
149
175
  - bang-preserved comments that start with `/*!` or `//!`
150
176
  - TypeScript-style triple-slash line comments that start with `///`
177
+ - Flow type annotations and includes that start with `/*:`, `/*::`, or `/*flow-include`, including Flow's supported
178
+ spaces or tabs before the marker
151
179
  - empty comment bodies
152
180
  - `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
181
+ - other directive comments such as `@license`, `@preserve`, JSX, TypeScript, and Flow pragmas, Flow error suppressions,
182
+ `flowlint` comments, source map directives, `#__PURE__`, `@__PURE__`, exact Node test coverage controls, other
183
+ lint/coverage/formatter directives, `vite-ignore`, and webpack magic comments
155
184
 
156
185
  ## Supported Parsers
157
186
 
@@ -159,6 +188,28 @@ The plugin leaves these comments unchanged:
159
188
  - `babel-ts`
160
189
  - `typescript`
161
190
 
191
+ ## Performance
192
+
193
+ `npm run benchmark` compares uncached, in-memory `prettier.format()` calls with and without the plugin across five
194
+ generated files. Lower times are better. The plugin/plain column divides the plugin mean by the plain Prettier mean.
195
+
196
+ These results are from a representative run on August 8, 2026, using Linux 6.8, an Intel Core i7-10750H, Node.js
197
+ 24.18.0, Prettier 3.9.6, and Vitest 4.1.10. The suite was configured with `time: 500`, `iterations: 10`,
198
+ `warmupTime: 100`, and `warmupIterations: 2` for each case.
199
+
200
+ | Generated workload | Characters | Plain Prettier mean | Plugin mean | Plugin/plain |
201
+ | ------------------------------------------ | ---------: | ------------------: | -----------------: | -----------: |
202
+ | Comment-free JavaScript | 29,995 | 54.78 ms (±10.09%) | 45.59 ms (±5.82%) | 0.83× |
203
+ | Code-heavy JavaScript with sparse comments | 30,494 | 44.05 ms (±3.22%) | 135.66 ms (±3.81%) | 3.08× |
204
+ | Comment-heavy TypeScript | 64,432 | 56.75 ms (±11.82%) | 327.61 ms (±2.67%) | 5.77× |
205
+ | Preformatted comment-heavy TypeScript | 54,467 | 54.54 ms (±2.47%) | 279.65 ms (±2.50%) | 5.13× |
206
+ | JSX-comment-heavy TSX | 40,575 | 57.52 ms (±6.87%) | 263.67 ms (±3.83%) | 4.58× |
207
+
208
+ The comment-free case takes the plugin's early exit after scanning for comment delimiters. Its apparent speedup should
209
+ be treated as benchmark variation, not as an expected optimization over plain Prettier. In this run, sparse comments
210
+ took about 3.08× the plain formatting time, while comment-heavy inputs took 4.58–5.77×. These measurements isolate
211
+ formatter work on changed files; enabling the CLI cache above skips that work for unchanged files.
212
+
162
213
  ## Development
163
214
 
164
215
  Source files are organized by responsibility. `src/plugin/` contains parser and printer integration,
@@ -168,6 +219,7 @@ concerns and source file names wherever practical.
168
219
 
169
220
  ```sh
170
221
  npm install
222
+ npm run benchmark
171
223
  npm run format:check
172
224
  npm run typecheck
173
225
  npm run test
@@ -177,3 +229,10 @@ npm run build
177
229
  `npm run test` runs the TypeScript unit and fixture-based integration suites with Vitest. Use `npm run test:unit` or
178
230
  `npm run test:integration` to run one suite. `npm run build` removes and recreates `dist` using a cross-platform Node
179
231
  cleanup script, and `npm run verify` runs formatting, type checking, the build, and both test suites.
232
+
233
+ `npm run benchmark` compares plain Prettier with Prettier using the plugin. Its JavaScript, TypeScript, and TSX inputs
234
+ are generated in memory by the files under `test/benchmark`, so large benchmark fixtures are not stored in the
235
+ repository. Benchmarks use Vitest's separate benchmark mode and do not run as part of `npm run test` or
236
+ `npm run verify`.
237
+
238
+ [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;
@@ -37,7 +37,9 @@ function buildBlockReplacement(text, comment, formattedLines, options, layout) {
37
37
  const singleLine = `/* ${formattedLines.join(' ')} */`;
38
38
  const singleLineWidth = getColumns(singleLine, tabWidth);
39
39
  const singleLineSuffixWidth = layout.singleLineSuffixWidth ?? 0;
40
- if (formattedLines.length === 1 && markerColumn + singleLineWidth + singleLineSuffixWidth <= getPrintWidth(options)) {
40
+ if (layout.preserveMultiline !== true &&
41
+ formattedLines.length === 1 &&
42
+ markerColumn + singleLineWidth + singleLineSuffixWidth <= getPrintWidth(options)) {
41
43
  return singleLine;
42
44
  }
43
45
  if (layout.placement === 'inline') {
@@ -1,7 +1,7 @@
1
1
  import { shouldSkipLineComment } from './comment-eligibility.js';
2
2
  import { isStandaloneBlockComment, isStandaloneLineComment } from './comment-location.js';
3
3
  import { collectCommentEntries } from './comment-ranges.js';
4
- import { collectEmbeddedExpressionRanges, doesBlockCommentSeparateEmbeddedTrailingLineComment, getEmbeddedTrailingLineCommentMove, isCommentInEmbeddedExpression, } from './embedded-expression-ranges.js';
4
+ import { collectEmbeddedExpressionRanges, doesBlockCommentSeparateEmbeddedTrailingLineComment, getEmbeddedTrailingLineCommentMove, } from './embedded-expression-ranges.js';
5
5
  import { collectJsxExpressionContainerRanges, getJsxExpressionBlockCommentLayout } from './jsx-expression-layout.js';
6
6
  import { collectLineCommentGroup } from './line-comment-groups.js';
7
7
  import { collectPrettierIgnoredLineRanges, isCommentInIgnoredLineRange, isPrettierIgnoredBlockComment, isPrettierIgnoredStandaloneLineComment, isPrettierIgnoredTrailingLineComment, } from './prettier-ignore.js';
@@ -9,7 +9,10 @@ import { getPrinterLayout } from './printer-layout.js';
9
9
  import { wrapBlockComment } from './wrap-block-comment.js';
10
10
  import { wrapLineCommentGroup } from './wrap-line-comment-group.js';
11
11
  import { wrapTrailingLineComment } from './wrap-trailing-line-comment.js';
12
+ import { matchOrderedRangesToSmallestContainers } from '../utils/ast.js';
13
+ import { getColumnAt, getColumns } from '../utils/display-width.js';
12
14
  import { applyReplacements } from '../utils/replacements.js';
15
+ import { getLineEnd, getLineStart } from '../utils/source-lines.js';
13
16
  import { getTabWidth } from '../utils/wrap-options.js';
14
17
  export async function wrapComments(text, ast, options, printerLayoutSource) {
15
18
  return (await wrapCommentsWithMetadata(text, ast, options, printerLayoutSource)).text;
@@ -20,6 +23,8 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
20
23
  const embeddedExpressionRanges = collectEmbeddedExpressionRanges(ast);
21
24
  const jsxExpressionContainers = collectJsxExpressionContainerRanges(ast);
22
25
  const ignoredLineRanges = collectPrettierIgnoredLineRanges(text, ast, commentEntries);
26
+ const embeddedExpressionRangeMatches = matchOrderedRangesToSmallestContainers(comments, embeddedExpressionRanges);
27
+ const jsxExpressionContainerMatches = matchOrderedRangesToSmallestContainers(comments, jsxExpressionContainers);
23
28
  if (comments.length === 0) {
24
29
  return { jsxBlockCommentRewrites: [], text };
25
30
  }
@@ -27,21 +32,34 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
27
32
  const jsxBlockCommentRewrites = [];
28
33
  const tabWidth = getTabWidth(options);
29
34
  const printerLayout = getPrinterLayout(text, commentEntries, jsxExpressionContainers, printerLayoutSource, tabWidth);
35
+ const sameLineWidthDeltas = {
36
+ output: new Map(),
37
+ source: new Map(),
38
+ };
30
39
  let blockCommentIndex = -1;
40
+ let ignoredLineRangeIndex = 0;
31
41
  for (let index = 0; index < comments.length; index += 1) {
32
42
  const comment = comments[index];
33
43
  const outputCommentLayout = printerLayout.comments[index];
34
44
  if (comment?.kind === 'block') {
35
45
  blockCommentIndex += 1;
36
46
  }
37
- if (comment === undefined || isCommentInIgnoredLineRange(comment, ignoredLineRanges)) {
47
+ if (comment === undefined) {
48
+ continue;
49
+ }
50
+ let ignoredLineRange = ignoredLineRanges[ignoredLineRangeIndex];
51
+ while (ignoredLineRange !== undefined && ignoredLineRange.end <= comment.start) {
52
+ ignoredLineRangeIndex += 1;
53
+ ignoredLineRange = ignoredLineRanges[ignoredLineRangeIndex];
54
+ }
55
+ if (isCommentInIgnoredLineRange(comment, ignoredLineRange)) {
38
56
  continue;
39
57
  }
40
58
  if (comment.kind === 'block') {
41
59
  if (isPrettierIgnoredBlockComment(text, commentEntries, index)) {
42
60
  continue;
43
61
  }
44
- const jsxLayout = getJsxExpressionBlockCommentLayout(text, comment, comments[index - 1], jsxExpressionContainers, tabWidth, outputCommentLayout, printerLayout.jsxCommentMarkerColumns);
62
+ const jsxLayout = getJsxExpressionBlockCommentLayout(text, comment, comments[index - 1], jsxExpressionContainerMatches[index], tabWidth, outputCommentLayout, printerLayout.jsxCommentMarkerColumns);
45
63
  if (jsxLayout?.placement === 'inline') {
46
64
  continue;
47
65
  }
@@ -52,7 +70,7 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
52
70
  placement: isStandaloneBlockComment(text, comment) ? 'standalone' : 'inline',
53
71
  };
54
72
  const replacement = await wrapBlockComment(text, comment, options, jsxLayout ?? outputLayout);
55
- const preservesEmbeddedCommentOrder = doesBlockCommentSeparateEmbeddedTrailingLineComment(text, comment, comments[index + 1], embeddedExpressionRanges);
73
+ const preservesEmbeddedCommentOrder = doesBlockCommentSeparateEmbeddedTrailingLineComment(text, comment, comments[index + 1], embeddedExpressionRangeMatches[index]?.range, embeddedExpressionRangeMatches[index + 1]?.range);
56
74
  if (Array.isArray(replacement)) {
57
75
  if (!preservesEmbeddedCommentOrder) {
58
76
  replacements.push(...replacement);
@@ -60,6 +78,7 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
60
78
  }
61
79
  else if (replacement !== undefined) {
62
80
  replacements.push(replacement);
81
+ recordSameLineReplacementWidthDelta(text, comment, replacement, outputCommentLayout, tabWidth, sameLineWidthDeltas);
63
82
  if (jsxLayout !== undefined) {
64
83
  jsxBlockCommentRewrites.push({ blockCommentIndex, text: replacement.text });
65
84
  }
@@ -77,11 +96,13 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
77
96
  if (isPrettierIgnoredTrailingLineComment(text, commentEntries, index)) {
78
97
  continue;
79
98
  }
80
- const embeddedMove = getEmbeddedTrailingLineCommentMove(text, comment, embeddedExpressionRanges);
81
- if (embeddedMove === undefined && isCommentInEmbeddedExpression(comment, embeddedExpressionRanges)) {
99
+ const embeddedExpressionRange = embeddedExpressionRangeMatches[index]?.range;
100
+ const embeddedMove = getEmbeddedTrailingLineCommentMove(text, comment, embeddedExpressionRange);
101
+ if (embeddedMove === undefined && embeddedExpressionRange !== undefined) {
82
102
  continue;
83
103
  }
84
- const replacement = await wrapTrailingLineComment(text, comment, options, outputCommentLayout, embeddedMove);
104
+ const trailingLayout = getTrailingLineCommentLayout(text, comment, outputCommentLayout, tabWidth, sameLineWidthDeltas);
105
+ const replacement = await wrapTrailingLineComment(text, comment, options, trailingLayout, embeddedMove);
85
106
  if (replacement !== undefined) {
86
107
  replacements.push(...replacement);
87
108
  }
@@ -99,3 +120,50 @@ export async function wrapCommentsWithMetadata(text, ast, options, printerLayout
99
120
  text: applyReplacements(text, replacements),
100
121
  };
101
122
  }
123
+ function recordSameLineReplacementWidthDelta(text, comment, replacement, outputLayout, tabWidth, sameLineWidthDeltas) {
124
+ const original = text.slice(comment.start, comment.end);
125
+ if (containsLineTerminator(original) || containsLineTerminator(replacement.text)) {
126
+ return;
127
+ }
128
+ const sourceLineStart = getLineStart(text, comment.start);
129
+ const sourceWidthDelta = sameLineWidthDeltas.source.get(sourceLineStart) ?? 0;
130
+ const sourceMarkerColumn = getColumnAt(text, comment.start, tabWidth) + sourceWidthDelta;
131
+ const sourceReplacementWidthDelta = getReplacementWidthDelta(original, replacement.text, sourceMarkerColumn, tabWidth);
132
+ addLineWidthDelta(sameLineWidthDeltas.source, sourceLineStart, sourceReplacementWidthDelta);
133
+ if (outputLayout === undefined) {
134
+ return;
135
+ }
136
+ const outputWidthDelta = sameLineWidthDeltas.output.get(outputLayout.lineStart) ?? 0;
137
+ const outputMarkerColumn = outputLayout.markerColumn + outputWidthDelta;
138
+ const outputReplacementWidthDelta = getReplacementWidthDelta(original, replacement.text, outputMarkerColumn, tabWidth);
139
+ addLineWidthDelta(sameLineWidthDeltas.output, outputLayout.lineStart, outputReplacementWidthDelta);
140
+ }
141
+ function getTrailingLineCommentLayout(text, comment, outputLayout, tabWidth, sameLineWidthDeltas) {
142
+ if (outputLayout !== undefined) {
143
+ const lineWidthDelta = sameLineWidthDeltas.output.get(outputLayout.lineStart) ?? 0;
144
+ return lineWidthDelta === 0
145
+ ? outputLayout
146
+ : { ...outputLayout, lineWidth: outputLayout.lineWidth + lineWidthDelta };
147
+ }
148
+ const lineStart = getLineStart(text, comment.start);
149
+ const lineWidthDelta = sameLineWidthDeltas.source.get(lineStart) ?? 0;
150
+ if (lineWidthDelta === 0) {
151
+ return undefined;
152
+ }
153
+ const lineEnd = getLineEnd(text, comment.start);
154
+ const lineText = text.slice(lineStart, lineEnd).replace(/[ \t]+$/u, '');
155
+ return { lineWidth: getColumns(lineText, tabWidth) + lineWidthDelta };
156
+ }
157
+ function getReplacementWidthDelta(original, replacement, markerColumn, tabWidth) {
158
+ const originalWidth = getColumns(original, tabWidth, markerColumn);
159
+ const replacementWidth = getColumns(replacement, tabWidth, markerColumn);
160
+ return replacementWidth - originalWidth;
161
+ }
162
+ function addLineWidthDelta(lineWidthDeltas, lineStart, delta) {
163
+ if (delta !== 0) {
164
+ lineWidthDeltas.set(lineStart, (lineWidthDeltas.get(lineStart) ?? 0) + delta);
165
+ }
166
+ }
167
+ function containsLineTerminator(text) {
168
+ return /[\r\n\u2028\u2029]/u.test(text);
169
+ }
@@ -3,7 +3,7 @@ import type { EmbeddedTrailingLineCommentMove } from './embedded-expression-rang
3
3
  import type { Replacement } from '../utils/replacements.js';
4
4
  import type { WrapOptions } from '../utils/wrap-options.js';
5
5
  export type TrailingLineCommentLayout = {
6
- lineIndentColumn: number;
6
+ lineIndentColumn?: number;
7
7
  lineWidth: number;
8
8
  };
9
9
  export declare function wrapTrailingLineComment(text: string, comment: CommentRange, options: WrapOptions, outputLayout?: TrailingLineCommentLayout, move?: EmbeddedTrailingLineCommentMove): Promise<Replacement[] | undefined>;
@@ -32,9 +32,12 @@ function createWrappedParser(parserName, parser) {
32
32
  if (hasOffsetSensitiveFormatting(text, options)) {
33
33
  return preprocessed;
34
34
  }
35
+ if (!preprocessed.includes('//') && !preprocessed.includes('/*')) {
36
+ return preprocessed;
37
+ }
35
38
  let ast;
36
39
  try {
37
- ast = await parser.parse(preprocessed, options);
40
+ ast = await parser.parse(preprocessed, { ...options, parser: parserName });
38
41
  }
39
42
  catch {
40
43
  return preprocessed;
@@ -1,12 +1,18 @@
1
1
  import { doc } from 'prettier';
2
2
  import * as estreePlugin from 'prettier/plugins/estree';
3
+ import { getNeutralizedPrettierIgnoreOriginalText } from '../comments/prettier-ignore.js';
4
+ import { normalizeLineTerminators } from '../utils/source-lines.js';
3
5
  import { isRecord } from '../utils/type-guards.js';
4
6
  import { isRewrittenJsxBlockComment } from './jsx-comment-rewrite-metadata.js';
5
7
  const { hardline, indent } = doc.builders;
8
+ const { mapDoc, replaceEndOfLine } = doc.utils;
6
9
  export function createPrinters() {
7
10
  const estreePrinter = estreePlugin.printers.estree;
11
+ const estreePrintComment = estreePrinter.printComment;
12
+ if (estreePrintComment === undefined) {
13
+ throw new Error('Expected the native estree printer to provide printComment');
14
+ }
8
15
  return {
9
- ...estreePlugin.printers,
10
16
  estree: {
11
17
  ...estreePrinter,
12
18
  print(path, options, print, args) {
@@ -15,6 +21,21 @@ export function createPrinters() {
15
21
  }
16
22
  return estreePrinter.print(path, options, print, args);
17
23
  },
24
+ printComment(path, options) {
25
+ const originalText = getNeutralizedPrettierIgnoreOriginalText(path.node);
26
+ if (originalText !== undefined) {
27
+ const normalizedOriginalText = normalizeLineTerminators(originalText);
28
+ return normalizedOriginalText.includes('\n')
29
+ ? replaceEndOfLine(normalizedOriginalText)
30
+ : normalizedOriginalText;
31
+ }
32
+ if (isRewrittenMultilineBlockComment(path.node)) {
33
+ const printedComment = estreePrintComment(path, options);
34
+ const normalizedComment = mapDoc(printedComment, (currentDoc) => typeof currentDoc === 'string' ? normalizeLineTerminators(currentDoc) : currentDoc);
35
+ return replaceEndOfLine(normalizedComment, hardline);
36
+ }
37
+ return estreePrintComment(path, options);
38
+ },
18
39
  },
19
40
  };
20
41
  }
@@ -34,5 +55,5 @@ function isRewrittenMultilineBlockComment(comment) {
34
55
  return false;
35
56
  }
36
57
  const value = comment['value'];
37
- return typeof value === 'string' && value.includes('\n') && isRewrittenJsxBlockComment(comment);
58
+ return typeof value === 'string' && /[\r\n\u2028\u2029]/u.test(value) && isRewrittenJsxBlockComment(comment);
38
59
  }
@@ -1,8 +1,16 @@
1
1
  import { format } from 'prettier';
2
+ import { neutralizePrettierIgnoreForIgnoredComments } from '../comments/prettier-ignore.js';
2
3
  import { createPrinters } from './create-printers.js';
3
4
  export async function getPrinterLayoutSource(text, ast, parserName, parser, options) {
5
+ const printerLayoutParser = {
6
+ ...parser,
7
+ async parse(source, parserOptions) {
8
+ const printerLayoutAst = await parser.parse(source, parserOptions);
9
+ return neutralizePrettierIgnoreForIgnoredComments(source, printerLayoutAst);
10
+ },
11
+ };
4
12
  const printerLayoutPlugin = {
5
- parsers: { [parserName]: parser },
13
+ parsers: { [parserName]: printerLayoutParser },
6
14
  printers: createPrinters(),
7
15
  };
8
16
  try {
@@ -15,7 +23,7 @@ export async function getPrinterLayoutSource(text, ast, parserName, parser, opti
15
23
  if (formattedText === text) {
16
24
  return { ast, text };
17
25
  }
18
- const formattedAst = await parser.parse(formattedText, options);
26
+ const formattedAst = await parser.parse(formattedText, { ...options, parser: parserName });
19
27
  return {
20
28
  ast: formattedAst,
21
29
  text: formattedText,
@@ -2,5 +2,11 @@ export type SourceRange = {
2
2
  end: number;
3
3
  start: number;
4
4
  };
5
+ export type ContainingRangeMatch<Range extends SourceRange> = {
6
+ index: number;
7
+ range: Range;
8
+ };
5
9
  export declare function visitAstNodes(ast: unknown, visitor: (node: Record<string, unknown>) => void): void;
6
10
  export declare function getAstNodeRange(node: Record<string, unknown>): SourceRange | undefined;
11
+ export declare function collectAstNodeRangesByStart(ast: unknown): Map<number, SourceRange>;
12
+ export declare function matchOrderedRangesToSmallestContainers<Range extends SourceRange>(targets: readonly SourceRange[], containers: readonly Range[]): Array<ContainingRangeMatch<Range> | undefined>;
package/dist/utils/ast.js CHANGED
@@ -44,6 +44,49 @@ export function getAstNodeRange(node) {
44
44
  }
45
45
  return { end, start };
46
46
  }
47
+ export function collectAstNodeRangesByStart(ast) {
48
+ const rangesByStart = new Map();
49
+ visitAstNodes(ast, (node) => {
50
+ const range = getAstNodeRange(node);
51
+ if (range === undefined) {
52
+ return;
53
+ }
54
+ const existingRange = rangesByStart.get(range.start);
55
+ if (existingRange === undefined || range.end > existingRange.end) {
56
+ rangesByStart.set(range.start, range);
57
+ }
58
+ });
59
+ return rangesByStart;
60
+ }
61
+ export function matchOrderedRangesToSmallestContainers(targets, containers) {
62
+ const matches = [];
63
+ const openContainers = [];
64
+ let containerIndex = 0;
65
+ for (const target of targets) {
66
+ let container = containers[containerIndex];
67
+ while (container !== undefined && container.start < target.start) {
68
+ let previousContainer = openContainers[openContainers.length - 1];
69
+ while (previousContainer !== undefined && previousContainer.range.end <= container.start) {
70
+ openContainers.pop();
71
+ previousContainer = openContainers[openContainers.length - 1];
72
+ }
73
+ if (previousContainer === undefined ||
74
+ previousContainer.range.start !== container.start ||
75
+ previousContainer.range.end !== container.end) {
76
+ openContainers.push({ index: containerIndex, range: container });
77
+ }
78
+ containerIndex += 1;
79
+ container = containers[containerIndex];
80
+ }
81
+ let containingRange = openContainers[openContainers.length - 1];
82
+ while (containingRange !== undefined && containingRange.range.end <= target.end) {
83
+ openContainers.pop();
84
+ containingRange = openContainers[openContainers.length - 1];
85
+ }
86
+ matches.push(containingRange);
87
+ }
88
+ return matches;
89
+ }
47
90
  function getRangeNumber(range, index) {
48
91
  if (!Array.isArray(range)) {
49
92
  return undefined;
@@ -1,2 +1,2 @@
1
1
  export declare function getColumnAt(text: string, index: number, tabWidth: number): number;
2
- export declare function getColumns(text: string, tabWidth: number): number;
2
+ export declare function getColumns(text: string, tabWidth: number, startColumn?: number): number;
@@ -3,8 +3,8 @@ import { getLineStart } from './source-lines.js';
3
3
  export function getColumnAt(text, index, tabWidth) {
4
4
  return getColumns(text.slice(getLineStart(text, index), index), tabWidth);
5
5
  }
6
- export function getColumns(text, tabWidth) {
7
- let column = 0;
6
+ export function getColumns(text, tabWidth, startColumn = 0) {
7
+ let column = startColumn;
8
8
  let segmentStart = 0;
9
9
  for (let index = 0; index < text.length; index += 1) {
10
10
  if (text[index] === '\t') {
@@ -15,5 +15,5 @@ export function getColumns(text, tabWidth) {
15
15
  segmentStart = index + 1;
16
16
  }
17
17
  }
18
- return column + util.getStringWidth(text.slice(segmentStart));
18
+ return column + util.getStringWidth(text.slice(segmentStart)) - startColumn;
19
19
  }
@@ -1,22 +1,61 @@
1
1
  import { format } from 'prettier';
2
- import { isBlankLine } from './source-lines.js';
2
+ import { isBlankLine, normalizeLineTerminators } from './source-lines.js';
3
3
  import { getTabWidth } from './wrap-options.js';
4
+ const MAX_MARKDOWN_FORMAT_PASSES = 3;
4
5
  export async function formatMarkdownLines(markdown, printWidth, options) {
5
- const normalized = trimBlankEdges(markdown.replace(/\r\n?/g, '\n'));
6
+ const normalized = trimBlankEdges(normalizeLineTerminators(markdown));
7
+ let formatted;
6
8
  try {
7
- const formatted = await format(normalized, {
8
- endOfLine: 'lf',
9
- parser: 'markdown',
10
- printWidth,
11
- proseWrap: 'always',
12
- tabWidth: getTabWidth(options),
13
- useTabs: options.useTabs,
14
- });
15
- return formatted.replace(/\n$/, '').split('\n');
9
+ formatted = await formatMarkdown(normalized, printWidth, options);
16
10
  }
17
11
  catch {
18
12
  return normalized.split('\n');
19
13
  }
14
+ if (startsWithThematicRule(normalized)) {
15
+ formatted = await stabilizePotentialFrontMatter(formatted, printWidth, options);
16
+ }
17
+ return formatted.replace(/\n$/, '').split('\n');
18
+ }
19
+ async function formatMarkdown(markdown, printWidth, options) {
20
+ return format(markdown, {
21
+ endOfLine: 'lf',
22
+ parser: 'markdown',
23
+ printWidth,
24
+ proseWrap: 'always',
25
+ tabWidth: getTabWidth(options),
26
+ useTabs: options.useTabs,
27
+ });
28
+ }
29
+ async function stabilizePotentialFrontMatter(formatted, printWidth, options) {
30
+ const seen = new Set([formatted]);
31
+ let current = formatted;
32
+ for (let pass = 1; pass < MAX_MARKDOWN_FORMAT_PASSES && hasPotentialFrontMatterAmbiguity(current); pass += 1) {
33
+ let next;
34
+ try {
35
+ next = await formatMarkdown(current, printWidth, options);
36
+ }
37
+ catch {
38
+ return current;
39
+ }
40
+ if (next === current || seen.has(next)) {
41
+ return current;
42
+ }
43
+ seen.add(next);
44
+ current = next;
45
+ }
46
+ return current;
47
+ }
48
+ function startsWithThematicRule(markdown) {
49
+ const [firstLine = ''] = markdown.split('\n', 1);
50
+ return /^[ \t]{0,3}([*_-])(?:[ \t]*\1){2,}[ \t]*$/u.test(firstLine);
51
+ }
52
+ function hasPotentialFrontMatterAmbiguity(markdown) {
53
+ const lines = markdown.replace(/\n$/u, '').split('\n');
54
+ if (lines[0] !== '---') {
55
+ return false;
56
+ }
57
+ const closingDelimiterIndex = lines.indexOf('---', 1);
58
+ return closingDelimiterIndex > 1 && (isBlankLine(lines[1]) || isBlankLine(lines[closingDelimiterIndex - 1]));
20
59
  }
21
60
  function trimBlankEdges(markdown) {
22
61
  const lines = markdown.split('\n');
@@ -1,4 +1,5 @@
1
1
  import type { WrapOptions } from './wrap-options.js';
2
+ export declare function normalizeLineTerminators(text: string): string;
2
3
  export declare function getPreferredNewline(text: string, options: WrapOptions): string;
3
4
  export declare function getLinePrefix(text: string, index: number): string;
4
5
  export declare function getLineStart(text: string, index: number): number;
@@ -1,3 +1,6 @@
1
+ export function normalizeLineTerminators(text) {
2
+ return text.replace(/\r\n|[\r\u2028\u2029]/gu, '\n');
3
+ }
1
4
  export function getPreferredNewline(text, options) {
2
5
  if (options.endOfLine === 'crlf') {
3
6
  return '\r\n';
@@ -1,2 +1,3 @@
1
+ export declare function isEcmaScriptHorizontalWhitespace(character: string): boolean;
1
2
  export declare function skipWhitespace(text: string, index: number): number;
2
3
  export declare function trimWhitespaceEnd(text: string, start: number, end: number): number;
@@ -1,3 +1,7 @@
1
+ const ECMASCRIPT_HORIZONTAL_WHITESPACE_PATTERN = /^[\t\u000b\u000c \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]$/u;
2
+ export function isEcmaScriptHorizontalWhitespace(character) {
3
+ return ECMASCRIPT_HORIZONTAL_WHITESPACE_PATTERN.test(character);
4
+ }
1
5
  export function skipWhitespace(text, index) {
2
6
  let cursor = index;
3
7
  while (cursor < text.length) {
package/package.json CHANGED
@@ -9,9 +9,15 @@
9
9
  "prettier": "3.9.6",
10
10
  "prettier-plugin-wrap-comments-stable": "npm:@aforemendude/prettier-plugin-wrap-comments@1.0.6",
11
11
  "typescript": "7.0.2",
12
- "vite": "8.1.5",
12
+ "vite": "8.2.0",
13
13
  "vitest": "4.1.10"
14
14
  },
15
+ "devEngines": {
16
+ "runtime": {
17
+ "name": "node",
18
+ "version": ">=22.12.0"
19
+ }
20
+ },
15
21
  "engines": {
16
22
  "node": ">=20.0.0"
17
23
  },
@@ -46,18 +52,20 @@
46
52
  "url": "git+https://github.com/aforemendude/prettier-plugin-wrap-comments.git"
47
53
  },
48
54
  "scripts": {
55
+ "benchmark": "vitest bench --run",
49
56
  "build": "npm run clean && tsc -p tsconfig.build.json",
50
57
  "clean": "node scripts/clean.mjs",
51
- "format": "prettier --write .",
52
- "format:check": "prettier --check .",
58
+ "format": "prettier --write --cache --cache-strategy metadata .",
59
+ "format:check": "prettier --check --cache --cache-strategy metadata .",
60
+ "format:nocache": "prettier --write .",
53
61
  "prepack": "npm install && git diff --exit-code -- package-lock.json && npm run verify",
54
62
  "test": "vitest run",
55
63
  "test:integration": "vitest run test/integration",
56
64
  "test:unit": "vitest run test/unit",
57
65
  "typecheck": "tsc -p tsconfig.json",
58
- "verify": "npm run format:check && npm run typecheck && npm run build && npm run test"
66
+ "verify": "prettier --check . && npm run typecheck && npm run build && npm run test"
59
67
  },
60
68
  "type": "module",
61
69
  "types": "./dist/index.d.ts",
62
- "version": "1.1.0"
70
+ "version": "1.1.1"
63
71
  }