@aforemendude/prettier-plugin-wrap-comments 1.0.2 → 1.0.4

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
@@ -1,7 +1,12 @@
1
1
  # @aforemendude/prettier-plugin-wrap-comments
2
2
 
3
- A Prettier plugin that wraps non-JSDoc JavaScript and TypeScript comments as Markdown. It uses the comment marker's real
4
- column to calculate the available content width, so nested comments wrap more narrowly than top-level comments.
3
+ A Prettier plugin that wraps non-JSDoc JavaScript and TypeScript comments as Markdown. It uses each comment marker's
4
+ real column to calculate the available content width, so nested comments wrap more narrowly than top-level comments.
5
+
6
+ ## Requirements
7
+
8
+ - Node.js 18 or newer
9
+ - Prettier 3 or newer
5
10
 
6
11
  ## Install
7
12
 
@@ -27,8 +32,17 @@ npx prettier --write .
27
32
 
28
33
  ## Behavior
29
34
 
30
- The plugin wraps standalone `//` and `/* ... */` comments before Prettier parses the file, then delegates formatting to
31
- Prettier's built-in JavaScript and TypeScript parsers.
35
+ 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.
39
+
40
+ Comment text is normalized and reflowed with Prettier's Markdown parser. The available content width is based on
41
+ Prettier's `printWidth` minus the column where the comment text starts. `tabWidth`, `useTabs`, and `endOfLine` are used
42
+ when measuring and rebuilding comments.
43
+
44
+ Standalone `//` comments are wrapped in place. Adjacent standalone line comments are combined and reflowed as one
45
+ Markdown block when they are directly next to each other and their `//` markers start in the same column.
32
46
 
33
47
  ```ts
34
48
  function example() {
@@ -38,18 +52,78 @@ function example() {
38
52
  }
39
53
  ```
40
54
 
41
- JSDoc comments are left unchanged:
55
+ 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.
57
+
58
+ ```ts
59
+ function example() {
60
+ // This trailing comment moved above the statement because the original line
61
+ // was too long.
62
+ const value = 1;
63
+ }
64
+ ```
65
+
66
+ Non-JSDoc `/* ... */` comments are also normalized as Markdown. A block comment may stay on one line if the normalized
67
+ comment fits within `printWidth`; otherwise, only standalone block comments are expanded into star-prefixed blocks. Long
68
+ inline block comments are left unchanged when they cannot fit on one line.
42
69
 
43
70
  ```ts
44
- /**
45
- * This documentation comment is not wrapped by the plugin.
46
- */
71
+ if (ready) {
72
+ /*
73
+ * This block comment is wrapped with the nested indentation included in the
74
+ * available width calculation.
75
+ */
76
+ run();
77
+ }
47
78
  ```
48
79
 
49
- Tooling directives such as `eslint-disable`, `@ts-expect-error`, `prettier-ignore`, source maps, and TypeScript
50
- triple-slash directives are also left untouched so their meaning is not changed. Trailing comments after code stay in
51
- place when the full line fits the configured print width. If the line is too long, the comment is moved above the code
52
- and wrapped there.
80
+ JSX and TSX expression comments use the expression contents, not the outer React expression braces, to decide whether a
81
+ block comment is standalone, trailing, or inline. A comment-only expression like `{/* ... */}` can wrap as a standalone
82
+ JSX comment. A trailing expression comment like `{label /* ... */}` can move before `label` and wrap. A true inline
83
+ expression comment like `{"abc" + /* ... */ "123"}` is left unchanged when it cannot fit on one line.
84
+
85
+ <!-- prettier-ignore-start -->
86
+ ```tsx
87
+ <span>
88
+ {
89
+ /*
90
+ * This expression comment wraps because the surrounding JSX braces do not
91
+ * make it inline.
92
+ */
93
+ }
94
+ {
95
+ /*
96
+ * This expression comment moved above the expression value because it was
97
+ * trailing.
98
+ */
99
+ label
100
+ }
101
+ {'abc' + /* This inline expression comment stays in place. */ '123'}
102
+ </span>
103
+ ```
104
+ <!-- prettier-ignore-end -->
105
+
106
+ `prettier-ignore` markers are preserved and affect wrapping only when the marker body is exactly `prettier-ignore`. When
107
+ one of these markers is directly above a standalone block comment, the plugin leaves that block comment unchanged. If
108
+ the block comment is one the plugin would otherwise wrap, the following code still formats normally; if the block
109
+ comment is already skipped by the plugin, such as a JSDoc or directive block, Prettier keeps its normal ignore behavior
110
+ for the following code.
111
+
112
+ For trailing line comments, a `// prettier-ignore` line can apply to the code line and its inline comment. The plugin
113
+ walks past adjacent standalone comments that it normally leaves alone, such as an `eslint-disable-next-line` directive,
114
+ so an ignored code line's trailing comment remains inline and unchanged. A directive comment by itself does not ignore
115
+ the following code line; without `// prettier-ignore`, an overlong trailing comment below a directive is still moved
116
+ above the statement and wrapped.
117
+
118
+ The plugin leaves these comments unchanged:
119
+
120
+ - JSDoc comments that start with `/**`
121
+ - bang-preserved comments that start with `/*!` or `//!`
122
+ - TypeScript-style triple-slash line comments that start with `///`
123
+ - empty comment bodies
124
+ - `prettier-ignore` markers themselves
125
+ - other directive comments such as `@license`, `@preserve`, JSX and TypeScript pragmas, source map directives,
126
+ `#__PURE__`, `@__PURE__`, lint/coverage/formatter directives, `vite-ignore`, and webpack magic comments
53
127
 
54
128
  ## Supported Parsers
55
129
 
@@ -61,19 +135,9 @@ and wrapped there.
61
135
 
62
136
  ```sh
63
137
  npm install
64
- npm run check
65
- ```
66
-
67
- ## Publishing
68
-
69
- The npm package name is `@aforemendude/prettier-plugin-wrap-comments`.
70
-
71
- Before publishing, make sure the version in `package.json` has been updated, then run:
72
-
73
- ```sh
74
- npm run check
75
- npm publish --access public
138
+ npm run format:check
139
+ npm run test
76
140
  ```
77
141
 
78
- The package scripts already run the important publish checks: `prepublishOnly` runs `npm run check`, and `prepack`
79
- builds the `dist` files that are included in the published package.
142
+ `npm run test` builds `dist` before running the Node test suite. `npm run verify` runs `npm install`,
143
+ `npm run format:check`, and `npm run test`.
@@ -1,2 +1,12 @@
1
1
  import type { CommentRange, Replacement, WrapOptions } from '../shared/types.js';
2
- export declare function wrapBlockComment(text: string, comment: CommentRange, options: WrapOptions): Promise<Replacement | undefined>;
2
+ export type BlockCommentLayout = {
3
+ contentColumn?: number;
4
+ multilineIndent?: string;
5
+ placement: 'inline' | 'standalone' | 'trailing';
6
+ trailingMove?: {
7
+ insertAt: number;
8
+ removeEnd: number;
9
+ removeStart: number;
10
+ };
11
+ };
12
+ export declare function wrapBlockComment(text: string, comment: CommentRange, options: WrapOptions, layout?: BlockCommentLayout): Promise<Replacement | Replacement[] | undefined>;
@@ -1,10 +1,10 @@
1
- import { isDirectiveComment, normalizeBlockCommentBody } from './core.js';
1
+ import { hasPreserveCommentMarker, isDirectiveComment, normalizeBlockCommentBody } from './core.js';
2
2
  import { formatMarkdownLines } from '../shared/markdown.js';
3
3
  import { getAvailableContentWidth, getPrintWidth, getTabWidth } from '../shared/options.js';
4
4
  import { getColumnAt, getColumns, getLinePrefix, getPreferredNewline, isStandaloneBlockComment, } from '../shared/text.js';
5
- export async function wrapBlockComment(text, comment, options) {
5
+ export async function wrapBlockComment(text, comment, options, layout = getDefaultBlockCommentLayout(text, comment)) {
6
6
  const raw = text.slice(comment.start, comment.end);
7
- if (raw.startsWith('/**')) {
7
+ if (raw.startsWith('/**') || hasPreserveCommentMarker(raw)) {
8
8
  return undefined;
9
9
  }
10
10
  const markdown = normalizeBlockCommentBody(raw);
@@ -13,19 +13,25 @@ export async function wrapBlockComment(text, comment, options) {
13
13
  }
14
14
  const tabWidth = getTabWidth(options);
15
15
  const markerColumn = getColumnAt(text, comment.start, tabWidth);
16
- const availableWidth = getAvailableContentWidth(options, markerColumn + 3);
16
+ const availableWidth = getAvailableContentWidth(options, layout.contentColumn ?? markerColumn + 3);
17
17
  const formattedLines = await formatMarkdownLines(markdown, availableWidth, options);
18
- const replacementText = buildBlockReplacement(text, comment, formattedLines, options);
19
- if (replacementText === undefined || replacementText === text.slice(comment.start, comment.end)) {
18
+ const replacement = buildBlockReplacement(text, comment, formattedLines, options, layout);
19
+ if (replacement === undefined) {
20
+ return undefined;
21
+ }
22
+ if (Array.isArray(replacement)) {
23
+ return replacement;
24
+ }
25
+ if (replacement === text.slice(comment.start, comment.end)) {
20
26
  return undefined;
21
27
  }
22
28
  return {
23
29
  end: comment.end,
24
30
  start: comment.start,
25
- text: replacementText,
31
+ text: replacement,
26
32
  };
27
33
  }
28
- function buildBlockReplacement(text, comment, formattedLines, options) {
34
+ function buildBlockReplacement(text, comment, formattedLines, options, layout) {
29
35
  const tabWidth = getTabWidth(options);
30
36
  const markerColumn = getColumnAt(text, comment.start, tabWidth);
31
37
  const singleLine = `/* ${formattedLines.join(' ')} */`;
@@ -33,11 +39,34 @@ function buildBlockReplacement(text, comment, formattedLines, options) {
33
39
  if (formattedLines.length === 1 && markerColumn + singleLineWidth <= getPrintWidth(options)) {
34
40
  return singleLine;
35
41
  }
36
- if (!isStandaloneBlockComment(text, comment)) {
42
+ if (layout.placement === 'inline') {
37
43
  return undefined;
38
44
  }
39
45
  const newline = getPreferredNewline(text, options);
40
- const indent = getLinePrefix(text, comment.start);
46
+ const indent = layout.multilineIndent ?? getLinePrefix(text, comment.start);
47
+ const replacementText = buildMultilineBlockReplacement(formattedLines, newline, indent);
48
+ if (layout.trailingMove !== undefined) {
49
+ return [
50
+ {
51
+ end: layout.trailingMove.insertAt,
52
+ start: layout.trailingMove.insertAt,
53
+ text: `${replacementText}${newline}`,
54
+ },
55
+ {
56
+ end: layout.trailingMove.removeEnd,
57
+ start: layout.trailingMove.removeStart,
58
+ text: '',
59
+ },
60
+ ];
61
+ }
62
+ return replacementText;
63
+ }
64
+ function buildMultilineBlockReplacement(formattedLines, newline, indent) {
41
65
  const body = formattedLines.map((line) => `${indent} *${line.length === 0 ? '' : ` ${line}`}`).join(newline);
42
66
  return `/*${newline}${body}${newline}${indent} */`;
43
67
  }
68
+ function getDefaultBlockCommentLayout(text, comment) {
69
+ return {
70
+ placement: isStandaloneBlockComment(text, comment) ? 'standalone' : 'inline',
71
+ };
72
+ }
@@ -4,3 +4,5 @@ export declare function toCommentRange(comment: RawComment, text: string): Comme
4
4
  export declare function normalizeLineCommentBody(rawBody: string): string;
5
5
  export declare function normalizeBlockCommentBody(rawComment: string): string;
6
6
  export declare function isDirectiveComment(body: string): boolean;
7
+ export declare function isPrettierIgnoreComment(body: string): boolean;
8
+ export declare function hasPreserveCommentMarker(rawComment: string): boolean;
@@ -63,8 +63,56 @@ export function normalizeBlockCommentBody(rawComment) {
63
63
  })
64
64
  .join('\n');
65
65
  }
66
+ const PRAGMA_DIRECTIVE_COMMENT_PATTERNS = [
67
+ /^@(?:license|preserve)\b/u,
68
+ /^@(?:jsxFrag|jsxImportSource|jsxRuntime|jsx)\b/u,
69
+ /^@(?:ts-check|ts-expect-error|ts-ignore|ts-nocheck)\b/u,
70
+ /^[@#]__(?:NO_SIDE_EFFECTS|PURE)__\b/u,
71
+ ];
72
+ const SOURCE_MAP_DIRECTIVE_COMMENT_PATTERNS = [
73
+ /^[#@][ \t]*sourceMappingURL=/u,
74
+ /^[#@][ \t]*sourceURL=/u,
75
+ /^sourceMappingURL=/u,
76
+ /^sourceURL=/u,
77
+ ];
78
+ const TOOL_DIRECTIVE_COMMENT_PATTERNS = [
79
+ /^biome-ignore\b/u,
80
+ /^c8\b/u,
81
+ /^deno-lint-ignore\b/u,
82
+ /^eslint\b/u,
83
+ /^eslint-/u,
84
+ /^exported\b/u,
85
+ /^globals?\b/u,
86
+ /^istanbul\b/u,
87
+ /^jshint\b/u,
88
+ /^nyc\b/u,
89
+ /^oxlint\b/u,
90
+ /^prettier-ignore\b/u,
91
+ /^prettier-ignore-start\b/u,
92
+ /^prettier-ignore-end\b/u,
93
+ /^stylelint\b/u,
94
+ /^tslint\b/u,
95
+ /^v8\b/u,
96
+ /^vite-ignore\b/u,
97
+ ];
98
+ const BUNDLER_DIRECTIVE_COMMENT_PATTERNS = [
99
+ /^webpack(?:ChunkName|Exclude|Ignore|Include|Mode|Prefetch|Preload)\b/u,
100
+ ];
101
+ const DIRECTIVE_COMMENT_PATTERNS = [
102
+ ...PRAGMA_DIRECTIVE_COMMENT_PATTERNS,
103
+ ...SOURCE_MAP_DIRECTIVE_COMMENT_PATTERNS,
104
+ ...TOOL_DIRECTIVE_COMMENT_PATTERNS,
105
+ ...BUNDLER_DIRECTIVE_COMMENT_PATTERNS,
106
+ ];
66
107
  export function isDirectiveComment(body) {
67
- return /^(?:@(?:__NO_SIDE_EFFECTS__|__PURE__|jsx|jsxImportSource|license|preserve|ts-check|ts-expect-error|ts-ignore|ts-nocheck)\b|#\s*sourceMappingURL=|[@#]__PURE__\b|biome-ignore\b|c8\b|deno-lint-ignore\b|eslint\b|eslint-|exported\b|globals?\b|istanbul\b|jshint\b|nyc\b|oxlint\b|prettier-ignore\b|prettier-ignore-start\b|prettier-ignore-end\b|sourceMappingURL=|stylelint\b|tslint\b|v8\b|vite-ignore\b|webpack(?:ChunkName|Exclude|Ignore|Include|Mode|Prefetch|Preload)\b)/u.test(body.trimStart());
108
+ const normalizedBody = body.trimStart();
109
+ return DIRECTIVE_COMMENT_PATTERNS.some((pattern) => pattern.test(normalizedBody));
110
+ }
111
+ export function isPrettierIgnoreComment(body) {
112
+ return body.trim() === 'prettier-ignore';
113
+ }
114
+ export function hasPreserveCommentMarker(rawComment) {
115
+ return rawComment.startsWith('/*!') || rawComment.startsWith('//!');
68
116
  }
69
117
  function isBlankLine(line) {
70
118
  return line !== undefined && line.trim() === '';
@@ -1,6 +1,6 @@
1
1
  import type { CommentRange, Replacement, WrapOptions } from '../shared/types.js';
2
2
  export declare function wrapLineCommentGroup(text: string, comments: CommentRange[], options: WrapOptions): Promise<Replacement | undefined>;
3
- export declare function wrapTrailingLineComment(text: string, comment: CommentRange, options: WrapOptions): Promise<Replacement | undefined>;
3
+ export declare function wrapTrailingLineComment(text: string, comment: CommentRange, options: WrapOptions): Promise<Replacement[] | undefined>;
4
4
  export declare function shouldSkipLineComment(text: string, comment: CommentRange): boolean;
5
5
  export declare function isStandaloneLineComment(text: string, comment: CommentRange): boolean;
6
6
  export declare function areAdjacentLineComments(text: string, previous: CommentRange, next: CommentRange): boolean;
@@ -1,7 +1,7 @@
1
- import { isDirectiveComment, normalizeLineCommentBody } from './core.js';
1
+ import { hasPreserveCommentMarker, isDirectiveComment, normalizeLineCommentBody } from './core.js';
2
2
  import { formatMarkdownLines } from '../shared/markdown.js';
3
3
  import { getAvailableContentWidth, getPrintWidth, getTabWidth } from '../shared/options.js';
4
- import { getColumnAt, getColumns, getContinuationIndent, getLineEnd, getLinePrefix, getLineStart, getPreferredNewline, } from '../shared/text.js';
4
+ import { getColumnAt, getColumns, getContinuationIndent, getLineEnd, getLinePrefix, getLineStart, getPreferredNewline, makeIndent, } from '../shared/text.js';
5
5
  export async function wrapLineCommentGroup(text, comments, options) {
6
6
  const firstComment = comments[0];
7
7
  if (firstComment === undefined) {
@@ -51,23 +51,30 @@ export async function wrapTrailingLineComment(text, comment, options) {
51
51
  return undefined;
52
52
  }
53
53
  const tabWidth = getTabWidth(options);
54
- const indent = getLineIndent(linePrefix);
54
+ const indent = getTrailingCommentIndent(codeText, linePrefix, options);
55
55
  const availableWidth = getAvailableContentWidth(options, getColumns(indent, tabWidth) + 3);
56
56
  const formattedLines = await formatMarkdownLines(body, availableWidth, options);
57
57
  const newline = getPreferredNewline(text, options);
58
58
  const leadingCommentText = formattedLines
59
59
  .map((line) => `${indent}${line.length === 0 ? '//' : `// ${line}`}`)
60
60
  .join(newline);
61
- const replacementText = `${leadingCommentText}${newline}${codeText}`;
62
- return {
63
- end: lineEnd,
64
- start: lineStart,
65
- text: replacementText,
66
- };
61
+ const codeEnd = lineStart + codeText.length;
62
+ return [
63
+ {
64
+ end: lineStart,
65
+ start: lineStart,
66
+ text: `${leadingCommentText}${newline}`,
67
+ },
68
+ {
69
+ end: lineEnd,
70
+ start: codeEnd,
71
+ text: '',
72
+ },
73
+ ];
67
74
  }
68
75
  export function shouldSkipLineComment(text, comment) {
69
76
  const raw = text.slice(comment.start, comment.end);
70
- if (raw.startsWith('///')) {
77
+ if (raw.startsWith('///') || hasPreserveCommentMarker(raw)) {
71
78
  return true;
72
79
  }
73
80
  return isDirectiveComment(normalizeLineCommentBody(raw.slice(2)));
@@ -88,3 +95,14 @@ function isTrailingLineCommentWithinPrintWidth(text, comment, options) {
88
95
  function getLineIndent(linePrefix) {
89
96
  return /^[ \t]*/u.exec(linePrefix)?.[0] ?? '';
90
97
  }
98
+ function getTrailingCommentIndent(codeText, linePrefix, options) {
99
+ const indent = getLineIndent(linePrefix);
100
+ if (!isClosingDelimiterLine(codeText)) {
101
+ return indent;
102
+ }
103
+ const tabWidth = getTabWidth(options);
104
+ return makeIndent(getColumns(indent, tabWidth) + tabWidth, options);
105
+ }
106
+ function isClosingDelimiterLine(codeText) {
107
+ return /^[ \t]*[\])}]+[\])};,]*[ \t]*$/u.test(codeText);
108
+ }
@@ -1,2 +1,3 @@
1
1
  import type { WrapOptions } from '../shared/types.js';
2
2
  export declare function wrapComments<T>(text: string, ast: T, options: WrapOptions): Promise<string>;
3
+ export declare function neutralizePrettierIgnoreForIgnoredBlockComments<T>(text: string, ast: T): T;
@@ -1,13 +1,25 @@
1
1
  import { wrapBlockComment } from './block.js';
2
- import { collectComments, toCommentRange } from './core.js';
2
+ import { collectComments, hasPreserveCommentMarker, isDirectiveComment, isPrettierIgnoreComment, normalizeBlockCommentBody, normalizeLineCommentBody, toCommentRange, } from './core.js';
3
3
  import { areAdjacentLineComments, isStandaloneLineComment, shouldSkipLineComment, wrapLineCommentGroup, wrapTrailingLineComment, } from './line.js';
4
4
  import { getTabWidth } from '../shared/options.js';
5
- import { applyReplacements, getColumnAt } from '../shared/text.js';
5
+ import { applyReplacements, getColumnAt, getColumns, getLineEnd, getLinePrefix, getLineStart, isStandaloneBlockComment, } from '../shared/text.js';
6
+ const NEUTRALIZED_PRETTIER_IGNORE_COMMENT = 'prettier-ignore wrap-comments';
7
+ const AST_TRAVERSAL_SKIP_KEYS = new Set([
8
+ 'comments',
9
+ 'errors',
10
+ 'innerComments',
11
+ 'leadingComments',
12
+ 'loc',
13
+ 'parent',
14
+ 'range',
15
+ 'tokens',
16
+ 'trailingComments',
17
+ ]);
6
18
  export async function wrapComments(text, ast, options) {
7
- const comments = collectComments(ast)
8
- .map((comment) => toCommentRange(comment, text))
9
- .filter((comment) => comment !== undefined)
10
- .sort((left, right) => left.start - right.start);
19
+ const commentEntries = collectSortedCommentEntries(ast, text);
20
+ const comments = commentEntries.map((entry) => entry.range);
21
+ const jsxExpressionContainers = collectJsxExpressionContainerRanges(ast);
22
+ const ignoredLineRanges = collectPrettierIgnoredLineRanges(text, ast, commentEntries);
11
23
  if (comments.length === 0) {
12
24
  return text;
13
25
  }
@@ -18,9 +30,22 @@ export async function wrapComments(text, ast, options) {
18
30
  if (comment === undefined) {
19
31
  continue;
20
32
  }
33
+ if (isCommentInIgnoredLineRange(comment, ignoredLineRanges)) {
34
+ continue;
35
+ }
21
36
  if (comment.kind === 'block') {
22
- const replacement = await wrapBlockComment(text, comment, options);
23
- if (replacement !== undefined) {
37
+ if (isPrettierIgnoredBlockComment(text, commentEntries, index)) {
38
+ continue;
39
+ }
40
+ const jsxLayout = getJsxExpressionBlockCommentLayout(text, comment, jsxExpressionContainers, tabWidth);
41
+ if (jsxLayout?.placement === 'inline') {
42
+ continue;
43
+ }
44
+ const replacement = await wrapBlockComment(text, comment, options, jsxLayout);
45
+ if (Array.isArray(replacement)) {
46
+ replacements.push(...replacement);
47
+ }
48
+ else if (replacement !== undefined) {
24
49
  replacements.push(replacement);
25
50
  }
26
51
  continue;
@@ -29,9 +54,12 @@ export async function wrapComments(text, ast, options) {
29
54
  continue;
30
55
  }
31
56
  if (!isStandaloneLineComment(text, comment)) {
57
+ if (isPrettierIgnoredTrailingLineComment(text, commentEntries, index)) {
58
+ continue;
59
+ }
32
60
  const replacement = await wrapTrailingLineComment(text, comment, options);
33
61
  if (replacement !== undefined) {
34
- replacements.push(replacement);
62
+ replacements.push(...replacement);
35
63
  }
36
64
  continue;
37
65
  }
@@ -60,3 +88,289 @@ export async function wrapComments(text, ast, options) {
60
88
  }
61
89
  return applyReplacements(text, replacements);
62
90
  }
91
+ export function neutralizePrettierIgnoreForIgnoredBlockComments(text, ast) {
92
+ const comments = collectSortedCommentEntries(ast, text);
93
+ for (let index = 0; index < comments.length; index += 1) {
94
+ const entry = comments[index];
95
+ const previousEntry = comments[index - 1];
96
+ if (entry !== undefined &&
97
+ previousEntry !== undefined &&
98
+ isPrettierIgnoredBlockComment(text, comments, index) &&
99
+ !isBlockCommentNormallyIgnored(text, entry.range)) {
100
+ previousEntry.raw.value = NEUTRALIZED_PRETTIER_IGNORE_COMMENT;
101
+ }
102
+ }
103
+ return ast;
104
+ }
105
+ function collectSortedCommentEntries(ast, text) {
106
+ return collectComments(ast)
107
+ .map((raw) => ({ range: toCommentRange(raw, text), raw }))
108
+ .filter((entry) => entry.range !== undefined)
109
+ .sort((left, right) => left.range.start - right.range.start);
110
+ }
111
+ function collectPrettierIgnoredLineRanges(text, ast, comments) {
112
+ const nodeRanges = collectAstNodeRanges(ast);
113
+ const ignoredLineRanges = [];
114
+ for (let index = 0; index < comments.length; index += 1) {
115
+ const comment = comments[index]?.range;
116
+ if (comment === undefined ||
117
+ comment.kind !== 'line' ||
118
+ !isStandaloneLineComment(text, comment) ||
119
+ !isPrettierIgnoreComment(getCommentBody(text, comment))) {
120
+ continue;
121
+ }
122
+ const targetStart = getPrettierIgnoreTargetStart(text, comments, index);
123
+ if (targetStart === undefined) {
124
+ continue;
125
+ }
126
+ const targetRange = nodeRanges.find((range) => range.start === targetStart);
127
+ if (targetRange === undefined) {
128
+ continue;
129
+ }
130
+ ignoredLineRanges.push({
131
+ end: getLineEnd(text, targetRange.end),
132
+ start: getLineStart(text, targetRange.start),
133
+ });
134
+ }
135
+ return ignoredLineRanges;
136
+ }
137
+ function collectAstNodeRanges(ast) {
138
+ const ranges = [];
139
+ const seen = new Set();
140
+ visit(ast);
141
+ return ranges.sort((left, right) => left.start - right.start || right.end - left.end);
142
+ function visit(value) {
143
+ if (!isRecord(value) || seen.has(value)) {
144
+ return;
145
+ }
146
+ seen.add(value);
147
+ const range = getAstNodeRange(value);
148
+ if (range !== undefined && typeof value['type'] === 'string') {
149
+ ranges.push(range);
150
+ }
151
+ for (const [key, child] of Object.entries(value)) {
152
+ if (AST_TRAVERSAL_SKIP_KEYS.has(key)) {
153
+ continue;
154
+ }
155
+ if (Array.isArray(child)) {
156
+ for (const item of child) {
157
+ visit(item);
158
+ }
159
+ }
160
+ else {
161
+ visit(child);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ function collectJsxExpressionContainerRanges(ast) {
167
+ const ranges = [];
168
+ const seen = new Set();
169
+ visit(ast);
170
+ return ranges.sort((left, right) => left.start - right.start || right.end - left.end);
171
+ function visit(value) {
172
+ if (!isRecord(value) || seen.has(value)) {
173
+ return;
174
+ }
175
+ seen.add(value);
176
+ if (value['type'] === 'JSXExpressionContainer') {
177
+ const range = getAstNodeRange(value);
178
+ if (range !== undefined) {
179
+ ranges.push(range);
180
+ }
181
+ }
182
+ for (const [key, child] of Object.entries(value)) {
183
+ if (AST_TRAVERSAL_SKIP_KEYS.has(key)) {
184
+ continue;
185
+ }
186
+ if (Array.isArray(child)) {
187
+ for (const item of child) {
188
+ visit(item);
189
+ }
190
+ }
191
+ else {
192
+ visit(child);
193
+ }
194
+ }
195
+ }
196
+ }
197
+ function getAstNodeRange(node) {
198
+ const start = numberOrUndefined(node['start']) ?? getRangeNumber(node['range'], 0);
199
+ const end = numberOrUndefined(node['end']) ?? getRangeNumber(node['range'], 1);
200
+ if (start === undefined || end === undefined || start >= end) {
201
+ return undefined;
202
+ }
203
+ return { end, start };
204
+ }
205
+ function getRangeNumber(range, index) {
206
+ if (!Array.isArray(range)) {
207
+ return undefined;
208
+ }
209
+ return numberOrUndefined(range[index]);
210
+ }
211
+ function getPrettierIgnoreTargetStart(text, comments, ignoreCommentIndex) {
212
+ const ignoreComment = comments[ignoreCommentIndex]?.range;
213
+ if (ignoreComment === undefined) {
214
+ return undefined;
215
+ }
216
+ let cursor = ignoreComment.end;
217
+ for (let index = ignoreCommentIndex + 1; index < comments.length; index += 1) {
218
+ cursor = skipWhitespace(text, cursor);
219
+ const comment = comments[index]?.range;
220
+ if (comment === undefined || comment.start !== cursor) {
221
+ break;
222
+ }
223
+ if (!isStandaloneComment(text, comment) || !isCommentNormallyIgnored(text, comment)) {
224
+ return undefined;
225
+ }
226
+ cursor = comment.end;
227
+ }
228
+ const targetStart = skipWhitespace(text, cursor);
229
+ return targetStart >= text.length ? undefined : targetStart;
230
+ }
231
+ function skipWhitespace(text, index) {
232
+ let cursor = index;
233
+ while (cursor < text.length) {
234
+ const character = text[cursor];
235
+ if (character === undefined || !/\s/u.test(character)) {
236
+ break;
237
+ }
238
+ cursor += 1;
239
+ }
240
+ return cursor;
241
+ }
242
+ function isCommentInIgnoredLineRange(comment, ignoredLineRanges) {
243
+ return ignoredLineRanges.some((range) => comment.start >= range.start && comment.start < range.end);
244
+ }
245
+ function getJsxExpressionBlockCommentLayout(text, comment, jsxExpressionContainers, tabWidth) {
246
+ const container = getSmallestContainingRange(comment, jsxExpressionContainers);
247
+ if (container === undefined || text[container.start] !== '{' || text[container.end - 1] !== '}') {
248
+ return undefined;
249
+ }
250
+ const expressionTextBeforeComment = text.slice(container.start + 1, comment.start).trim();
251
+ const expressionTextAfterComment = text.slice(comment.end, container.end - 1).trim();
252
+ const containerOutputColumn = getJsxExpressionContainerOutputColumn(text, container, tabWidth);
253
+ if (expressionTextBeforeComment === '' && expressionTextAfterComment === '') {
254
+ return { contentColumn: containerOutputColumn + tabWidth + 3, multilineIndent: '', placement: 'standalone' };
255
+ }
256
+ if (expressionTextBeforeComment !== '' && expressionTextAfterComment === '') {
257
+ const expressionStart = skipWhitespace(text, container.start + 1);
258
+ const expressionEnd = trimWhitespaceEnd(text, container.start + 1, comment.start);
259
+ const removalEnd = Math.min(skipWhitespace(text, comment.end), container.end - 1);
260
+ return {
261
+ contentColumn: containerOutputColumn + tabWidth + 3,
262
+ multilineIndent: '',
263
+ placement: 'trailing',
264
+ trailingMove: {
265
+ insertAt: expressionStart,
266
+ removeEnd: removalEnd,
267
+ removeStart: expressionEnd,
268
+ },
269
+ };
270
+ }
271
+ return { placement: 'inline' };
272
+ }
273
+ function getJsxExpressionContainerOutputColumn(text, container, tabWidth) {
274
+ const linePrefix = getLinePrefix(text, container.start);
275
+ if (/^[ \t]*$/u.test(linePrefix)) {
276
+ return getColumns(linePrefix, tabWidth);
277
+ }
278
+ const lineIndent = /^[ \t]*/u.exec(linePrefix)?.[0] ?? '';
279
+ return getColumns(lineIndent, tabWidth) + tabWidth;
280
+ }
281
+ function trimWhitespaceEnd(text, start, end) {
282
+ let cursor = end;
283
+ while (cursor > start) {
284
+ const character = text[cursor - 1];
285
+ if (character === undefined || !/\s/u.test(character)) {
286
+ break;
287
+ }
288
+ cursor -= 1;
289
+ }
290
+ return cursor;
291
+ }
292
+ function getSmallestContainingRange(comment, ranges) {
293
+ let containingRange;
294
+ for (const range of ranges) {
295
+ if (comment.start <= range.start || comment.end >= range.end) {
296
+ continue;
297
+ }
298
+ if (containingRange === undefined || range.end - range.start < containingRange.end - containingRange.start) {
299
+ containingRange = range;
300
+ }
301
+ }
302
+ return containingRange;
303
+ }
304
+ function isPrettierIgnoredBlockComment(text, comments, index) {
305
+ const comment = comments[index]?.range;
306
+ const previousComment = comments[index - 1]?.range;
307
+ if (comment === undefined || comment.kind !== 'block' || previousComment === undefined) {
308
+ return false;
309
+ }
310
+ if (!isStandaloneComment(text, previousComment) || !isAdjacentPreviousComment(text, previousComment, comment)) {
311
+ return false;
312
+ }
313
+ return isPrettierIgnoreComment(getCommentBody(text, previousComment));
314
+ }
315
+ function isPrettierIgnoredTrailingLineComment(text, comments, index) {
316
+ const comment = comments[index]?.range;
317
+ if (comment === undefined || comment.kind !== 'line' || isStandaloneLineComment(text, comment)) {
318
+ return false;
319
+ }
320
+ let cursor = getLineStart(text, comment.start);
321
+ for (let previousIndex = index - 1; previousIndex >= 0; previousIndex -= 1) {
322
+ const previousComment = comments[previousIndex]?.range;
323
+ if (previousComment !== undefined && previousComment.end > cursor) {
324
+ continue;
325
+ }
326
+ if (previousComment === undefined || !isStandaloneComment(text, previousComment)) {
327
+ return false;
328
+ }
329
+ if (!isAdjacentCommentBeforeIndex(text, previousComment, cursor)) {
330
+ return false;
331
+ }
332
+ const body = getCommentBody(text, previousComment);
333
+ if (previousComment.kind === 'line' && isPrettierIgnoreComment(body)) {
334
+ return true;
335
+ }
336
+ if (!isCommentNormallyIgnored(text, previousComment)) {
337
+ return false;
338
+ }
339
+ cursor = getLineStart(text, previousComment.start);
340
+ }
341
+ return false;
342
+ }
343
+ function isCommentNormallyIgnored(text, comment) {
344
+ if (comment.kind === 'line') {
345
+ const raw = text.slice(comment.start, comment.end);
346
+ return shouldSkipLineComment(text, comment) && !isPrettierIgnoreComment(normalizeLineCommentBody(raw.slice(2)));
347
+ }
348
+ return isBlockCommentNormallyIgnored(text, comment);
349
+ }
350
+ function isBlockCommentNormallyIgnored(text, comment) {
351
+ const raw = text.slice(comment.start, comment.end);
352
+ if (raw.startsWith('/**') || hasPreserveCommentMarker(raw)) {
353
+ return true;
354
+ }
355
+ const body = normalizeBlockCommentBody(raw);
356
+ return body.trim() === '' || isDirectiveComment(body);
357
+ }
358
+ function isStandaloneComment(text, comment) {
359
+ return comment.kind === 'line' ? isStandaloneLineComment(text, comment) : isStandaloneBlockComment(text, comment);
360
+ }
361
+ function isAdjacentPreviousComment(text, previousComment, comment) {
362
+ return /^(?:\r\n|\n|\r)[ \t]*$/u.test(text.slice(previousComment.end, comment.start));
363
+ }
364
+ function isAdjacentCommentBeforeIndex(text, comment, index) {
365
+ return /^(?:\r\n|\n|\r)[ \t]*$/u.test(text.slice(comment.end, index));
366
+ }
367
+ function getCommentBody(text, comment) {
368
+ const raw = text.slice(comment.start, comment.end);
369
+ return comment.kind === 'line' ? normalizeLineCommentBody(raw.slice(2)) : normalizeBlockCommentBody(raw);
370
+ }
371
+ function isRecord(value) {
372
+ return typeof value === 'object' && value !== null;
373
+ }
374
+ function numberOrUndefined(value) {
375
+ return typeof value === 'number' ? value : undefined;
376
+ }
package/dist/index.d.ts CHANGED
@@ -3,9 +3,8 @@ declare const parsers: {
3
3
  [parserName: string]: import("prettier").Parser<any>;
4
4
  } | undefined;
5
5
  declare const printers: {
6
- estree: import("prettier").Printer;
7
- "estree-json": import("prettier").Printer;
8
- };
6
+ [astFormat: string]: import("prettier").Printer<any>;
7
+ } | undefined;
9
8
  declare const plugin: Plugin;
10
9
  export { parsers, printers };
11
10
  export default plugin;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import * as estreePlugin from 'prettier/plugins/estree';
2
1
  import { buildParsers } from './plugin/parsers.js';
2
+ import { buildPrinters } from './plugin/printers.js';
3
3
  const parsers = buildParsers();
4
- const printers = estreePlugin.printers;
4
+ const printers = buildPrinters();
5
5
  const plugin = {
6
6
  parsers,
7
7
  printers,
@@ -1,10 +1,14 @@
1
1
  import * as babelPlugin from 'prettier/plugins/babel';
2
2
  import * as typescriptPlugin from 'prettier/plugins/typescript';
3
- import { wrapComments } from '../comments/wrap.js';
3
+ import { neutralizePrettierIgnoreForIgnoredBlockComments, wrapComments } from '../comments/wrap.js';
4
4
  const parserNames = ['babel', 'babel-ts', 'typescript'];
5
5
  function wrapParser(parser) {
6
6
  return {
7
7
  ...parser,
8
+ async parse(text, options) {
9
+ const ast = await parser.parse(text, options);
10
+ return neutralizePrettierIgnoreForIgnoredBlockComments(text, ast);
11
+ },
8
12
  async preprocess(text, options) {
9
13
  const preprocessed = parser.preprocess === undefined ? text : await parser.preprocess(text, options);
10
14
  let ast;
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'prettier';
2
+ export declare function buildPrinters(): Plugin['printers'];
@@ -0,0 +1,39 @@
1
+ import { doc } from 'prettier';
2
+ import * as estreePlugin from 'prettier/plugins/estree';
3
+ const { hardline, indent } = doc.builders;
4
+ export function buildPrinters() {
5
+ const estreePrinter = estreePlugin.printers.estree;
6
+ return {
7
+ ...estreePlugin.printers,
8
+ estree: {
9
+ ...estreePrinter,
10
+ print(path, options, print, args) {
11
+ if (isMultilineEmptyJsxExpressionBlockComment(path.node)) {
12
+ return ['{', indent([hardline, path.call(print, 'expression')]), hardline, '}'];
13
+ }
14
+ return estreePrinter.print(path, options, print, args);
15
+ },
16
+ },
17
+ };
18
+ }
19
+ function isMultilineEmptyJsxExpressionBlockComment(node) {
20
+ if (!isRecord(node) || node['type'] !== 'JSXExpressionContainer') {
21
+ return false;
22
+ }
23
+ const expression = node['expression'];
24
+ if (!isRecord(expression) || expression['type'] !== 'JSXEmptyExpression') {
25
+ return false;
26
+ }
27
+ const comments = expression['comments'];
28
+ return Array.isArray(comments) && comments.some(isMultilineBlockComment);
29
+ }
30
+ function isMultilineBlockComment(comment) {
31
+ if (!isRecord(comment) || (comment['type'] !== 'Block' && comment['type'] !== 'CommentBlock')) {
32
+ return false;
33
+ }
34
+ const value = comment['value'];
35
+ return typeof value === 'string' && value.includes('\n');
36
+ }
37
+ function isRecord(value) {
38
+ return typeof value === 'object' && value !== null;
39
+ }
@@ -12,3 +12,4 @@ export declare function isStandaloneBlockComment(text: string, comment: {
12
12
  start: number;
13
13
  }): boolean;
14
14
  export declare function trimBlankEdges(markdown: string): string;
15
+ export declare function makeIndent(column: number, options: WrapOptions): string;
@@ -1,11 +1,26 @@
1
1
  import { getTabWidth } from './options.js';
2
2
  export function applyReplacements(text, replacements) {
3
3
  let result = text;
4
- for (const replacement of [...replacements].sort((left, right) => right.start - left.start)) {
4
+ for (const replacement of getNonOverlappingReplacements(replacements).sort((left, right) => right.start - left.start)) {
5
5
  result = result.slice(0, replacement.start) + replacement.text + result.slice(replacement.end);
6
6
  }
7
7
  return result;
8
8
  }
9
+ function getNonOverlappingReplacements(replacements) {
10
+ return [...replacements]
11
+ .sort((left, right) => left.start - right.start || right.end - left.end)
12
+ .reduce((accepted, replacement) => {
13
+ const previous = accepted.at(-1);
14
+ if (previous === undefined || !rangesOverlap(previous, replacement)) {
15
+ accepted.push(replacement);
16
+ return accepted;
17
+ }
18
+ return accepted;
19
+ }, []);
20
+ }
21
+ function rangesOverlap(left, right) {
22
+ return left.start < right.end && right.start < left.end;
23
+ }
9
24
  export function getPreferredNewline(text, options) {
10
25
  if (options.endOfLine === 'crlf') {
11
26
  return '\r\n';
@@ -73,7 +88,7 @@ export function trimBlankEdges(markdown) {
73
88
  function isBlankLine(line) {
74
89
  return line !== undefined && line.trim() === '';
75
90
  }
76
- function makeIndent(column, options) {
91
+ export function makeIndent(column, options) {
77
92
  const tabWidth = getTabWidth(options);
78
93
  if (options.useTabs === true) {
79
94
  const tabs = Math.floor(column / tabWidth);
package/package.json CHANGED
@@ -51,5 +51,5 @@
51
51
  },
52
52
  "type": "module",
53
53
  "types": "./dist/index.d.ts",
54
- "version": "1.0.2"
54
+ "version": "1.0.4"
55
55
  }