@aforemendude/prettier-plugin-wrap-comments 1.0.6 → 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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -8
  3. package/dist/comments/comment-body.d.ts +4 -0
  4. package/dist/comments/comment-body.js +36 -0
  5. package/dist/comments/comment-directives.d.ts +2 -0
  6. package/dist/comments/comment-directives.js +52 -0
  7. package/dist/comments/comment-eligibility.d.ts +5 -0
  8. package/dist/comments/comment-eligibility.js +23 -0
  9. package/dist/comments/comment-location.d.ts +9 -0
  10. package/dist/comments/comment-location.js +39 -0
  11. package/dist/comments/comment-ranges.d.ts +24 -0
  12. package/dist/comments/comment-ranges.js +36 -0
  13. package/dist/comments/embedded-expression-ranges.d.ts +12 -0
  14. package/dist/comments/embedded-expression-ranges.js +117 -0
  15. package/dist/comments/jsx-expression-layout.d.ts +10 -0
  16. package/dist/comments/jsx-expression-layout.js +114 -0
  17. package/dist/comments/line-comment-groups.d.ts +6 -0
  18. package/dist/comments/line-comment-groups.js +28 -0
  19. package/dist/comments/prettier-ignore.d.ts +9 -0
  20. package/dist/comments/prettier-ignore.js +239 -0
  21. package/dist/comments/printer-layout.d.ts +18 -0
  22. package/dist/comments/printer-layout.js +65 -0
  23. package/dist/comments/{block.d.ts → wrap-block-comment.d.ts} +5 -1
  24. package/dist/comments/{block.js → wrap-block-comment.js} +18 -15
  25. package/dist/comments/wrap-comments.d.ts +9 -0
  26. package/dist/comments/wrap-comments.js +169 -0
  27. package/dist/comments/wrap-line-comment-group.d.ts +4 -0
  28. package/dist/comments/wrap-line-comment-group.js +39 -0
  29. package/dist/comments/wrap-trailing-line-comment.d.ts +9 -0
  30. package/dist/comments/wrap-trailing-line-comment.js +78 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +4 -4
  33. package/dist/plugin/create-parsers.d.ts +2 -0
  34. package/dist/plugin/create-parsers.js +60 -0
  35. package/dist/plugin/create-printers.d.ts +2 -0
  36. package/dist/plugin/create-printers.js +59 -0
  37. package/dist/plugin/get-printer-layout-source.d.ts +4 -0
  38. package/dist/plugin/get-printer-layout-source.js +35 -0
  39. package/dist/plugin/jsx-comment-rewrite-metadata.d.ts +8 -0
  40. package/dist/plugin/jsx-comment-rewrite-metadata.js +40 -0
  41. package/dist/plugin/parser-names.d.ts +2 -0
  42. package/dist/plugin/parser-names.js +1 -0
  43. package/dist/utils/ast.d.ts +12 -0
  44. package/dist/utils/ast.js +95 -0
  45. package/dist/utils/display-width.d.ts +2 -0
  46. package/dist/utils/display-width.js +19 -0
  47. package/dist/{shared/markdown.d.ts → utils/format-markdown.d.ts} +1 -1
  48. package/dist/utils/format-markdown.js +69 -0
  49. package/dist/utils/indentation.d.ts +4 -0
  50. package/dist/utils/indentation.js +21 -0
  51. package/dist/utils/replacements.d.ts +6 -0
  52. package/dist/utils/replacements.js +21 -0
  53. package/dist/utils/source-lines.d.ts +7 -0
  54. package/dist/utils/source-lines.js +43 -0
  55. package/dist/utils/type-guards.d.ts +2 -0
  56. package/dist/utils/type-guards.js +6 -0
  57. package/dist/utils/whitespace.d.ts +3 -0
  58. package/dist/utils/whitespace.js +26 -0
  59. package/dist/{shared/options.d.ts → utils/wrap-options.d.ts} +2 -1
  60. package/package.json +27 -11
  61. package/dist/comments/core.d.ts +0 -8
  62. package/dist/comments/core.js +0 -114
  63. package/dist/comments/line.d.ts +0 -6
  64. package/dist/comments/line.js +0 -108
  65. package/dist/comments/wrap.d.ts +0 -3
  66. package/dist/comments/wrap.js +0 -439
  67. package/dist/plugin/parsers.d.ts +0 -2
  68. package/dist/plugin/parsers.js +0 -35
  69. package/dist/plugin/printers.d.ts +0 -2
  70. package/dist/plugin/printers.js +0 -39
  71. package/dist/shared/markdown.js +0 -20
  72. package/dist/shared/text.d.ts +0 -15
  73. package/dist/shared/text.js +0 -99
  74. package/dist/shared/types.d.ts +0 -30
  75. package/dist/shared/types.js +0 -1
  76. /package/dist/{shared/options.js → utils/wrap-options.js} +0 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aforemendude
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,8 +5,9 @@ real column to calculate the available content width, so nested comments wrap mo
5
5
 
6
6
  ## Requirements
7
7
 
8
- - Node.js 18 or newer
9
- - Prettier 3 or newer
8
+ - Node.js 20 or newer to use the plugin
9
+ - Node.js 22.12 or newer to develop the plugin
10
+ - Prettier 3 (`>=3.0.0 <4.0.0`)
10
11
 
11
12
  ## Install
12
13
 
@@ -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
@@ -37,9 +63,15 @@ preprocessing: the underlying Prettier parser preprocesses and parses the source
37
63
  comments from that parsed comment list, and Prettier then formats the rewritten source with its built-in JavaScript and
38
64
  TypeScript printers. If the parser cannot parse the preprocessed source, the plugin leaves the source unchanged.
39
65
 
66
+ Offset-sensitive formatting is conservative. Full-file `formatWithCursor` calls skip comment rewriting so Prettier can
67
+ map the cursor from the original source. During range formatting, preprocessing does not rewrite text outside Prettier's
68
+ selected range; eligible comments inside that range can still be wrapped.
69
+
40
70
  Comment text is normalized and reflowed with Prettier's Markdown parser. The available content width is based on
41
71
  Prettier's `printWidth` minus the column where the comment text starts. `tabWidth`, `useTabs`, and `endOfLine` are used
42
- when measuring and rebuilding comments.
72
+ when measuring and rebuilding comments. When `tabWidth` is `0`, tabs contribute zero columns during measurement, as they
73
+ do in Prettier. If the plugin must synthesize indentation at a positive column, it uses spaces because zero-width tabs
74
+ cannot advance to that column.
43
75
 
44
76
  Standalone `//` comments are wrapped in place. Adjacent standalone line comments are combined and reflowed as one
45
77
  Markdown block when they are directly next to each other and their `//` markers start in the same column.
@@ -53,7 +85,11 @@ function example() {
53
85
  ```
54
86
 
55
87
  Trailing `//` comments stay in place when the full source line fits within `printWidth`. If the source line is too long,
56
- the comment is moved above the code and wrapped using the code line's indentation.
88
+ the comment is moved above the code and normally wrapped using the code line's indentation. On a line made up of closing
89
+ delimiters such as `}`, `]`, or `)`, the moved comment is indented one additional level so it remains visually inside
90
+ the construct being closed. When a comment directly trails the root value of a JSX expression or template interpolation,
91
+ it can move above that value and wrap inside the expression delimiters. Nested or otherwise ambiguous expression
92
+ comments and comments in JSX spreads stay in place.
57
93
 
58
94
  ```ts
59
95
  function example() {
@@ -138,10 +174,13 @@ The plugin leaves these comments unchanged:
138
174
  - JSDoc comments that start with `/**`
139
175
  - bang-preserved comments that start with `/*!` or `//!`
140
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
141
179
  - empty comment bodies
142
180
  - `prettier-ignore` markers themselves
143
- - other directive comments such as `@license`, `@preserve`, JSX and TypeScript pragmas, source map directives,
144
- `#__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
145
184
 
146
185
  ## Supported Parsers
147
186
 
@@ -149,13 +188,51 @@ The plugin leaves these comments unchanged:
149
188
  - `babel-ts`
150
189
  - `typescript`
151
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
+
152
213
  ## Development
153
214
 
215
+ Source files are organized by responsibility. `src/plugin/` contains parser and printer integration,
216
+ `src/comments/wrap-comments.ts` coordinates the rewriting pipeline, the other `src/comments/` modules isolate comment
217
+ analysis and wrapping concerns, and `src/utils/` contains reusable utilities. Unit tests under `test/unit/` mirror those
218
+ concerns and source file names wherever practical.
219
+
154
220
  ```sh
155
221
  npm install
222
+ npm run benchmark
156
223
  npm run format:check
224
+ npm run typecheck
157
225
  npm run test
226
+ npm run build
158
227
  ```
159
228
 
160
- `npm run test` builds `dist` before running the Node test suite. `npm run verify` runs `npm install`,
161
- `npm run format:check`, and `npm run test`.
229
+ `npm run test` runs the TypeScript unit and fixture-based integration suites with Vitest. Use `npm run test:unit` or
230
+ `npm run test:integration` to run one suite. `npm run build` removes and recreates `dist` using a cross-platform Node
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
@@ -0,0 +1,4 @@
1
+ import type { CommentRange } from './comment-ranges.js';
2
+ export declare function getCommentBody(text: string, comment: CommentRange): string;
3
+ export declare function normalizeLineCommentBody(rawBody: string): string;
4
+ export declare function normalizeBlockCommentBody(rawComment: string): string;
@@ -0,0 +1,36 @@
1
+ import { isBlankLine, normalizeLineTerminators } from '../utils/source-lines.js';
2
+ export function getCommentBody(text, comment) {
3
+ const raw = text.slice(comment.start, comment.end);
4
+ return comment.kind === 'line' ? normalizeLineCommentBody(raw.slice(2)) : normalizeBlockCommentBody(raw);
5
+ }
6
+ export function normalizeLineCommentBody(rawBody) {
7
+ if (rawBody.trim() === '') {
8
+ return '';
9
+ }
10
+ return rawBody.replace(/^[ \t]?/, '').replace(/[ \t]+$/u, '');
11
+ }
12
+ export function normalizeBlockCommentBody(rawComment) {
13
+ const body = normalizeLineTerminators(rawComment.slice(2, -2));
14
+ const lines = body.split('\n');
15
+ if (lines.length === 1) {
16
+ return lines[0]?.trim() ?? '';
17
+ }
18
+ while (isBlankLine(lines[0])) {
19
+ lines.shift();
20
+ }
21
+ while (isBlankLine(lines.at(-1))) {
22
+ lines.pop();
23
+ }
24
+ return lines
25
+ .map((line) => {
26
+ const withoutIndent = line.replace(/^[ \t]*/u, '');
27
+ if (!withoutIndent.startsWith('*')) {
28
+ return withoutIndent.replace(/[ \t]+$/u, '');
29
+ }
30
+ return withoutIndent
31
+ .slice(1)
32
+ .replace(/^[ \t]?/u, '')
33
+ .replace(/[ \t]+$/u, '');
34
+ })
35
+ .join('\n');
36
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isDirectiveComment(body: string): boolean;
2
+ export declare function isPrettierIgnoreComment(body: string): boolean;
@@ -0,0 +1,52 @@
1
+ const PRAGMA_DIRECTIVE_COMMENT_PATTERNS = [
2
+ /^@(?:flow|noflow)\b/u,
3
+ /^@(?:license|preserve)\b/u,
4
+ /^@(?:jsxFrag|jsxImportSource|jsxRuntime|jsx)\b/u,
5
+ /^@(?:ts-check|ts-expect-error|ts-ignore|ts-nocheck)\b/u,
6
+ /^[@#]__(?:NO_SIDE_EFFECTS|PURE)__\b/u,
7
+ ];
8
+ const SOURCE_MAP_DIRECTIVE_COMMENT_PATTERNS = [
9
+ /^[#@][ \t]*sourceMappingURL=/u,
10
+ /^[#@][ \t]*sourceURL=/u,
11
+ /^sourceMappingURL=/u,
12
+ /^sourceURL=/u,
13
+ ];
14
+ const TOOL_DIRECTIVE_COMMENT_PATTERNS = [
15
+ /^biome-ignore\b/u,
16
+ /^c8\b/u,
17
+ /^deno-lint-ignore\b/u,
18
+ /^eslint\b/u,
19
+ /^eslint-/u,
20
+ /^exported\b/u,
21
+ /^flowlint(?:-next-line|-line)?\b/u,
22
+ /^globals?\b/u,
23
+ /^istanbul\b/u,
24
+ /^jshint\b/u,
25
+ /^node:coverage (?:disable|enable|ignore next(?: [1-9]\d*)?)$/u,
26
+ /^nyc\b/u,
27
+ /^oxlint\b/u,
28
+ /^prettier-ignore\b/u,
29
+ /^prettier-ignore-start\b/u,
30
+ /^prettier-ignore-end\b/u,
31
+ /^stylelint\b/u,
32
+ /^tslint\b/u,
33
+ /^v8\b/u,
34
+ /^vite-ignore\b/u,
35
+ /^\$(?:FlowExpectedError|FlowFixMe)\b/u,
36
+ ];
37
+ const BUNDLER_DIRECTIVE_COMMENT_PATTERNS = [
38
+ /^webpack(?:ChunkName|Exclude|Ignore|Include|Mode|Prefetch|Preload)\b/u,
39
+ ];
40
+ const DIRECTIVE_COMMENT_PATTERNS = [
41
+ ...PRAGMA_DIRECTIVE_COMMENT_PATTERNS,
42
+ ...SOURCE_MAP_DIRECTIVE_COMMENT_PATTERNS,
43
+ ...TOOL_DIRECTIVE_COMMENT_PATTERNS,
44
+ ...BUNDLER_DIRECTIVE_COMMENT_PATTERNS,
45
+ ];
46
+ export function isDirectiveComment(body) {
47
+ const normalizedBody = body.trimStart();
48
+ return DIRECTIVE_COMMENT_PATTERNS.some((pattern) => pattern.test(normalizedBody));
49
+ }
50
+ export function isPrettierIgnoreComment(body) {
51
+ return body.trim() === 'prettier-ignore';
52
+ }
@@ -0,0 +1,5 @@
1
+ import type { CommentRange } from './comment-ranges.js';
2
+ export declare function shouldSkipLineComment(text: string, comment: CommentRange): boolean;
3
+ export declare function shouldSkipBlockComment(text: string, comment: CommentRange): boolean;
4
+ export declare function hasPreserveCommentMarker(rawComment: string): boolean;
5
+ export declare function hasFlowCommentTypeMarker(rawComment: string): boolean;
@@ -0,0 +1,23 @@
1
+ import { normalizeBlockCommentBody, normalizeLineCommentBody } from './comment-body.js';
2
+ import { isDirectiveComment } from './comment-directives.js';
3
+ export function shouldSkipLineComment(text, comment) {
4
+ const raw = text.slice(comment.start, comment.end);
5
+ if (raw.startsWith('///') || hasPreserveCommentMarker(raw)) {
6
+ return true;
7
+ }
8
+ return isDirectiveComment(normalizeLineCommentBody(raw.slice(2)));
9
+ }
10
+ export function shouldSkipBlockComment(text, comment) {
11
+ const raw = text.slice(comment.start, comment.end);
12
+ if (raw.startsWith('/**') || hasPreserveCommentMarker(raw) || hasFlowCommentTypeMarker(raw)) {
13
+ return true;
14
+ }
15
+ const body = normalizeBlockCommentBody(raw);
16
+ return body.trim() === '' || isDirectiveComment(body);
17
+ }
18
+ export function hasPreserveCommentMarker(rawComment) {
19
+ return rawComment.startsWith('/*!') || rawComment.startsWith('//!');
20
+ }
21
+ export function hasFlowCommentTypeMarker(rawComment) {
22
+ return /^\/\*[ \t]*(?::|flow-include)/u.test(rawComment);
23
+ }
@@ -0,0 +1,9 @@
1
+ import type { CommentRange } from './comment-ranges.js';
2
+ export declare function isStandaloneComment(text: string, comment: CommentRange): boolean;
3
+ export declare function isStandaloneLineComment(text: string, comment: CommentRange): boolean;
4
+ export declare function isStandaloneBlockComment(text: string, comment: {
5
+ end: number;
6
+ start: number;
7
+ }): boolean;
8
+ export declare function areCommentsOnAdjacentLines(text: string, previousComment: CommentRange, comment: CommentRange): boolean;
9
+ export declare function isCommentAdjacentBeforeIndex(text: string, comment: CommentRange, index: number): boolean;
@@ -0,0 +1,39 @@
1
+ import { getLineEnd, getLinePrefix, getLineStart } from '../utils/source-lines.js';
2
+ import { isEcmaScriptHorizontalWhitespace } from '../utils/whitespace.js';
3
+ export function isStandaloneComment(text, comment) {
4
+ return comment.kind === 'line' ? isStandaloneLineComment(text, comment) : isStandaloneBlockComment(text, comment);
5
+ }
6
+ export function isStandaloneLineComment(text, comment) {
7
+ return isOnlyHorizontalWhitespace(getLinePrefix(text, comment.start));
8
+ }
9
+ export function isStandaloneBlockComment(text, comment) {
10
+ const before = text.slice(getLineStart(text, comment.start), comment.start);
11
+ const after = text.slice(comment.end, getLineEnd(text, comment.end));
12
+ return isOnlyHorizontalWhitespace(before) && isOnlyHorizontalWhitespace(after);
13
+ }
14
+ export function areCommentsOnAdjacentLines(text, previousComment, comment) {
15
+ return isOnlyHorizontalWhitespaceAroundNewline(text.slice(previousComment.end, comment.start));
16
+ }
17
+ export function isCommentAdjacentBeforeIndex(text, comment, index) {
18
+ return isOnlyHorizontalWhitespaceAroundNewline(text.slice(comment.end, index));
19
+ }
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;
39
+ }
@@ -0,0 +1,24 @@
1
+ export type RawComment = {
2
+ end?: unknown;
3
+ loc?: {
4
+ start?: {
5
+ column?: unknown;
6
+ };
7
+ };
8
+ range?: unknown;
9
+ start?: unknown;
10
+ type?: unknown;
11
+ value?: unknown;
12
+ };
13
+ export type CommentRange = {
14
+ end: number;
15
+ kind: 'block' | 'line';
16
+ start: number;
17
+ };
18
+ export type CommentEntry = {
19
+ range: CommentRange;
20
+ raw: RawComment;
21
+ };
22
+ export declare function collectAstComments(ast: unknown): RawComment[];
23
+ export declare function collectCommentEntries(ast: unknown, text: string): CommentEntry[];
24
+ export declare function toCommentRange(comment: RawComment, text: string): CommentRange | undefined;
@@ -0,0 +1,36 @@
1
+ import { numberOrUndefined } from '../utils/type-guards.js';
2
+ export function collectAstComments(ast) {
3
+ if (ast === null || ast === undefined) {
4
+ return [];
5
+ }
6
+ const candidate = ast;
7
+ if (Array.isArray(candidate.comments)) {
8
+ return candidate.comments;
9
+ }
10
+ if (Array.isArray(candidate.program?.comments)) {
11
+ return candidate.program.comments;
12
+ }
13
+ return [];
14
+ }
15
+ export function collectCommentEntries(ast, text) {
16
+ return collectAstComments(ast)
17
+ .map((raw) => ({ range: toCommentRange(raw, text), raw }))
18
+ .filter((entry) => entry.range !== undefined)
19
+ .sort((left, right) => left.range.start - right.range.start);
20
+ }
21
+ export function toCommentRange(comment, text) {
22
+ const range = Array.isArray(comment.range) ? comment.range : undefined;
23
+ const start = numberOrUndefined(comment.start) ?? numberOrUndefined(range?.[0]);
24
+ const end = numberOrUndefined(comment.end) ?? numberOrUndefined(range?.[1]);
25
+ if (start === undefined || end === undefined || start >= end) {
26
+ return undefined;
27
+ }
28
+ const rawStart = text.slice(start, start + 3);
29
+ if (rawStart.startsWith('//')) {
30
+ return { end, kind: 'line', start };
31
+ }
32
+ if (rawStart.startsWith('/*')) {
33
+ return { end, kind: 'block', start };
34
+ }
35
+ return undefined;
36
+ }
@@ -0,0 +1,12 @@
1
+ import type { CommentRange } from './comment-ranges.js';
2
+ import type { SourceRange } from '../utils/ast.js';
3
+ export type EmbeddedExpressionRange = SourceRange & {
4
+ expression?: SourceRange;
5
+ };
6
+ export type EmbeddedTrailingLineCommentMove = {
7
+ insertAt: number;
8
+ removeStart: number;
9
+ };
10
+ export declare function collectEmbeddedExpressionRanges(ast: unknown): EmbeddedExpressionRange[];
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;
@@ -0,0 +1,117 @@
1
+ import { getAstNodeRange, visitAstNodes } from '../utils/ast.js';
2
+ import { getLineEnd } from '../utils/source-lines.js';
3
+ import { isRecord, numberOrUndefined } from '../utils/type-guards.js';
4
+ import { skipWhitespace, trimWhitespaceEnd } from '../utils/whitespace.js';
5
+ const JSX_EMBEDDED_EXPRESSION_TYPES = new Set(['JSXExpressionContainer', 'JSXSpreadAttribute', 'JSXSpreadChild']);
6
+ export function collectEmbeddedExpressionRanges(ast) {
7
+ const ranges = [];
8
+ visitAstNodes(ast, (node) => {
9
+ const type = node['type'];
10
+ if (typeof type !== 'string') {
11
+ return;
12
+ }
13
+ if (JSX_EMBEDDED_EXPRESSION_TYPES.has(type)) {
14
+ const range = getAstNodeRange(node);
15
+ if (range !== undefined) {
16
+ const expressionNode = type === 'JSXExpressionContainer' ? node['expression'] : undefined;
17
+ const expression = isRecord(expressionNode) ? getAstNodeRange(expressionNode) : undefined;
18
+ ranges.push(expression === undefined ? range : { ...range, expression });
19
+ }
20
+ return;
21
+ }
22
+ if (type === 'TemplateLiteral') {
23
+ collectTemplateInterpolationRanges(node, 'expressions', ranges);
24
+ }
25
+ else if (type === 'TSTemplateLiteralType') {
26
+ collectTemplateInterpolationRanges(node, 'types', ranges);
27
+ }
28
+ });
29
+ return ranges.sort((left, right) => left.start - right.start || right.end - left.end);
30
+ }
31
+ export function getEmbeddedTrailingLineCommentMove(text, comment, range) {
32
+ const expression = range?.expression;
33
+ if (range === undefined || expression === undefined) {
34
+ return undefined;
35
+ }
36
+ const rootExpression = getRootExpressionRangeBefore(text, expression, comment.start, range.start);
37
+ return rootExpression === undefined ? undefined : { insertAt: rootExpression.start, removeStart: rootExpression.end };
38
+ }
39
+ export function doesBlockCommentSeparateEmbeddedTrailingLineComment(text, blockComment, nextComment, range, nextRange) {
40
+ if (blockComment.kind !== 'block' ||
41
+ nextComment?.kind !== 'line' ||
42
+ blockComment.end >= nextComment.start ||
43
+ nextComment.end > getLineEnd(text, blockComment.start)) {
44
+ return false;
45
+ }
46
+ const expression = range?.expression;
47
+ const rootExpression = range === undefined || expression === undefined
48
+ ? undefined
49
+ : getRootExpressionRangeBefore(text, expression, blockComment.start, range.start);
50
+ return (range !== undefined &&
51
+ range === nextRange &&
52
+ rootExpression !== undefined &&
53
+ /^[\t ]*$/u.test(text.slice(blockComment.end, nextComment.start)));
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
+ }
84
+ function collectTemplateInterpolationRanges(node, expressionsKey, ranges) {
85
+ const quasis = node['quasis'];
86
+ const expressions = node[expressionsKey];
87
+ if (!Array.isArray(quasis) || !Array.isArray(expressions) || quasis.length !== expressions.length + 1) {
88
+ return;
89
+ }
90
+ for (let index = 0; index < expressions.length; index += 1) {
91
+ const precedingQuasi = getNodeBoundary(quasis[index]);
92
+ const followingQuasi = getNodeBoundary(quasis[index + 1]);
93
+ const expression = getExpressionRange(expressions[index]);
94
+ if (precedingQuasi === undefined ||
95
+ followingQuasi === undefined ||
96
+ expression === undefined ||
97
+ precedingQuasi.end >= followingQuasi.start) {
98
+ continue;
99
+ }
100
+ ranges.push({ end: followingQuasi.start, expression, start: precedingQuasi.end });
101
+ }
102
+ }
103
+ function getExpressionRange(value) {
104
+ return isRecord(value) ? getAstNodeRange(value) : undefined;
105
+ }
106
+ function getNodeBoundary(value) {
107
+ if (!isRecord(value)) {
108
+ return undefined;
109
+ }
110
+ const range = Array.isArray(value['range']) ? value['range'] : undefined;
111
+ const start = numberOrUndefined(value['start']) ?? numberOrUndefined(range?.[0]);
112
+ const end = numberOrUndefined(value['end']) ?? numberOrUndefined(range?.[1]);
113
+ if (start === undefined || end === undefined || start > end) {
114
+ return undefined;
115
+ }
116
+ return { end, start };
117
+ }
@@ -0,0 +1,10 @@
1
+ import type { CommentRange } from './comment-ranges.js';
2
+ import type { PrinterCommentLayout } from './printer-layout.js';
3
+ import type { BlockCommentLayout } from './wrap-block-comment.js';
4
+ import type { ContainingRangeMatch, SourceRange } from '../utils/ast.js';
5
+ export type JsxExpressionContainerRange = SourceRange & {
6
+ expression: SourceRange | undefined;
7
+ };
8
+ export declare function collectJsxExpressionContainerRanges(ast: unknown): JsxExpressionContainerRange[];
9
+ export declare function getPrintedJsxCommentMarkerColumn(text: string, container: SourceRange, tabWidth: number): number;
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;