@aforemendude/prettier-plugin-wrap-comments 1.0.2
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 +79 -0
- package/dist/comments/block.d.ts +2 -0
- package/dist/comments/block.js +43 -0
- package/dist/comments/core.d.ts +6 -0
- package/dist/comments/core.js +74 -0
- package/dist/comments/line.d.ts +6 -0
- package/dist/comments/line.js +90 -0
- package/dist/comments/wrap.d.ts +2 -0
- package/dist/comments/wrap.js +62 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/plugin/parsers.d.ts +2 -0
- package/dist/plugin/parsers.js +31 -0
- package/dist/shared/markdown.d.ts +2 -0
- package/dist/shared/markdown.js +20 -0
- package/dist/shared/options.d.ts +4 -0
- package/dist/shared/options.js +11 -0
- package/dist/shared/text.d.ts +14 -0
- package/dist/shared/text.js +84 -0
- package/dist/shared/types.d.ts +30 -0
- package/dist/shared/types.js +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @aforemendude/prettier-plugin-wrap-comments
|
|
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.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install --save-dev @aforemendude/prettier-plugin-wrap-comments prettier
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Use
|
|
13
|
+
|
|
14
|
+
Add the plugin to your Prettier config:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"plugins": ["@aforemendude/prettier-plugin-wrap-comments"]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run Prettier normally:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npx prettier --write .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Behavior
|
|
29
|
+
|
|
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.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
function example() {
|
|
35
|
+
// This long paragraph is wrapped as Markdown, and the nested indentation is
|
|
36
|
+
// subtracted from the configured print width.
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
JSDoc comments are left unchanged:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
/**
|
|
45
|
+
* This documentation comment is not wrapped by the plugin.
|
|
46
|
+
*/
|
|
47
|
+
```
|
|
48
|
+
|
|
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.
|
|
53
|
+
|
|
54
|
+
## Supported Parsers
|
|
55
|
+
|
|
56
|
+
- `babel`
|
|
57
|
+
- `babel-ts`
|
|
58
|
+
- `typescript`
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
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
|
|
76
|
+
```
|
|
77
|
+
|
|
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.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isDirectiveComment, normalizeBlockCommentBody } from './core.js';
|
|
2
|
+
import { formatMarkdownLines } from '../shared/markdown.js';
|
|
3
|
+
import { getAvailableContentWidth, getPrintWidth, getTabWidth } from '../shared/options.js';
|
|
4
|
+
import { getColumnAt, getColumns, getLinePrefix, getPreferredNewline, isStandaloneBlockComment, } from '../shared/text.js';
|
|
5
|
+
export async function wrapBlockComment(text, comment, options) {
|
|
6
|
+
const raw = text.slice(comment.start, comment.end);
|
|
7
|
+
if (raw.startsWith('/**')) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const markdown = normalizeBlockCommentBody(raw);
|
|
11
|
+
if (markdown.trim() === '' || isDirectiveComment(markdown)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const tabWidth = getTabWidth(options);
|
|
15
|
+
const markerColumn = getColumnAt(text, comment.start, tabWidth);
|
|
16
|
+
const availableWidth = getAvailableContentWidth(options, markerColumn + 3);
|
|
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)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
end: comment.end,
|
|
24
|
+
start: comment.start,
|
|
25
|
+
text: replacementText,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function buildBlockReplacement(text, comment, formattedLines, options) {
|
|
29
|
+
const tabWidth = getTabWidth(options);
|
|
30
|
+
const markerColumn = getColumnAt(text, comment.start, tabWidth);
|
|
31
|
+
const singleLine = `/* ${formattedLines.join(' ')} */`;
|
|
32
|
+
const singleLineWidth = getColumns(singleLine, tabWidth);
|
|
33
|
+
if (formattedLines.length === 1 && markerColumn + singleLineWidth <= getPrintWidth(options)) {
|
|
34
|
+
return singleLine;
|
|
35
|
+
}
|
|
36
|
+
if (!isStandaloneBlockComment(text, comment)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const newline = getPreferredNewline(text, options);
|
|
40
|
+
const indent = getLinePrefix(text, comment.start);
|
|
41
|
+
const body = formattedLines.map((line) => `${indent} *${line.length === 0 ? '' : ` ${line}`}`).join(newline);
|
|
42
|
+
return `/*${newline}${body}${newline}${indent} */`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CommentRange, RawComment } from '../shared/types.js';
|
|
2
|
+
export declare function collectComments(ast: unknown): RawComment[];
|
|
3
|
+
export declare function toCommentRange(comment: RawComment, text: string): CommentRange | undefined;
|
|
4
|
+
export declare function normalizeLineCommentBody(rawBody: string): string;
|
|
5
|
+
export declare function normalizeBlockCommentBody(rawComment: string): string;
|
|
6
|
+
export declare function isDirectiveComment(body: string): boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function collectComments(ast) {
|
|
2
|
+
const candidate = ast;
|
|
3
|
+
if (Array.isArray(candidate.comments)) {
|
|
4
|
+
return candidate.comments;
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(candidate.program?.comments)) {
|
|
7
|
+
return candidate.program.comments;
|
|
8
|
+
}
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
export function toCommentRange(comment, text) {
|
|
12
|
+
const range = Array.isArray(comment.range) ? comment.range : undefined;
|
|
13
|
+
const start = numberOrUndefined(comment.start) ?? numberOrUndefined(range?.[0]);
|
|
14
|
+
const end = numberOrUndefined(comment.end) ?? numberOrUndefined(range?.[1]);
|
|
15
|
+
if (start === undefined || end === undefined || start >= end) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const rawStart = text.slice(start, start + 3);
|
|
19
|
+
if (rawStart.startsWith('//')) {
|
|
20
|
+
return { end, kind: 'line', start };
|
|
21
|
+
}
|
|
22
|
+
if (rawStart.startsWith('/*')) {
|
|
23
|
+
return { end, kind: 'block', start };
|
|
24
|
+
}
|
|
25
|
+
if (typeof comment.type === 'string') {
|
|
26
|
+
if (comment.type.includes('Line')) {
|
|
27
|
+
return { end, kind: 'line', start };
|
|
28
|
+
}
|
|
29
|
+
if (comment.type.includes('Block')) {
|
|
30
|
+
return { end, kind: 'block', start };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
export function normalizeLineCommentBody(rawBody) {
|
|
36
|
+
if (rawBody.trim() === '') {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
return rawBody.replace(/^[ \t]?/, '').replace(/[ \t]+$/u, '');
|
|
40
|
+
}
|
|
41
|
+
export function normalizeBlockCommentBody(rawComment) {
|
|
42
|
+
const body = rawComment.slice(2, -2).replace(/\r\n?/g, '\n');
|
|
43
|
+
const lines = body.split('\n');
|
|
44
|
+
if (lines.length === 1) {
|
|
45
|
+
return lines[0]?.trim() ?? '';
|
|
46
|
+
}
|
|
47
|
+
while (isBlankLine(lines[0])) {
|
|
48
|
+
lines.shift();
|
|
49
|
+
}
|
|
50
|
+
while (isBlankLine(lines.at(-1))) {
|
|
51
|
+
lines.pop();
|
|
52
|
+
}
|
|
53
|
+
return lines
|
|
54
|
+
.map((line) => {
|
|
55
|
+
const withoutIndent = line.replace(/^[ \t]*/u, '');
|
|
56
|
+
if (!withoutIndent.startsWith('*')) {
|
|
57
|
+
return withoutIndent.replace(/[ \t]+$/u, '');
|
|
58
|
+
}
|
|
59
|
+
return withoutIndent
|
|
60
|
+
.slice(1)
|
|
61
|
+
.replace(/^[ \t]?/u, '')
|
|
62
|
+
.replace(/[ \t]+$/u, '');
|
|
63
|
+
})
|
|
64
|
+
.join('\n');
|
|
65
|
+
}
|
|
66
|
+
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());
|
|
68
|
+
}
|
|
69
|
+
function isBlankLine(line) {
|
|
70
|
+
return line !== undefined && line.trim() === '';
|
|
71
|
+
}
|
|
72
|
+
function numberOrUndefined(value) {
|
|
73
|
+
return typeof value === 'number' ? value : undefined;
|
|
74
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CommentRange, Replacement, WrapOptions } from '../shared/types.js';
|
|
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>;
|
|
4
|
+
export declare function shouldSkipLineComment(text: string, comment: CommentRange): boolean;
|
|
5
|
+
export declare function isStandaloneLineComment(text: string, comment: CommentRange): boolean;
|
|
6
|
+
export declare function areAdjacentLineComments(text: string, previous: CommentRange, next: CommentRange): boolean;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isDirectiveComment, normalizeLineCommentBody } from './core.js';
|
|
2
|
+
import { formatMarkdownLines } from '../shared/markdown.js';
|
|
3
|
+
import { getAvailableContentWidth, getPrintWidth, getTabWidth } from '../shared/options.js';
|
|
4
|
+
import { getColumnAt, getColumns, getContinuationIndent, getLineEnd, getLinePrefix, getLineStart, getPreferredNewline, } from '../shared/text.js';
|
|
5
|
+
export async function wrapLineCommentGroup(text, comments, options) {
|
|
6
|
+
const firstComment = comments[0];
|
|
7
|
+
if (firstComment === undefined) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const lastComment = comments.at(-1) ?? firstComment;
|
|
11
|
+
const bodyLines = comments.map((comment) => normalizeLineCommentBody(text.slice(comment.start + 2, comment.end)));
|
|
12
|
+
if (bodyLines.every((line) => line.trim() === '')) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const tabWidth = getTabWidth(options);
|
|
16
|
+
const markerColumn = getColumnAt(text, firstComment.start, tabWidth);
|
|
17
|
+
const availableWidth = getAvailableContentWidth(options, markerColumn + 3);
|
|
18
|
+
const formattedLines = await formatMarkdownLines(bodyLines.join('\n'), availableWidth, options);
|
|
19
|
+
const newline = getPreferredNewline(text, options);
|
|
20
|
+
const continuationIndent = getContinuationIndent(text, firstComment.start, markerColumn, options);
|
|
21
|
+
const replacementText = formattedLines
|
|
22
|
+
.map((line, index) => {
|
|
23
|
+
const commentText = line.length === 0 ? '//' : `// ${line}`;
|
|
24
|
+
return index === 0 ? commentText : `${newline}${continuationIndent}${commentText}`;
|
|
25
|
+
})
|
|
26
|
+
.join('');
|
|
27
|
+
const start = firstComment.start;
|
|
28
|
+
const end = lastComment.end;
|
|
29
|
+
if (replacementText === text.slice(start, end)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
end,
|
|
34
|
+
start,
|
|
35
|
+
text: replacementText,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export async function wrapTrailingLineComment(text, comment, options) {
|
|
39
|
+
if (isTrailingLineCommentWithinPrintWidth(text, comment, options)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const lineStart = getLineStart(text, comment.start);
|
|
43
|
+
const lineEnd = getLineEnd(text, comment.end);
|
|
44
|
+
const linePrefix = text.slice(lineStart, comment.start);
|
|
45
|
+
const codeText = linePrefix.replace(/[ \t]+$/u, '');
|
|
46
|
+
if (codeText.trim() === '') {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const body = normalizeLineCommentBody(text.slice(comment.start + 2, comment.end));
|
|
50
|
+
if (body.trim() === '') {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const tabWidth = getTabWidth(options);
|
|
54
|
+
const indent = getLineIndent(linePrefix);
|
|
55
|
+
const availableWidth = getAvailableContentWidth(options, getColumns(indent, tabWidth) + 3);
|
|
56
|
+
const formattedLines = await formatMarkdownLines(body, availableWidth, options);
|
|
57
|
+
const newline = getPreferredNewline(text, options);
|
|
58
|
+
const leadingCommentText = formattedLines
|
|
59
|
+
.map((line) => `${indent}${line.length === 0 ? '//' : `// ${line}`}`)
|
|
60
|
+
.join(newline);
|
|
61
|
+
const replacementText = `${leadingCommentText}${newline}${codeText}`;
|
|
62
|
+
return {
|
|
63
|
+
end: lineEnd,
|
|
64
|
+
start: lineStart,
|
|
65
|
+
text: replacementText,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function shouldSkipLineComment(text, comment) {
|
|
69
|
+
const raw = text.slice(comment.start, comment.end);
|
|
70
|
+
if (raw.startsWith('///')) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return isDirectiveComment(normalizeLineCommentBody(raw.slice(2)));
|
|
74
|
+
}
|
|
75
|
+
export function isStandaloneLineComment(text, comment) {
|
|
76
|
+
return /^[ \t]*$/u.test(getLinePrefix(text, comment.start));
|
|
77
|
+
}
|
|
78
|
+
export function areAdjacentLineComments(text, previous, next) {
|
|
79
|
+
return /^(?:\r\n|\n|\r)[ \t]*$/u.test(text.slice(previous.end, next.start));
|
|
80
|
+
}
|
|
81
|
+
function isTrailingLineCommentWithinPrintWidth(text, comment, options) {
|
|
82
|
+
const tabWidth = getTabWidth(options);
|
|
83
|
+
const lineStart = getLineStart(text, comment.start);
|
|
84
|
+
const lineEnd = getLineEnd(text, comment.end);
|
|
85
|
+
const lineText = text.slice(lineStart, lineEnd).replace(/[ \t]+$/u, '');
|
|
86
|
+
return getColumns(lineText, tabWidth) <= getPrintWidth(options);
|
|
87
|
+
}
|
|
88
|
+
function getLineIndent(linePrefix) {
|
|
89
|
+
return /^[ \t]*/u.exec(linePrefix)?.[0] ?? '';
|
|
90
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { wrapBlockComment } from './block.js';
|
|
2
|
+
import { collectComments, toCommentRange } from './core.js';
|
|
3
|
+
import { areAdjacentLineComments, isStandaloneLineComment, shouldSkipLineComment, wrapLineCommentGroup, wrapTrailingLineComment, } from './line.js';
|
|
4
|
+
import { getTabWidth } from '../shared/options.js';
|
|
5
|
+
import { applyReplacements, getColumnAt } from '../shared/text.js';
|
|
6
|
+
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);
|
|
11
|
+
if (comments.length === 0) {
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
const replacements = [];
|
|
15
|
+
const tabWidth = getTabWidth(options);
|
|
16
|
+
for (let index = 0; index < comments.length; index += 1) {
|
|
17
|
+
const comment = comments[index];
|
|
18
|
+
if (comment === undefined) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (comment.kind === 'block') {
|
|
22
|
+
const replacement = await wrapBlockComment(text, comment, options);
|
|
23
|
+
if (replacement !== undefined) {
|
|
24
|
+
replacements.push(replacement);
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (shouldSkipLineComment(text, comment)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!isStandaloneLineComment(text, comment)) {
|
|
32
|
+
const replacement = await wrapTrailingLineComment(text, comment, options);
|
|
33
|
+
if (replacement !== undefined) {
|
|
34
|
+
replacements.push(replacement);
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const group = [comment];
|
|
39
|
+
let previousComment = comment;
|
|
40
|
+
while (index + 1 < comments.length) {
|
|
41
|
+
const nextComment = comments[index + 1];
|
|
42
|
+
if (nextComment === undefined) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (nextComment.kind !== 'line' ||
|
|
46
|
+
!isStandaloneLineComment(text, nextComment) ||
|
|
47
|
+
shouldSkipLineComment(text, nextComment) ||
|
|
48
|
+
!areAdjacentLineComments(text, previousComment, nextComment) ||
|
|
49
|
+
getColumnAt(text, comment.start, tabWidth) !== getColumnAt(text, nextComment.start, tabWidth)) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
group.push(nextComment);
|
|
53
|
+
previousComment = nextComment;
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
const replacement = await wrapLineCommentGroup(text, group, options);
|
|
57
|
+
if (replacement !== undefined) {
|
|
58
|
+
replacements.push(replacement);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return applyReplacements(text, replacements);
|
|
62
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from 'prettier';
|
|
2
|
+
declare const parsers: {
|
|
3
|
+
[parserName: string]: import("prettier").Parser<any>;
|
|
4
|
+
} | undefined;
|
|
5
|
+
declare const printers: {
|
|
6
|
+
estree: import("prettier").Printer;
|
|
7
|
+
"estree-json": import("prettier").Printer;
|
|
8
|
+
};
|
|
9
|
+
declare const plugin: Plugin;
|
|
10
|
+
export { parsers, printers };
|
|
11
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as estreePlugin from 'prettier/plugins/estree';
|
|
2
|
+
import { buildParsers } from './plugin/parsers.js';
|
|
3
|
+
const parsers = buildParsers();
|
|
4
|
+
const printers = estreePlugin.printers;
|
|
5
|
+
const plugin = {
|
|
6
|
+
parsers,
|
|
7
|
+
printers,
|
|
8
|
+
};
|
|
9
|
+
export { parsers, printers };
|
|
10
|
+
export default plugin;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as babelPlugin from 'prettier/plugins/babel';
|
|
2
|
+
import * as typescriptPlugin from 'prettier/plugins/typescript';
|
|
3
|
+
import { wrapComments } from '../comments/wrap.js';
|
|
4
|
+
const parserNames = ['babel', 'babel-ts', 'typescript'];
|
|
5
|
+
function wrapParser(parser) {
|
|
6
|
+
return {
|
|
7
|
+
...parser,
|
|
8
|
+
async preprocess(text, options) {
|
|
9
|
+
const preprocessed = parser.preprocess === undefined ? text : await parser.preprocess(text, options);
|
|
10
|
+
let ast;
|
|
11
|
+
try {
|
|
12
|
+
ast = await parser.parse(preprocessed, options);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return preprocessed;
|
|
16
|
+
}
|
|
17
|
+
return wrapComments(preprocessed, ast, options);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function buildParsers() {
|
|
22
|
+
const parsers = {};
|
|
23
|
+
for (const parserName of parserNames) {
|
|
24
|
+
const sourceParsers = parserName === 'typescript' ? typescriptPlugin.parsers : babelPlugin.parsers;
|
|
25
|
+
const parser = sourceParsers[parserName];
|
|
26
|
+
if (parser !== undefined) {
|
|
27
|
+
parsers[parserName] = wrapParser(parser);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return parsers;
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { format } from 'prettier';
|
|
2
|
+
import { getTabWidth } from './options.js';
|
|
3
|
+
import { trimBlankEdges } from './text.js';
|
|
4
|
+
export async function formatMarkdownLines(markdown, printWidth, options) {
|
|
5
|
+
const normalized = trimBlankEdges(markdown.replace(/\r\n?/g, '\n'));
|
|
6
|
+
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');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return normalized.split('\n');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WrapOptions } from './types.js';
|
|
2
|
+
export declare function getAvailableContentWidth(options: WrapOptions, contentStartColumn: number): number;
|
|
3
|
+
export declare function getPrintWidth(options: WrapOptions): number;
|
|
4
|
+
export declare function getTabWidth(options: WrapOptions): number;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const DEFAULT_PRINT_WIDTH = 80;
|
|
2
|
+
const DEFAULT_TAB_WIDTH = 2;
|
|
3
|
+
export function getAvailableContentWidth(options, contentStartColumn) {
|
|
4
|
+
return Math.max(1, getPrintWidth(options) - contentStartColumn);
|
|
5
|
+
}
|
|
6
|
+
export function getPrintWidth(options) {
|
|
7
|
+
return typeof options.printWidth === 'number' ? options.printWidth : DEFAULT_PRINT_WIDTH;
|
|
8
|
+
}
|
|
9
|
+
export function getTabWidth(options) {
|
|
10
|
+
return typeof options.tabWidth === 'number' ? options.tabWidth : DEFAULT_TAB_WIDTH;
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Replacement, WrapOptions } from './types.js';
|
|
2
|
+
export declare function applyReplacements(text: string, replacements: Replacement[]): string;
|
|
3
|
+
export declare function getPreferredNewline(text: string, options: WrapOptions): string;
|
|
4
|
+
export declare function getContinuationIndent(text: string, commentStart: number, markerColumn: number, options: WrapOptions): string;
|
|
5
|
+
export declare function getLinePrefix(text: string, index: number): string;
|
|
6
|
+
export declare function getLineStart(text: string, index: number): number;
|
|
7
|
+
export declare function getLineEnd(text: string, index: number): number;
|
|
8
|
+
export declare function getColumnAt(text: string, index: number, tabWidth: number): number;
|
|
9
|
+
export declare function getColumns(text: string, tabWidth: number): number;
|
|
10
|
+
export declare function isStandaloneBlockComment(text: string, comment: {
|
|
11
|
+
end: number;
|
|
12
|
+
start: number;
|
|
13
|
+
}): boolean;
|
|
14
|
+
export declare function trimBlankEdges(markdown: string): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getTabWidth } from './options.js';
|
|
2
|
+
export function applyReplacements(text, replacements) {
|
|
3
|
+
let result = text;
|
|
4
|
+
for (const replacement of [...replacements].sort((left, right) => right.start - left.start)) {
|
|
5
|
+
result = result.slice(0, replacement.start) + replacement.text + result.slice(replacement.end);
|
|
6
|
+
}
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
export function getPreferredNewline(text, options) {
|
|
10
|
+
if (options.endOfLine === 'crlf') {
|
|
11
|
+
return '\r\n';
|
|
12
|
+
}
|
|
13
|
+
if (options.endOfLine === 'cr') {
|
|
14
|
+
return '\r';
|
|
15
|
+
}
|
|
16
|
+
if (options.endOfLine === 'auto') {
|
|
17
|
+
const match = /\r\n|\n|\r/u.exec(text);
|
|
18
|
+
return match?.[0] ?? '\n';
|
|
19
|
+
}
|
|
20
|
+
return '\n';
|
|
21
|
+
}
|
|
22
|
+
export function getContinuationIndent(text, commentStart, markerColumn, options) {
|
|
23
|
+
const linePrefix = getLinePrefix(text, commentStart);
|
|
24
|
+
if (/^[ \t]*$/u.test(linePrefix)) {
|
|
25
|
+
return linePrefix;
|
|
26
|
+
}
|
|
27
|
+
return makeIndent(markerColumn, options);
|
|
28
|
+
}
|
|
29
|
+
export function getLinePrefix(text, index) {
|
|
30
|
+
return text.slice(getLineStart(text, index), index);
|
|
31
|
+
}
|
|
32
|
+
export function getLineStart(text, index) {
|
|
33
|
+
const newlineIndex = text.lastIndexOf('\n', index - 1);
|
|
34
|
+
return newlineIndex === -1 ? 0 : newlineIndex + 1;
|
|
35
|
+
}
|
|
36
|
+
export function getLineEnd(text, index) {
|
|
37
|
+
const newlineIndex = text.indexOf('\n', index);
|
|
38
|
+
if (newlineIndex === -1) {
|
|
39
|
+
return text.length;
|
|
40
|
+
}
|
|
41
|
+
return text[newlineIndex - 1] === '\r' ? newlineIndex - 1 : newlineIndex;
|
|
42
|
+
}
|
|
43
|
+
export function getColumnAt(text, index, tabWidth) {
|
|
44
|
+
return getColumns(text.slice(getLineStart(text, index), index), tabWidth);
|
|
45
|
+
}
|
|
46
|
+
export function getColumns(text, tabWidth) {
|
|
47
|
+
let column = 0;
|
|
48
|
+
for (const character of text) {
|
|
49
|
+
if (character === '\t') {
|
|
50
|
+
column += tabWidth - (column % tabWidth);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
column += 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return column;
|
|
57
|
+
}
|
|
58
|
+
export function isStandaloneBlockComment(text, comment) {
|
|
59
|
+
const before = text.slice(getLineStart(text, comment.start), comment.start);
|
|
60
|
+
const after = text.slice(comment.end, getLineEnd(text, comment.end));
|
|
61
|
+
return /^[ \t]*$/u.test(before) && /^[ \t]*$/u.test(after);
|
|
62
|
+
}
|
|
63
|
+
export function trimBlankEdges(markdown) {
|
|
64
|
+
const lines = markdown.split('\n');
|
|
65
|
+
while (isBlankLine(lines[0])) {
|
|
66
|
+
lines.shift();
|
|
67
|
+
}
|
|
68
|
+
while (isBlankLine(lines.at(-1))) {
|
|
69
|
+
lines.pop();
|
|
70
|
+
}
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
function isBlankLine(line) {
|
|
74
|
+
return line !== undefined && line.trim() === '';
|
|
75
|
+
}
|
|
76
|
+
function makeIndent(column, options) {
|
|
77
|
+
const tabWidth = getTabWidth(options);
|
|
78
|
+
if (options.useTabs === true) {
|
|
79
|
+
const tabs = Math.floor(column / tabWidth);
|
|
80
|
+
const spaces = column % tabWidth;
|
|
81
|
+
return `${'\t'.repeat(tabs)}${' '.repeat(spaces)}`;
|
|
82
|
+
}
|
|
83
|
+
return ' '.repeat(column);
|
|
84
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ParserOptions } from 'prettier';
|
|
2
|
+
export type AstWithComments = {
|
|
3
|
+
comments?: unknown;
|
|
4
|
+
program?: {
|
|
5
|
+
comments?: unknown;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export type RawComment = {
|
|
9
|
+
end?: unknown;
|
|
10
|
+
loc?: {
|
|
11
|
+
start?: {
|
|
12
|
+
column?: unknown;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
range?: unknown;
|
|
16
|
+
start?: unknown;
|
|
17
|
+
type?: unknown;
|
|
18
|
+
value?: unknown;
|
|
19
|
+
};
|
|
20
|
+
export type CommentRange = {
|
|
21
|
+
end: number;
|
|
22
|
+
kind: 'block' | 'line';
|
|
23
|
+
start: number;
|
|
24
|
+
};
|
|
25
|
+
export type Replacement = {
|
|
26
|
+
end: number;
|
|
27
|
+
start: number;
|
|
28
|
+
text: string;
|
|
29
|
+
};
|
|
30
|
+
export type WrapOptions = Pick<ParserOptions, 'endOfLine' | 'printWidth' | 'tabWidth' | 'useTabs'>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "aforemendude",
|
|
3
|
+
"bugs": {
|
|
4
|
+
"url": "https://github.com/aforemendude/prettier-plugin-wrap-comments/issues"
|
|
5
|
+
},
|
|
6
|
+
"description": "A Prettier plugin that wraps non-JSDoc JavaScript and TypeScript comments as Markdown.",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"prettier": "3.8.3",
|
|
9
|
+
"typescript": "6.0.3"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"default": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://github.com/aforemendude/prettier-plugin-wrap-comments",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"prettier",
|
|
26
|
+
"prettier-plugin",
|
|
27
|
+
"comments",
|
|
28
|
+
"markdown",
|
|
29
|
+
"typescript"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"name": "@aforemendude/prettier-plugin-wrap-comments",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"prettier": ">=3.0.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/aforemendude/prettier-plugin-wrap-comments.git"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
46
|
+
"format": "prettier --write .",
|
|
47
|
+
"format:check": "prettier --check .",
|
|
48
|
+
"prepack": "npm run verify",
|
|
49
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
50
|
+
"verify": "npm install && npm run format:check && npm run test"
|
|
51
|
+
},
|
|
52
|
+
"type": "module",
|
|
53
|
+
"types": "./dist/index.d.ts",
|
|
54
|
+
"version": "1.0.2"
|
|
55
|
+
}
|